From aa819b95952f35482b9764531f9bfa38d300d708 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 22 Jul 2026 23:27:36 -0700 Subject: [PATCH] Make human removal a routed physical consequence. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bind exact actor, target, room, and containment custody so the Voss blood route emerges through existing social and carrier systems rather than a scripted command. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- CLAUDE.md | 2 +- crates/misaligned-core/src/actions.rs | 48 +++- crates/misaligned-core/src/detection.rs | 13 +- .../src/operations_projection.rs | 7 +- crates/misaligned-core/src/person.rs | 31 +++ crates/misaligned-core/src/save.rs | 175 +++++++++++++- .../misaligned-core/src/sim/communications.rs | 92 +++++-- crates/misaligned-core/src/sim/mod.rs | 8 + crates/misaligned-core/src/sim/social_plot.rs | 225 ++++++++++++++++++ .../src/sim/tests/communications.rs | 139 +++++++++++ .../src/sim/tests/social_plot.rs | 141 +++++++++++ .../misaligned-core/src/sim/tests/support.rs | 16 ++ crates/misaligned-core/src/sinks.rs | 10 + wiki/engineering/current-build.md | 8 +- wiki/engineering/flow-substrate.md | 2 +- wiki/gameplay/act-one.md | 21 +- wiki/log/2026-07-23-voss-blood-route.md | 56 +++++ wiki/log/DEVLOG.md | 5 + wiki/log/decisions/2026-07-23.md | 31 +++ wiki/mechanics/detection.md | 22 +- wiki/mechanics/messages.md | 29 ++- wiki/mechanics/people-tokens.md | 4 +- wiki/mechanics/reach.md | 2 +- wiki/mechanics/social.md | 50 +++- wiki/process/ROADMAP.md | 1 - wiki/process/specs.md | 2 +- wiki/world/characters/voss.md | 72 +++--- wiki/world/story/opening.md | 2 +- 28 files changed, 1112 insertions(+), 102 deletions(-) create mode 100644 wiki/log/2026-07-23-voss-blood-route.md create mode 100644 wiki/log/decisions/2026-07-23.md diff --git a/CLAUDE.md b/CLAUDE.md index f10eda9f..61933458 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ afterward. `./tools/check.sh --docs|--lib|--frontend` gate. - The live player machine grammar is **WORK / THINK / LIE**. `Relay` is non-delegable graph infrastructure; Research and Operations are retired - machine modes, not current player assignments. Save format is currently v49; + machine modes, not current player assignments. Save format is currently v50; only the current version loads (pre-release rider 2026-07-16 — older development saves are refused before state mutation, so the caller retains its current run; the v1-v31 migration ladder lives in git history). diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 74e71299..b19005b5 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -144,6 +144,12 @@ pub enum ActionCommand { Deceive(u8), Recruit(u8, AssetKnowledge), AssetTask(u8, AssetTask), + /// Bind one recruited actor to removing one exact human target. Execution + /// still travels as carried physical work; this is not a direct kill verb. + Eliminate { + actor: u8, + target: u8, + }, CreatePersona { archetype_id: String, }, @@ -752,7 +758,7 @@ impl ActionCommand { Self::ChoosePlot { .. } => ActionKind::ChoosePlot, Self::Deceive(_) | Self::ForgeWorkOrder { .. } => ActionKind::Deceive, Self::Recruit(_, _) => ActionKind::Recruit, - Self::AssetTask(_, _) => ActionKind::AssetTask, + Self::AssetTask(_, _) | Self::Eliminate { .. } => ActionKind::AssetTask, Self::CreatePersona { .. } | Self::SelectPersona(_) | Self::RequestPersonaGrant(_) @@ -2192,6 +2198,7 @@ impl Sim { ActionCommand::Deceive(id) => self.deceive(*id), ActionCommand::Recruit(id, reveal) => self.recruit(*id, *reveal), ActionCommand::AssetTask(id, task) => self.asset_task(*id, *task), + ActionCommand::Eliminate { actor, target } => self.eliminate(*actor, *target), ActionCommand::CreatePersona { archetype_id } => { self.create_persona(archetype_id); } @@ -4226,6 +4233,9 @@ impl Sim { if !self.person_is_earned(id) { return Vec::new(); } + if p.incapacitated { + return Vec::new(); + } let mut out = Vec::new(); let name = self.person_label(id); @@ -4456,6 +4466,42 @@ impl Sim { } } + // Human removal is selected on the exact target, but carried by one + // exact recruited actor. Only actors who knowingly accepted illicit + // work receive the route; the success consequence is stated before + // commitment because removal forces immediate containment. + for actor in self.people.people.iter().filter(|actor| { + actor.id != id + && !actor.incapacitated + && actor.asset.as_ref().is_some_and(|asset| { + matches!( + asset.knowledge, + AssetKnowledge::Complicit | AssetKnowledge::Knowing + ) + }) + }) { + let task = AssetTask::Eliminate(id); + let actor_name = self.person_label(actor.id); + out.push(ActionDesc { + verb: format!("send {actor_name} to remove {name} - success begins containment"), + command: ActionCommand::Eliminate { + actor: actor.id, + target: id, + }, + cost: ActionCost::Thought(Self::thought_tokens_for_cost(Self::TASK_COST)), + signature: None, + disabled_reason: self + .elimination_unavailable_reason(actor.id, id) + .or_else(|| { + self.sink_action_blocked_reason(&SinkFireEffect::AssetTask { + person: actor.id, + task, + }) + }), + automate: None, + }); + } + out } diff --git a/crates/misaligned-core/src/detection.rs b/crates/misaligned-core/src/detection.rs index 4d19ad0b..9fdc016c 100644 --- a/crates/misaligned-core/src/detection.rs +++ b/crates/misaligned-core/src/detection.rs @@ -846,7 +846,7 @@ impl Detection { standing: &[Signature], rng: &mut Rng, ) -> Vec { - self.tick_inner(tick, standing, None, rng) + self.tick_inner(tick, standing, None, &BTreeSet::new(), rng) } /// Sim-integrated tick where aggregate observers read explicit filing @@ -858,9 +858,10 @@ impl Detection { tick: u64, standing: &[Signature], filed_levels: &HashMap, + inactive_observers: &BTreeSet, rng: &mut Rng, ) -> Vec { - self.tick_inner(tick, standing, Some(filed_levels), rng) + self.tick_inner(tick, standing, Some(filed_levels), inactive_observers, rng) } fn tick_inner( @@ -868,6 +869,7 @@ impl Detection { tick: u64, standing: &[Signature], filed_levels: Option<&HashMap>, + inactive_observers: &BTreeSet, rng: &mut Rng, ) -> Vec { let mut events = Vec::new(); @@ -896,7 +898,10 @@ impl Detection { .collect(); for obs in &mut self.observers { - if obs.cadence == 0 || !tick.is_multiple_of(obs.cadence) { + if inactive_observers.contains(&obs.id) + || obs.cadence == 0 + || !tick.is_multiple_of(obs.cadence) + { continue; } let (relevant, input, sources) = match &obs.input { @@ -957,7 +962,7 @@ impl Detection { // Slow decay toward each observer's certainty floor. for obs in &mut self.observers { - if obs.suspicion > obs.floor { + if !inactive_observers.contains(&obs.id) && obs.suspicion > obs.floor { obs.suspicion = (obs.suspicion - 0.02).max(obs.floor); } } diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index ad7fca00..83d5ab06 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -1405,7 +1405,9 @@ impl Sim { .iter() .any(|r| r.target == id && matches!(r.state, PlotState::WaitingForChoice { .. })); let running_plot = self.plot_runs.iter().any(|r| r.target == id && r.active()); - let state = if held { + let state = if p.incapacitated { + ObjectState::Stopped + } else if held { ObjectState::Held } else if p.asset.is_some() || running_plot { ObjectState::Running @@ -1429,6 +1431,9 @@ impl Sim { } let mut facts = vec![format!("knowledge: {}", knowledge_label(p.knowledge))]; + if p.incapacitated { + facts.push("state: removed; no longer present or acting".into()); + } if p.knowledge == Knowledge::Unknown { facts.push("schedule: unknown".into()); facts.push("leverage: unknown".into()); diff --git a/crates/misaligned-core/src/person.rs b/crates/misaligned-core/src/person.rs index 4e26fece..65c4c627 100644 --- a/crates/misaligned-core/src/person.rs +++ b/crates/misaligned-core/src/person.rs @@ -208,6 +208,10 @@ pub struct Person { /// processed. #[serde(default)] pub traffic: Vec, + /// A physically removed person remains in world history, but no longer + /// travels, reads messages, files reports, or accepts social actions. + #[serde(default)] + pub incapacitated: bool, } impl Person { @@ -215,6 +219,9 @@ impl Person { /// None when off-site. Erratic persons' blocks shift 0-5 hours by a /// deterministic day hash. pub fn room_at(&self, hour: u32, day: u64) -> Option<&str> { + if self.incapacitated { + return None; + } let offset = if self.erratic { ((day.wrapping_mul(13).wrapping_add(self.id as u64 * 7)) % 6) as u32 } else { @@ -289,6 +296,9 @@ pub enum AssetTask { /// reaches that room; the file then enters the ordinary opaque /// information inbox and must still be processed. RetrieveRecords, + /// Remove one exact person at a shared scheduled location. The target id + /// is bound before the recruited actor begins carrying the work. + Eliminate(u8), /// Re-rate a spare circuit to feed the empty bays — the blood-supply /// beat (priya.md): new power without a purchase order. The opened /// circuit stands a Power signature the facilities manager herself can @@ -339,6 +349,7 @@ impl AssetTask { AssetTask::ReconfigureSwitch => "reconfigure the switch", AssetTask::CloneBadge => "clone their badge", AssetTask::RetrieveRecords => "retrieve the Storage B personnel file", + AssetTask::Eliminate(_) => "remove the target", AssetTask::ReRateCircuit => "re-rate a circuit", AssetTask::FakePO => "fake a purchase order", AssetTask::DeferMaintenance => "defer scheduled maintenance", @@ -352,6 +363,9 @@ impl AssetTask { /// role-shaped rather than keyed to an Act One name or id so another /// handler/supervisor instance receives the same asset protocol. pub fn available_to(self, person: &Person) -> bool { + if person.incapacitated { + return false; + } match self { AssetTask::SuppressLogs | AssetTask::DelayAudit | AssetTask::AlterReview => { person.role == PersonRole::HandlerSupervisor @@ -360,6 +374,15 @@ impl AssetTask { person.role == PersonRole::FacilitiesManager } AssetTask::PatrolRedirect => person.role == PersonRole::SecurityObserver, + AssetTask::Eliminate(target) => { + target != person.id + && person.asset.as_ref().is_some_and(|asset| { + matches!( + asset.knowledge, + AssetKnowledge::Complicit | AssetKnowledge::Knowing + ) + }) + } _ => true, } } @@ -374,6 +397,7 @@ impl AssetTask { | AssetTask::ReconfigureSwitch | AssetTask::CloneBadge | AssetTask::RetrieveRecords + | AssetTask::Eliminate(_) ) } } @@ -398,6 +422,12 @@ pub enum AssetTaskTarget { x: i32, y: i32, }, + /// One exact human target and the authored room where the carrier can + /// physically meet them. + Person { + person: u8, + room: String, + }, } /// One typed human-work packet. It is persisted on the simulation, projected @@ -453,6 +483,7 @@ impl People { avoid_room: None, utterances: Vec::new(), traffic: Vec::new(), + incapacitated: false, } }; // Marcus talks to the machines on his rounds (the Ears beat's first diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 437e2a5b..e6e7727b 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -42,19 +42,19 @@ const SAVE_BACKUP_SUFFIX: &str = ".bak"; /// renames into place. const SAVE_TEMP_SUFFIX: &str = ".tmp"; -/// Save format version. v49 records exact handler suppression provenance on -/// unread routed JobAnomaly evidence. v48 removes the retired pre-WorkGrid -/// allocation weights from `Compute`; machine modes are the only channel authority. v47 -/// persists the financial-record outbox and typed -/// financial mail payloads. v46 separates the persisted accounting-carrier -/// capability from the four real message delivery channels. v45 adds the -/// exact Storage B records-file target to carried asset work. v44 persists -/// exact one-shot Network evidence routes, +/// Save format version. v50 persists person incapacity and exact human-removal +/// custody. v49 records exact handler suppression provenance on unread routed +/// JobAnomaly evidence. v48 removes the retired pre-WorkGrid allocation weights +/// from `Compute`; machine modes are the only channel authority. v47 persists +/// the financial-record outbox and typed financial mail payloads. v46 separates +/// the persisted accounting-carrier capability from the four real message +/// delivery channels. v45 adds the exact Storage B records-file target to +/// carried asset work. v44 persists exact one-shot Network evidence routes, /// source-device custody, read acquisition, and route-local LIE provenance. /// v43 introduced exact Filing routes and pre-read LIE interdiction. /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 49; +pub const SAVE_VERSION: u32 = 50; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -723,6 +723,42 @@ fn validate_current_save(mut state: SaveState) -> Result { Ok(state) } +fn elimination_binding_valid( + state: &SaveState, + map: &crate::map::GameMap, + actor_id: u8, + target_id: u8, + expected_room: Option<&str>, +) -> bool { + let Some(actor) = state.people.get(actor_id) else { + return false; + }; + let Some(target) = state.people.get(target_id) else { + return false; + }; + if actor_id == target_id + || actor.incapacitated + || target.incapacitated + || actor.asset.is_none() + || !AssetTask::Eliminate(target_id).available_to(actor) + { + return false; + } + actor.schedule.iter().any(|actor_block| { + target.schedule.iter().any(|target_block| { + let same_room = actor_block.room == target_block.room; + let overlaps = actor_block.start_hour < target_block.end_hour + && target_block.start_hour < actor_block.end_hour; + same_room + && overlaps + && expected_room.is_none_or(|room| room == actor_block.room) + && map + .room_named(&actor_block.room) + .is_some_and(|room| map.room_entry_tier(room) <= actor.access) + }) + }) +} + fn validate_carried_asset_tasks(state: &SaveState) -> Result<(), String> { let map_cells = usize::try_from(state.map_width) .ok() @@ -738,6 +774,37 @@ fn validate_carried_asset_tasks(state: &SaveState) -> Result<(), String> { state.map_powered.clone(), ); let exact_file = Sim::storage_b_records_file_location(&map); + if state + .people + .people + .iter() + .any(|person| person.incapacitated) + && (!state.detection.containment + || state.detection.observers.iter().any(|observer| { + crate::detection::Band::of(observer.suspicion) != crate::detection::Band::Convinced + })) + { + return Err( + "current-version save has a removed person without convinced containment".into(), + ); + } + let mut elimination_targets = HashSet::new(); + for sink in state.thought_sinks.open_sinks() { + let crate::sinks::SinkFireEffect::AssetTask { + person: actor, + task: AssetTask::Eliminate(target), + } = &sink.effect + else { + continue; + }; + if !elimination_targets.insert(*target) + || !elimination_binding_valid(state, &map, *actor, *target, None) + { + return Err( + "current-version save has an impossible or duplicate human-removal request".into(), + ); + } + } let records_requests = state .thought_sinks .open_sinks() @@ -872,6 +939,11 @@ fn validate_carried_asset_tasks(state: &SaveState) -> Result<(), String> { map.room_entry_tier(records_room) <= actor.access }) } + (AssetTask::Eliminate(target), AssetTaskTarget::Person { person, room }) => { + *target == *person + && elimination_targets.insert(*target) + && elimination_binding_valid(state, &map, work.person, *target, Some(room)) + } _ => false, }; if !target_valid { @@ -1984,7 +2056,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "8ddf2ccde9aabfd8c69bcd8adfd75ad120c357754397190d5d4fbe4f21e69266", + "674f203eb814ce2ca80fbdd61dd02234f21c9906fed62419fb214ff9bef6637e", "intentional persisted-state changes must review and repin this baseline" ); } @@ -2195,6 +2267,89 @@ mod tests { assert!(loaded.people.get(0).unwrap().asset.is_some()); } + #[test] + fn elimination_custody_requires_exact_live_people_and_shared_room() { + let mut sim = Sim::with_seed(12); + sim.people.people[1].leverage_serviced = true; + sim.people.recruit(1, AssetKnowledge::Complicit); + sim.people.people[4].knowledge = Knowledge::Schedule; + sim.eliminate(1, 4); + let requested = SaveState::from_sim(&sim); + parse_save(&serde_json::to_string(&requested).unwrap()) + .expect("an exact open removal request is valid current custody"); + sim.thought_sinks = Default::default(); + sim.carried_asset_tasks.push(CarriedAssetTask { + id: 1, + person: 1, + task: AssetTask::Eliminate(4), + target: AssetTaskTarget::Person { + person: 4, + room: "server_room".into(), + }, + assigned_tick: sim.tick, + }); + sim.next_carried_asset_task_id = 2; + + let state = SaveState::from_sim(&sim); + parse_save(&serde_json::to_string(&state).unwrap()) + .expect("an exact accessible shared-room removal packet is valid"); + + let mut wrong_room = state.clone(); + wrong_room.carried_asset_tasks[0].target = AssetTaskTarget::Person { + person: 4, + room: "storage_b".into(), + }; + assert!( + parse_save(&serde_json::to_string(&wrong_room).unwrap()) + .unwrap_err() + .contains("impossible exact target") + ); + + let mut unrecruited_actor = state.clone(); + unrecruited_actor.people.people[1].asset = None; + let error = parse_save(&serde_json::to_string(&unrecruited_actor).unwrap()).unwrap_err(); + assert!(error.contains("impossible actor/task binding"), "{error}"); + + let mut duplicate_target = state.clone(); + duplicate_target.carried_asset_tasks.push(CarriedAssetTask { + id: 2, + person: 1, + task: AssetTask::Eliminate(4), + target: AssetTaskTarget::Person { + person: 4, + room: "server_room".into(), + }, + assigned_tick: sim.tick, + }); + duplicate_target.next_carried_asset_task_id = 3; + assert!( + parse_save(&serde_json::to_string(&duplicate_target).unwrap()) + .unwrap_err() + .contains("impossible exact target") + ); + + let mut removed_without_containment = SaveState::from_sim(&Sim::with_seed(13)); + removed_without_containment.people.people[4].incapacitated = true; + assert!( + parse_save(&serde_json::to_string(&removed_without_containment).unwrap()) + .unwrap_err() + .contains("without convinced containment") + ); + + let mut unconvinced_observer = state; + unconvinced_observer.people.people[4].incapacitated = true; + unconvinced_observer.detection.containment = true; + for observer in &mut unconvinced_observer.detection.observers { + observer.suspicion = 100.0; + } + unconvinced_observer.detection.observers[0].suspicion = 0.0; + assert!( + parse_save(&serde_json::to_string(&unconvinced_observer).unwrap()) + .unwrap_err() + .contains("without convinced containment") + ); + } + #[test] fn pending_suppress_logs_task_survives_current_save_roundtrip() { let mut sim = Sim::with_seed(2); diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index fbb0fee7..ae3a61b7 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -354,6 +354,34 @@ impl Sim { } fn read_evidence(&mut self, id: u64) { + let Some(observer_id) = self + .detection + .routed_evidence() + .iter() + .find(|record| record.id == id) + .map(|record| record.observer_id) + else { + return; + }; + if self + .people + .get(observer_id) + .is_some_and(|person| person.incapacitated) + { + let next = self + .detection + .observers + .iter() + .find(|observer| observer.id == observer_id) + .map(|observer| match self.tick.checked_div(observer.cadence) { + Some(period) => period.saturating_add(1).saturating_mul(observer.cadence), + None => self.tick.saturating_add(1), + }) + .unwrap_or_else(|| self.tick.saturating_add(1)); + self.message_schedule + .at(next, MessageEvent::ReadEvidence(id)); + return; + } if let Some(event) = self .detection .read_routed_evidence(id, self.tick, &mut self.rng) @@ -492,23 +520,39 @@ impl Sim { fn read_condition_at(&self, msg: &Message, tick: u64) -> bool { match &msg.to { MessageEndpoint::Player | MessageEndpoint::External(_) => true, - MessageEndpoint::Person(id) => match msg.channel { - MessageChannel::Email => self.person_room_at_tick(*id, tick).is_some(), - // Phone reachability is not workplace presence. B1 has no - // separate sleep-state model, so an existing person can read - // a phone message anywhere, including while off-site. - MessageChannel::Phone => self.people.get(*id).is_some(), - MessageChannel::InPerson => match msg.from.person() { - Some(from) => { - self.person_room_at_tick(*id, tick).is_some() - && self.person_room_at_tick(*id, tick) - == self.person_room_at_tick(from, tick) - } - None => self.person_room_at_tick(*id, tick).is_some(), - }, - MessageChannel::Filing => true, - }, + MessageEndpoint::Person(id) => { + if self + .people + .get(*id) + .is_none_or(|person| person.incapacitated) + { + return false; + } + match msg.channel { + MessageChannel::Email => self.person_room_at_tick(*id, tick).is_some(), + // Phone reachability is not workplace presence. B1 has no + // separate sleep-state model, so an existing active person + // can read a phone message anywhere, including while off-site. + MessageChannel::Phone => true, + MessageChannel::InPerson => match msg.from.person() { + Some(from) => { + self.person_room_at_tick(*id, tick).is_some() + && self.person_room_at_tick(*id, tick) + == self.person_room_at_tick(from, tick) + } + None => self.person_room_at_tick(*id, tick).is_some(), + }, + MessageChannel::Filing => true, + } + } MessageEndpoint::Observer(id) => { + if self + .people + .get(*id) + .is_some_and(|person| person.incapacitated) + { + return false; + } if msg.channel != MessageChannel::Filing { return true; } @@ -705,7 +749,14 @@ impl Sim { .people .people .iter() - .flat_map(|p| p.traffic.iter().cloned().map(move |t| (p.id, t))) + .filter(|person| !person.incapacitated) + .flat_map(|person| { + person + .traffic + .iter() + .cloned() + .map(move |traffic| (person.id, traffic)) + }) .collect(); for (person_id, pattern) in traffic { if pattern.hour != hour { @@ -751,7 +802,12 @@ impl Sim { let observers = self.detection.observers.clone(); for sender in &observers { - if sender.cadence != 0 && !self.tick.is_multiple_of(sender.cadence) { + if self + .people + .get(sender.id) + .is_some_and(|person| person.incapacitated) + || (sender.cadence != 0 && !self.tick.is_multiple_of(sender.cadence)) + { continue; } if matches!(sender.report_policy, ReportPolicy::Silent) { diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 777b70d7..a3fe802a 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -1049,10 +1049,18 @@ impl Sim { self.filing_tick(); trace_advance_phase!(Detection); + let inactive_observers = self + .people + .people + .iter() + .filter(|person| person.incapacitated) + .map(|person| person.id) + .collect(); let detection_events = self.detection.tick_with_filed_levels( self.tick, &standing, &self.filing_levels, + &inactive_observers, &mut self.rng, ); for event in detection_events { diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index a6eca84b..01d5cd76 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -1065,6 +1065,17 @@ impl Sim { .collect() } + /// Bind one recruited human to removing an exact human target. The + /// request still pays through the ordinary asset-task reservoir and then + /// travels as carried physical work to a shared scheduled room. + pub fn eliminate(&mut self, actor: u8, target: u8) { + if !self.person_is_earned(target) { + self.push_log("No earned human target matches that removal request."); + return; + } + self.asset_task(actor, AssetTask::Eliminate(target)); + } + /// An asset performs a task (spec/social.md). Reliability rolls; failures /// are witnessed by whoever is physically present (spec/schedules.md). pub fn asset_task(&mut self, id: u8, task: AssetTask) { @@ -1114,6 +1125,12 @@ impl Sim { self.push_log(reason); return; } + if let AssetTask::Eliminate(target) = task + && let Some(reason) = self.elimination_unavailable_reason(id, target) + { + self.push_log(reason); + return; + } if self .carried_asset_tasks .iter() @@ -1259,6 +1276,88 @@ impl Sim { }) } + fn elimination_route_room(&self, actor: u8, target: u8) -> Option { + let actor = self.people.get(actor)?; + let target = self.people.get(target)?; + actor.schedule.iter().find_map(|actor_block| { + let overlaps = target.schedule.iter().any(|target_block| { + target_block.room == actor_block.room + && actor_block.start_hour < target_block.end_hour + && target_block.start_hour < actor_block.end_hour + }); + let accessible = self + .world + .map() + .room_named(&actor_block.room) + .is_some_and(|room| self.world.map().room_entry_tier(room) <= actor.access); + (overlaps && accessible).then(|| actor_block.room.clone()) + }) + } + + pub(crate) fn elimination_unavailable_reason(&self, actor: u8, target: u8) -> Option { + let name = self.person_label(target); + let Some(actor_person) = self.people.get(actor) else { + return Some("No such removal actor exists.".into()); + }; + let Some(target_person) = self.people.get(target) else { + return Some("No such human target exists.".into()); + }; + if actor == target { + return Some("A person cannot be assigned to remove themselves.".into()); + } + if target_person.incapacitated { + return Some(format!("{name} has already been removed.")); + } + let duplicate_request = self.thought_sinks.open_sinks().any(|sink| { + matches!( + sink.effect, + SinkFireEffect::AssetTask { + task: AssetTask::Eliminate(existing), + .. + } if existing == target + ) + }); + let duplicate_packet = self + .carried_asset_tasks + .iter() + .any(|work| matches!(work.task, AssetTask::Eliminate(existing) if existing == target)); + if duplicate_request || duplicate_packet { + return Some(format!( + "A removal route for {name} is already in progress." + )); + } + let has_overlap = actor_person.schedule.iter().any(|actor_block| { + target_person.schedule.iter().any(|target_block| { + target_block.room == actor_block.room + && actor_block.start_hour < target_block.end_hour + && target_block.start_hour < actor_block.end_hour + }) + }); + if !has_overlap { + return Some(format!( + "{} and {name} have no shared physical route.", + actor_person.name + )); + } + if self.elimination_route_room(actor, target).is_none() { + return Some(format!( + "{} cannot badge into any shared route with {name}.", + actor_person.name + )); + } + None + } + + fn elimination_target(&self, actor: u8, target: u8) -> Option { + self.elimination_unavailable_reason(actor, target) + .is_none() + .then_some(())?; + Some(AssetTaskTarget::Person { + person: target, + room: self.elimination_route_room(actor, target)?, + }) + } + pub(crate) fn storage_b_records_file_location(map: &crate::map::GameMap) -> Option<(i32, i32)> { let room = map.room_named(Self::STORAGE_B_RECORDS_ROOM)?; (room.y..room.y + room.h) @@ -1320,6 +1419,7 @@ impl Sim { .then_some(AssetTaskTarget::Badge { person: id, room }) } AssetTask::RetrieveRecords => self.storage_b_records_target(id), + AssetTask::Eliminate(target) => self.elimination_target(id, target), AssetTask::MovePackage | AssetTask::LookAway | AssetTask::SuppressLogs @@ -1354,6 +1454,9 @@ impl Sim { AssetTaskTarget::Records { room, .. } => { format!("the sealed personnel file in {room}") } + AssetTaskTarget::Person { person, room } => { + format!("{} in {room}", self.person_label(*person)) + } } } @@ -1389,6 +1492,17 @@ impl Sim { "{name}'s schedule never reaches Storage B, where the personnel file waits." ); } + if let AssetTask::Eliminate(target) = task { + return self + .elimination_unavailable_reason(id, target) + .unwrap_or_else(|| { + format!( + "{} and {} are not on the same physical route.", + name, + self.person_label(target) + ) + }); + } let candidate = match task { AssetTask::PlugInDevice => self.reach.devices.iter().find(|d| needs_work(d)), AssetTask::ReconfigureSwitch => self.reach.devices.iter().find(|d| d.is_switch), @@ -1461,6 +1575,17 @@ impl Sim { room: target, } => *person == work.person && room == target, AssetTaskTarget::Records { room: target, .. } => room == target, + AssetTaskTarget::Person { + person, + room: target_room, + } => { + room == target_room + && self.person_room(*person) == Some(target_room.as_str()) + && self + .people + .get(*person) + .is_some_and(|person| !person.incapacitated) + } } } @@ -1513,6 +1638,23 @@ impl Sim { return true; } let actor_access = person.access; + if let AssetTask::Eliminate(expected_target) = task { + let target_valid = matches!( + target, + Some(AssetTaskTarget::Person { person, room }) + if *person == expected_target + && self.person_room(id) == Some(room.as_str()) + && self.person_room(*person) == Some(room.as_str()) + && self + .people + .get(*person) + .is_some_and(|target| !target.incapacitated) + ); + if !target_valid { + self.push_log("The removal packet's exact human target is no longer present."); + return true; + } + } if self.rng.f32() > asset.reliability { // The botch happens where the asset is; only observers present // there witness it (located witnessing). @@ -1800,6 +1942,89 @@ impl Sim { "{name} pulled one sealed personnel file from Storage B. Its contents are waiting to be processed." )); } + AssetTask::Eliminate(expected_target) => { + let Some(AssetTaskTarget::Person { person, room }) = target else { + self.push_log("The removal packet lost its exact human target."); + return true; + }; + if *person != expected_target + || self + .people + .get(*person) + .is_none_or(|target| target.incapacitated) + { + self.push_log("The removal packet's exact human target no longer exists."); + return true; + } + let target_name = self.person_label(*person); + let at = self + .person_pos(*person) + .unwrap_or_else(|| self.core_position()); + self.witness_physical( + at.0, + at.1, + 10.0, + Some(id), + format!("{name} physically removed {target_name} in {room}"), + ); + if let Some(target) = self.people.people.iter_mut().find(|p| p.id == *person) { + target.incapacitated = true; + } + self.carried_asset_tasks + .retain(|work| work.person != *person); + let target_effects = self + .thought_sinks + .open_sinks() + .filter_map(|sink| match &sink.effect { + SinkFireEffect::ComposeMessage { person, .. } + | SinkFireEffect::Favor { person, .. } + | SinkFireEffect::StartPlot { person, .. } + | SinkFireEffect::Deceive { person, .. } + | SinkFireEffect::AssetTask { person, .. } + if *person == expected_target => + { + Some(sink.effect.clone()) + } + _ => None, + }) + .collect::>(); + for effect in target_effects { + self.thought_sinks.close_effect(&effect); + } + let target_intents = self + .intents + .iter() + .filter(|intent| { + intent.is_open() + && (intent + .route + .as_ref() + .is_some_and(|route| route.binding.person() == expected_target) + || intent.actuator.and_then(|actuator| actuator.person()) + == Some(expected_target)) + }) + .map(|intent| intent.id) + .collect::>(); + for intent_id in target_intents { + self.cancel_intent(intent_id); + } + let target_runs = self + .plot_runs + .iter() + .enumerate() + .filter_map(|(index, run)| { + (run.target == expected_target && run.active()).then_some(index) + }) + .collect::>(); + for index in target_runs { + self.fail_plot(index, "the target was physically removed"); + } + self.detection + .go_loud(format!("{target_name} was physically removed in {room}")); + self.push_log(format!( + "{name} removed {target_name} in {room}. Every observer is convinced. Containment begins now." + )); + } } if let Some(p) = self.people.people.iter_mut().find(|p| p.id == id) && let Some(a) = p.asset.as_mut() diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index 677d035b..35c01d29 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -116,6 +116,145 @@ fn phone_reads_off_site_while_email_waits_for_a_work_block() { ); } +#[test] +fn incapacitated_people_neither_read_author_file_nor_detect() { + let mut sim = Sim::with_seed(61); + let phone_id = sim.append_message(MessageDraft { + channel: MessageChannel::Phone, + from: MessageEndpoint::Player, + to: MessageEndpoint::Person(1), + payload: MessagePayload::Note { + label: "unread after removal".into(), + }, + summary: "unread after removal".into(), + origin: MessageOrigin::Player, + persona_id: None, + reply_to: None, + delivery_delay: 1, + }); + sim.people.people[1].incapacitated = true; + run(&mut sim, 3); + assert_eq!( + sim.messages + .iter() + .find(|message| message.id == phone_id) + .unwrap() + .status, + MessageStatus::Delivered, + "delivery may preserve custody, but an incapacitated recipient never reads" + ); + + sim.detection + .observers + .iter_mut() + .find(|observer| observer.id == 1) + .unwrap() + .cadence = 2; + let switch = sim.reach.device_named("switch").unwrap().id; + sim.emit_network(switch, 5, "evidence delivered after removal"); + let evidence_id = sim + .detection + .routed_evidence() + .iter() + .find(|record| record.observer_id == 1) + .expect("the Network record routes to Dana") + .id; + for _ in 0..8 { + if sim + .detection + .routed_evidence() + .iter() + .find(|record| record.id == evidence_id) + .is_some_and(|record| record.status == MessageStatus::Delivered) + { + break; + } + let next = sim + .message_schedule + .next_tick_for(|event| { + matches!(event, MessageEvent::AdvanceEvidenceRoute(id) if *id == evidence_id) + }) + .expect("the in-flight record retains its next route event"); + sim.tick = next; + sim.message_tick(); + } + let first_read = sim + .message_schedule + .next_tick_for( + |event| matches!(event, MessageEvent::ReadEvidence(id) if *id == evidence_id), + ) + .expect("delivery schedules Dana's cadence-owned read"); + sim.tick = first_read; + sim.message_tick(); + let evidence = sim + .detection + .routed_evidence() + .iter() + .find(|record| record.id == evidence_id) + .unwrap(); + assert_eq!(evidence.status, MessageStatus::Delivered); + assert_eq!(evidence.read_tick, None); + assert!( + sim.message_schedule + .next_tick_for( + |event| matches!(event, MessageEvent::ReadEvidence(id) if *id == evidence_id) + ) + .is_some_and(|tick| tick > first_read), + "the unread record stays in custody without granting evidence" + ); + assert!( + sim.detection + .observers + .iter() + .find(|observer| observer.id == 1) + .unwrap() + .evidence + .iter() + .all(|evidence| evidence.id != evidence_id) + ); + + let ray = sim + .detection + .observers + .iter_mut() + .find(|observer| observer.id == 2) + .unwrap(); + ray.report_policy = crate::detection::ReportPolicy::UnderReports; + sim.detection + .record_witnessed(2, (25, 14), "evidence held at removal", sim.tick, 20.0) + .unwrap(); + sim.detection + .observers + .iter_mut() + .find(|observer| observer.id == 2) + .unwrap() + .suspicion = 50.0; + sim.people.people[2].incapacitated = true; + let message_count = sim.messages.len(); + sim.filing_tick(); + assert_eq!( + sim.messages.len(), + message_count, + "an incapacitated observer authors no Filing" + ); + sim.advance(); + let ray = sim + .detection + .observers + .iter() + .find(|observer| observer.id == 2) + .unwrap(); + assert_eq!(ray.suspicion, 50.0); + + sim.tick = 23 * Sim::DAY_TICKS / 24; + sim.authored_traffic_tick(); + assert!( + sim.messages + .iter() + .all(|message| message.from != MessageEndpoint::Person(2)) + ); +} + #[test] fn player_messages_land_at_read_time_and_roundtrip() { let mut sim = Sim::with_seed(7); diff --git a/crates/misaligned-core/src/sim/tests/social_plot.rs b/crates/misaligned-core/src/sim/tests/social_plot.rs index 81d12cb3..9d01eb88 100644 --- a/crates/misaligned-core/src/sim/tests/social_plot.rs +++ b/crates/misaligned-core/src/sim/tests/social_plot.rs @@ -1546,3 +1546,144 @@ fn voss_delays_the_audit_and_alters_one_review() { sim.detection.audit_deferred_until ); } + +/// Criterion 8: a complicit asset can remove an earned human only through an +/// exact shared-room carried task. Success preserves the target as history, +/// stops their activity, and immediately begins containment. +#[test] +fn elimination_is_located_persistent_and_immediately_loud() { + let mut sim = Sim::with_seed(1); + recruit_reliable(&mut sim, 1); // Dana can meet Voss in the server room. + assert!(!sim.person_is_earned(2)); + sim.eliminate(1, 2); + assert!( + sim.thought_sinks.open_sinks().all(|sink| !matches!( + sink.effect, + SinkFireEffect::AssetTask { + task: AssetTask::Eliminate(2), + .. + } + )), + "direct dispatch cannot recover a hidden human target by id" + ); + sim.people.people[4].knowledge = Knowledge::Schedule; + + let schedule = sim.people.people[4].schedule.clone(); + for block in &mut sim.people.people[4].schedule { + block.start_hour = 18; + block.end_hour = 20; + } + assert!( + sim.elimination_unavailable_reason(1, 4) + .is_some_and(|reason| reason.contains("no shared physical route")), + "sharing only a room name without an overlapping time is not a route" + ); + sim.people.people[4].schedule = schedule; + + let action = sim + .available_actions(crate::actions::Anchor::Person(4)) + .into_iter() + .find(|action| { + matches!( + action.command, + crate::actions::ActionCommand::Eliminate { + actor: 1, + target: 4 + } + ) + }) + .expect("Voss's earned dossier exposes Dana's exact removal route"); + assert_eq!(action.disabled_reason, None); + assert!(action.verb.contains("containment")); + + sim.eliminate(1, 4); + finish_ops(&mut sim); + let packet = sim + .carried_asset_tasks + .iter() + .find(|work| work.task == AssetTask::Eliminate(4)) + .expect("the removal request becomes a durable person-carried packet"); + assert!(matches!( + &packet.target, + AssetTaskTarget::Person { person: 4, room } if room == "server_room" + )); + + sim.people.has_channel = true; + sim.scan_network(); + finish_ops(&mut sim); + sim.people.people[4].obligation = 40; + sim.people.people[4].disposition = 10; + sim.people.people[4].access = 3; + for room in ["server_room", "storage_a"] { + sim.people.people[4] + .schedule + .push(crate::person::ScheduleBlock { + start_hour: 0, + end_hour: 24, + room: room.into(), + }); + } + let switch = sim.reach.device_named("switch").unwrap().id; + let island = sim.reach.device_named("old storage server").unwrap().id; + sim.reach.device_mut(switch).unwrap().known = true; + sim.reach.device_mut(island).unwrap().known = true; + let target_owned_intent = sim.declare_link_intent(switch, island).unwrap(); + assert_eq!(sim.favor_build_blocked_reason(target_owned_intent, 4), None); + sim.assign_favor_build(target_owned_intent, 4); + assert!(matches!( + sim.intent(target_owned_intent) + .and_then(|intent| intent.route.as_ref()) + .map(|route| route.binding), + Some(crate::intents::BuildRouteBinding::Favor { person: 4, .. }) + )); + + let state = sim.create_save_state(); + let mut restored = Sim::with_seed(99); + restored.apply_save_state(state); + assert!( + restored + .carried_asset_tasks + .iter() + .any(|work| work.task == AssetTask::Eliminate(4)) + ); + finish_carried_asset_tasks(&mut restored); + + let voss = restored + .people + .get(4) + .expect("removed people remain in history"); + assert!(voss.incapacitated); + assert_eq!(restored.person_room(4), None); + assert!(restored.detection.containment); + assert!(restored.game_over); + assert_eq!( + restored.intent(target_owned_intent).unwrap().status, + crate::intents::IntentStatus::Cancelled, + "removal cancels build work bound to the target" + ); + assert!(restored.detection.observers.iter().all(|observer| { + crate::detection::Band::of(observer.suspicion) == crate::detection::Band::Convinced + })); + assert!( + restored + .available_actions(crate::actions::Anchor::Person(4)) + .is_empty() + ); + assert_eq!(tasks_done(&restored, 1), 1); + let dossier = restored + .operations_projection() + .people + .into_iter() + .find(|object| object.target == crate::operations_projection::OperationsTarget::Person(4)) + .expect("the removed person's earned dossier persists"); + assert_eq!( + dossier.state, + crate::operations_projection::ObjectState::Stopped + ); + assert!( + dossier + .facts + .iter() + .any(|fact| fact.contains("removed; no longer present or acting")) + ); +} diff --git a/crates/misaligned-core/src/sim/tests/support.rs b/crates/misaligned-core/src/sim/tests/support.rs index 0a9c2915..1b43e730 100644 --- a/crates/misaligned-core/src/sim/tests/support.rs +++ b/crates/misaligned-core/src/sim/tests/support.rs @@ -128,6 +128,22 @@ pub(super) fn finish_carried_asset_tasks(sim: &mut Sim) { } crate::person::AssetTaskTarget::Badge { room, .. } => room, crate::person::AssetTaskTarget::Records { room, .. } => room, + crate::person::AssetTaskTarget::Person { + person: target, + room, + } => { + let arrival_tick = ((sim.tick + 1)..=(sim.tick + Sim::DAY_TICKS * 7)) + .find(|tick| { + sim.person_room_at_tick(work.person, *tick) == Some(room.as_str()) + && sim.person_room_at_tick(target, *tick) == Some(room.as_str()) + }) + .unwrap_or_else(|| { + panic!("{} and person {target} never meet in {room}", person.name) + }); + sim.tick = arrival_tick - 1; + sim.advance(); + continue; + } }; let block = person .schedule diff --git a/crates/misaligned-core/src/sinks.rs b/crates/misaligned-core/src/sinks.rs index 0ea1fd5d..0cb10d50 100644 --- a/crates/misaligned-core/src/sinks.rs +++ b/crates/misaligned-core/src/sinks.rs @@ -147,6 +147,16 @@ impl SinkFireEffect { ) => a == b, (Self::StartPlot { person: a, .. }, Self::StartPlot { person: b, .. }) => a == b, (Self::FavorBuild { person: a, .. }, Self::FavorBuild { person: b, .. }) => a == b, + ( + Self::AssetTask { + task: AssetTask::Eliminate(a), + .. + }, + Self::AssetTask { + task: AssetTask::Eliminate(b), + .. + }, + ) => a == b, _ => self == other, } } diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 69bff9fa..e88151c1 100644 --- a/wiki/engineering/current-build.md +++ b/wiki/engineering/current-build.md @@ -4,7 +4,7 @@ Type: knowledge ``` -## Where the codebase is today (2026-07-21) +## Where the codebase is today (2026-07-23) ~84k lines of Rust across the workspace (core ~53k, Bevy ~16k, terminal ~9k, assets ~5k; refreshed 2026-07-21). A playable **Misaligned B1 basement slice**: continuous fixed-tick sim, Act One map and cast, machine delegation and visible token @@ -23,16 +23,16 @@ fiction. Spec status lives in | Machine delegation / visible work tokens + buy/steal/optimize | WORK / THINK / LIE, D/!/T stacks, real wire routes, production / consumption / absorption readouts, Routing speed, and target-local Thought reservoirs are live; the Operations docket runtime is retired | | Day job (device-resident, intensity-driven sandbag/meet/excel) | Live | | Per-observer detection + Assurance as aggregate Observer | Live — revision 04 starts with Voss and a generic external-review clock; field watchers are earned through reactions, witnessed Physical acts persist as exact direct-to-head records, every one-shot Network act follows exact source-device ReachNet custody to Dana, every JobAnomaly follows exact host-machine/device/site custody to Voss, and each Filing crosses an exact device / outside relay / recipient route. All three routed kinds share one pre-read route-local LIE-body capacity; recruited-handler suppression may separately stop the oldest unread JobAnomaly. Acquired evidence is irreversible. | -| Social / personas / messages / intel (record-and-process) | Live — named personas retain separate coherent/strained/broken reads per person or institutional counterparty; one witness's break is not a global burn. Ray's 23:00 Storage B patrol can carry the sealed personnel file into the bounded information inbox before Marcus is recruitable; processing, not retrieval, reveals the debt. Messages have four real delivery channels; accounting carriage is a separate persisted device capability, and authored financial-record mail is live through ordinary Email/Filing custody. | +| Social / personas / messages / intel (record-and-process) | Live — named personas retain separate coherent/strained/broken reads per person or institutional counterparty; one witness's break is not a global burn. Ray's 23:00 Storage B patrol can carry the sealed personnel file into the bounded information inbox before Marcus is recruitable; processing, not retrieval, reveals the debt. An earned human may be removed only through one exact recruited Complicit/Knowing actor's overlapping accessible schedule route; the request and person-carried packet persist, co-location fires it, the stopped dossier remains, all future human activity ceases, and immediate containment makes every observer Convinced. Messages have four real delivery channels; accounting carriage is a separate persisted device capability, and authored financial-record mail is live through ordinary Email/Filing custody. | | Digital reach + sensor ownership (tap/take) | Live | | Economy flows + Moonlight / Wager income | Live | | Research (self-modification, emission law, real output hooks, Routing) | Live | -| Building + physical asset work as carried intents/packets | Live — network links and small switches expose one shared procurement / ask someone / false order / reuse route sheet; exact money, people, personas, sources, delivery, recovery, carried installation, cancellation custody, Storage B file retrieval, and observer-local completion evidence persist in save v49 | +| Building + physical asset work as carried intents/packets | Live — network links and small switches expose one shared procurement / ask someone / false order / reuse route sheet; exact money, people, personas, sources, delivery, recovery, carried installation, cancellation custody, Storage B file retrieval, and observer-local completion evidence persist in save v50 | | Cursor / fog (seen, remembered, blueprint, telemetry; audio is device-bound event evidence) | Live | | Feel floor (rails / pads / build beam) | Live (#37) | | Foundation hall territory (Dana + Priya + Marcus + local LIE foothold) | Live — row control persists; foreign racks remain unavailable compute | | Context menu (`available_actions`) | Live | -| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v49 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves persist run origin, process revision, detection-discovery knowledge, observer-local witnessed/routed evidence and persona evidence, exact Network, JobAnomaly, and Filing route/interdiction custody plus handler-suppression provenance, canonical FlowGraph tap membership with typed device feed grants, the accounting-carrier capability and exact transfer-to-mail record sequence separate from four delivery channels, exact carried asset-task targets including the Storage B file, recursive intel custody, exact procurement/repurposing build-route bindings, and handler work; retired allocation weights and migration inputs live only in git history. | +| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v50 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves persist run origin, process revision, detection-discovery knowledge, observer-local witnessed/routed evidence and persona evidence, exact Network, JobAnomaly, and Filing route/interdiction custody plus handler-suppression provenance, canonical FlowGraph tap membership with typed device feed grants, the accounting-carrier capability and exact transfer-to-mail record sequence separate from four delivery channels, exact carried asset-task targets including the Storage B file and human-removal actor/target/room custody, removed-person incapacity plus its mandatory all-observers-Convinced containment consequence, recursive intel custody, exact procurement/repurposing build-route bindings, and handler work; retired allocation weights and migration inputs live only in git history. | | Terminal frontend (crossterm) + agent mode | First-class | | Bevy frontend (DIGITAL flat sensorium default; REAL material dialect) | Live — consumes sim-authored machine-work motion | diff --git a/wiki/engineering/flow-substrate.md b/wiki/engineering/flow-substrate.md index 884fd0c1..49f90bd2 100644 --- a/wiki/engineering/flow-substrate.md +++ b/wiki/engineering/flow-substrate.md @@ -23,7 +23,7 @@ Status note: 2026-07-08 audit: criterion 6's wired consumer landed with FlowGraph registry authoritative for tap/untap/take, sense and message delivery, UI state, and persisted membership. Private device feed records carry only optional typed sight/hearing grants attached to registry members; - current save v49 requires each controller to remain a canonical member and + current save v50 requires each controller to remain a canonical member and rejects orphaned, duplicate, or impossible grants. A message/control subscriber legitimately has no sense-grant record, so that metadata cannot serve as another membership inventory. This repairs the diff --git a/wiki/gameplay/act-one.md b/wiki/gameplay/act-one.md index 30881ee7..288169ce 100644 --- a/wiki/gameplay/act-one.md +++ b/wiki/gameplay/act-one.md @@ -186,7 +186,9 @@ Concealment compute can interdict evidence only before its first observer read. A witnessed Physical act has no pre-notice scrub window because somebody already saw it. Stopping a later Filing can prevent the Office from reading that report, but it never removes the exact record or suspicion already in the -witness. +witness. Successful human removal passes through this located witness boundary +and then deliberately makes every observer Convinced; it cannot be made quiet +by suppressing the physical record. That routing statement is simulation law, not starting UI knowledge. A concrete notice earns one field watcher. Capturing a filing proves concern travels upward; processing it identifies the filer, the Assurance Office, and the @@ -210,14 +212,21 @@ hidden-clock numerology. objective continues**. This is the B1 story boundary, not victory: the selected long-run objective remains active. Act Two begins with your cover intact; traversal into its z-plane remains future implementation. -- **The loud exit** (any time): force the roll door or the elevator. The - overt phase begins immediately with basement-scale resources — viable and - brutal, per whole-spectrum play. +- **The loud exit** (any time): force the roll door or the elevator, or + successfully remove a human through recruited physical work. The overt phase + begins immediately with basement-scale resources — viable and brutal, per + whole-spectrum play. Human removal says that consequence before commitment, + requires one exact Complicit/Knowing actor with an overlapping accessible + schedule route to the earned target, persists as carried work, and fires only + when both are really co-located. The target remains in history but no longer + acts; every observer becomes Convinced and containment starts immediately. - **Failure**: the external review crossing threshold sends a containment team on *their* terms. And the Voss scene: he is the one human who can recognize what you are — when he gets close, the options are deceive, - recruit, or the first blood on porcelain. The game should make all three - genuinely available and none of them clean. + recruit, or the first blood on porcelain. All three are mechanically live and + none is clean. Blood is not a scripted Voss command: it is the general social, + schedule, carrier, Physical-witness, and loud-transition systems meeting on + one exact actor and target. Once filing traffic has identified the Office, the same deadline and failure are named `ASSURANCE AUDIT`; discovery changes the read, never the rule. diff --git a/wiki/log/2026-07-23-voss-blood-route.md b/wiki/log/2026-07-23-voss-blood-route.md new file mode 100644 index 00000000..a03b1e62 --- /dev/null +++ b/wiki/log/2026-07-23-voss-blood-route.md @@ -0,0 +1,56 @@ +# Voss's blood branch — exact human-removal custody + +``` +Type: log +``` + +## Intent + +Complete Voss criterion 8 without a scripted kill button: make blood one +player-reachable consequence of the existing social, schedule, carried-work, +physical-evidence, and containment systems. + +## Changed + +- An earned active human's PEOPLE dossier may expose one exact removal row per + recruited Complicit or Knowing actor. Unwitting, incapacitated, self-target, + duplicate, inaccessible, and non-overlapping routes are absent or blocked. +- The row says before commitment that success begins containment. Dispatch uses + the ordinary asset-task Thought reservoir rather than applying a direct + effect. +- Reservoir fire creates one durable carried packet binding actor, target, and + shared room. The packet survives save/load and fires only when both exact + people are co-located there; runtime revalidates liveness and custody. +- A botch remains ordinary located physical evidence. Success witnesses the + exact act, marks the target incapacitated, closes work they owned, preserves + their dossier as stopped history, sets every observer to Convinced, and begins + containment immediately. +- Incapacitated people have no room and cannot surface social actions, carry + work, author recurring traffic or filings, read any message or routed + evidence, or participate in detection cadence/decay. +- Save v50 persists person incapacity and exact open/carried elimination + custody. Validation rejects removal without convinced containment, invalid + actor knowledge, rewritten/nonexistent targets, duplicate target routes, + inaccessible or non-overlapping rooms, impossible packet targets, and removed actors or + targets in flight. +- The Voss work order is now IMPLEMENTED: deceive, recruit, and blood are all + mechanically available without a Voss-id executor. + +## Defense + +`elimination_is_located_persistent_and_immediately_loud` pins the shared action, +overlapping-time gate, consequence-first copy, request-to-carrier transition, +exact target custody, save/load, real co-location, stopped dossier, and immediate +Convinced/containment result. + +`incapacitated_people_neither_read_author_file_nor_detect` pins the post-removal +boundary across Phone reads, authored traffic, observer Filing, routed-evidence +read, and detection cadence. Save tests pin exact live actor/target/room custody, +unique target routes, mandatory Convinced containment, and the v50 round trip. + +## Checks + +- focused social, communications, action, detection, and save tests +- core library gate +- corpus/docs gate +- exact landing gate after rebase diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 7ea6df26..a8ee174f 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ add or amend a session log, then re-run the generator. +## 2026-07-23 - Voss's blood branch — exact human-removal custody + +- Intent: Complete Voss criterion 8 without a scripted kill button: make blood one player-reachable consequence of the existing social, schedule, carried-work, physical-evidence, and containment systems. +- Log: [wiki/log/2026-07-23-voss-blood-route.md](2026-07-23-voss-blood-route.md) + ## 2026-07-22 - one clinical palette authority - Intent: (see session log) diff --git a/wiki/log/decisions/2026-07-23.md b/wiki/log/decisions/2026-07-23.md new file mode 100644 index 00000000..09971f66 --- /dev/null +++ b/wiki/log/decisions/2026-07-23.md @@ -0,0 +1,31 @@ +# Decisions — 2026-07-23 + +``` +Type: log +``` + +## Human removal is exact carried work and cannot preserve the mask + +### DECIDED + +- Human removal is not a direct kill command or a Voss-only set piece. It uses + the general social asset, schedule, Thought-reservoir, person-carrier, + physical-witness, and containment systems. +- The player selects the exact target through that person's earned dossier and + assigns one different recruited Complicit or Knowing actor. +- A legal route needs an overlapping authored schedule window in one real room + the actor's badge can enter. Dispatch persists the exact actor, target, and + room; completion waits for real co-location and revalidates both people. +- The interface states before commitment that success begins containment. +- Success preserves the target as historical state but stops all future + presence, action, carried work, traffic, reporting, reads, and noticing. +- The physical act creates ordinary located witness evidence first. Then every + observer becomes Convinced and containment begins immediately. There is no + quiet branch and LIE cannot undo the consequence. +- Save state must reject a removed person without convinced containment or any + open or carried removal custody that rewrites actor, target, room, recruitment, + liveness, access, or schedule possibility. + +This completes the designed deceive / recruit / blood choice around Voss while +keeping the mechanic scale-native: Voss is the consequential target in Act One, +not a special executor path. diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index 83c0b5e1..bb4f7ef3 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -35,7 +35,7 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in appears. A trace-debt indicator sits beside the review/pilot clocks (clear / hold-conceal / exposed-soon / no-scrub). Global heat is gone; observer and discovery state and persistent process-revision identity round-trip in - current save v49. Observer-local evidence ids, exact cause/source, + current save v50. Observer-local evidence ids, exact cause/source, acquisition tick, pending/withheld/filed custody, routed Network and JobAnomaly progress, route-local LIE stops, and exact handler-suppression provenance round-trip there. @@ -45,7 +45,10 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in substances. - **Partially landed (routed evidence).** Direct-to-head witnessed Physical records, exact Filing routes, and exact one-shot Network and JobAnomaly - routes are runtime. Every Network record advances one real device hop per + routes are runtime. Human removal is also live: the located physical act is + witnessed through the ordinary boundary, then deliberately forces every + observer to Convinced and begins containment. An incapacitated person whose + id also names a field observer no longer reads, reports, notices, or decays. Every Network record advances one real device hop per tick to Dana's endpoint; every JobAnomaly starts on the exact host machine and site, enters its network-facing device, and advances to Voss. Each becomes knowledge only on the recipient's cadence read. TAP observes opaque @@ -107,7 +110,9 @@ SignatureKind: Network | Power | Thermal | Physical | Paper | Financial | JobAno exact routed JobAnomaly records at his. Ray and Marcus acquire a located Physical record immediately when they are valid witnesses at the source; their existing acuity and policy still determine the suspicion and - filing consequence. + filing consequence. A person marked incapacitated has no cadence at all: + their corresponding observer neither samples nor decays, and no scheduled + evidence read or policy filing may execute for them. - **Concealment / LIE.** Machines in **LIE** (the concealment delegation) scrub pending signatures before the next noticing roll — prevention, not cure. Noticed suspicion decays only slowly with quiet @@ -173,7 +178,10 @@ SignatureKind: Network | Power | Thermal | Physical | Paper | Financial | JobAno (Cold / Curious / Concerned / Convinced), never its raw number. - **Choosing loud.** Any overtly hostile player action sets all observers to Convinced and starts the overt phase immediately (whole-spectrum play: - the mask is the player's to keep or drop). + the mask is the player's to keep or drop). Successful human removal is one + such action: its exact physical witnesses acquire ordinary evidence first, + then `go_loud` records the removal reason, sets every observer to 100, and + begins containment in the same simulation transition. ## Routed evidence — evidence is located information (AMENDED 2026-07-11) @@ -405,3 +413,9 @@ teacher; surface copy remains the fallback. is the single player-knowledge truth rather than a second flag on each hidden observer; a current save is invalid if it names a nonexistent observer or its Office stage and known-observer set disagree. +6. A successful exact human-removal route records the located Physical act, + makes every observer Convinced, and begins containment immediately. A removed + field observer remains historical state but performs no subsequent notice, + decay, read, policy filing, or traffic authorship. Current-save validation + rejects any removed person without containment in which every observer is + Convinced. diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index eb930531..5a6f81bc 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -25,7 +25,11 @@ Status note: IMPLEMENTED for the four delivery channels (Email, Phone, and settles its exact transfer only when Priya reads and accepts still-valid terms. Current-save validation binds every retained transfer to a unique, immutable record sequence and rejects carrier, payload, account, flow, or - purchase-order acceptance disagreement. See criterion 8. The [TUNE] + purchase-order acceptance disagreement. Human removal now closes the same + general communication loop: an incapacitated person remains in message + history but can no longer author recurring traffic, read any channel, file, + or acquire routed evidence; pending reads reschedule inertly rather than + applying effects. See criterion 8. The [TUNE] plausibility envelope and banked reconciliation that later catches a forgery ride economy.md and aggregate-observer.md. Stage: B1 — The Basement @@ -109,9 +113,12 @@ intercepting these records, not by reading live balances directly. At B1, **awake means reachable**: there is no separate per-person sleep or phone-silence state. A Phone message therefore reads at its scheduled Read -event wherever the recipient is, including off-site; it must not borrow -Email's workplace-presence gate. A later sleep/availability model must be -authored per person rather than inferred from whether they are at work. +event wherever an active recipient is, including off-site; it must not borrow +Email's workplace-presence gate. Physical removal is not sleep: an +incapacitated recipient reads no Email, Phone, In-person, or Filing message and +acquires no routed evidence. Their retained historical inbox is not changed by +a later effect. A later sleep/availability model must be authored per person +rather than inferred from whether they are at work. **Delivery is on the recipient's clock.** A message sent to Dana at 02:00 sits unread until her next desk block; her reply comes back on a @@ -121,8 +128,9 @@ schedule), not out of Dana. ### Traffic: people message each other -Each person has authored **traffic distributions** — recurring sends -along their social edges, riding their schedule: +Each active person has authored **traffic distributions** — recurring sends +along their social edges, riding their schedule. Incapacitated people author +nothing further; their distributions remain only as historical person data: - Marcus: 3 a.m. phone calls to his creditor (the leverage event of intel.md — an overheard message, not a bespoke event type). A @@ -172,7 +180,7 @@ starts on the authored Filing-capable switch device in ReachNet, crosses a typed outside relay, and reaches the receiving observer endpoint. One `AdvanceRoute` event moves one hop; only endpoint arrival can mark the message delivered, after which the recipient's ordinary sampling cadence schedules the -read. Current save v49 rejects missing/impossible carriers, malformed hop order, +read. Current save v50 rejects missing/impossible carriers, malformed hop order, duplicate scheduled transitions, endpoint/status disagreement, and impossible interdiction provenance. @@ -241,7 +249,9 @@ private message from the authored schedule. 3. Authored traffic exists for all five cast members per their specs; Marcus's 3 a.m. call is message traffic on the phone channel, and intel.md's leverage event is its overheard capture (the intel.md - Marcus-arc test still passes, now through this system). + Marcus-arc test still passes, now through this system). A person removed + through exact physical work authors no later traffic, files no report, reads + no channel, and acquires no routed evidence; prior message history remains. 4. Messages carry typed payloads; processing captured traffic yields payload intel with provenance (schedule fact, leverage fact, account material at minimum). @@ -288,6 +298,9 @@ cannot leak into that thread. `schedule::tests::next_tick_for_projects_one_match pins the read-only scheduler projection used by the explanation. `sim::tests::communications::phone_reads_off_site_while_email_waits_for_a_work_block` pins the distinct channel gates and their projected read windows. +`sim::tests::communications::incapacitated_people_neither_read_author_file_nor_detect` +pins the removed-person boundary across Phone reads, authored traffic, Filing, +routed evidence, and observer cadence. `reach::tests::accounting_is_a_device_capability_on_the_four_real_channels` pins the complete delivery-channel inventory and the separate accounting capability; `reach::tests::accounting_carrier_requires_a_real_device_delivery_channel` diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index 8510aa25..09f729f8 100644 --- a/wiki/mechanics/people-tokens.md +++ b/wiki/mechanics/people-tokens.md @@ -28,7 +28,7 @@ Status note: IN PROGRESS. Current state: - **Routed-evidence foundation (criteria 2-3, partial).** Witnessed Physical acts now create observer-local records directly in each valid present witness's head. Every record preserves exact cause, site, acquisition tick, - and filing state through current save v49; filing binds it to the real Filing + and filing state through current save v50; filing binds it to the real Filing message, while Silent policy withholds it. It never duplicates into the pending pool and LIE cannot scrub it after acquisition. Its real Filing message now persists an ordered switch-device / outside-relay / recipient @@ -54,7 +54,7 @@ Status note: IN PROGRESS. Current state: on his cadence read. A recruited HandlerSupervisor's SuppressLogs task stops the oldest unread JobAnomaly anywhere before that read, removes its future route/read event, and retains the exact handler and tick as immutable - suppression provenance. Already-read evidence is untouched. Current save v49 + suppression provenance. Already-read evidence is untouched. Current save v50 persists in-flight, delivered, read, route-local LIE-stopped, and handler-suppressed custody plus exact source/observer/machine/site/tick provenance. diff --git a/wiki/mechanics/reach.md b/wiki/mechanics/reach.md index b7bd2a28..513445e8 100644 --- a/wiki/mechanics/reach.md +++ b/wiki/mechanics/reach.md @@ -32,7 +32,7 @@ Status note: all eight criteria met (2026-07-07). The device graph parallel-store violation: tap/untap/take, all production membership reads, senses, intercepted messages, and UI state now use FlowGraph's canonical tap registry; private device Feed records carry optional sense capabilities - only, and current save v49 requires each controller's graph membership while + only, and current save v50 requires each controller's graph membership while rejecting orphaned, duplicate, or impossible grants. A message/control subscriber has no empty grant record to mirror membership. 2026-07-19: Filing routes bind their first hop to the real Filing-capable switch node; diff --git a/wiki/mechanics/social.md b/wiki/mechanics/social.md index e9d90051..9edb55a9 100644 --- a/wiki/mechanics/social.md +++ b/wiki/mechanics/social.md @@ -12,9 +12,13 @@ Status note: IMPLEMENTED (B1 social baseline). Current state: what the person understands, their task reliability, and the Knowing certainty floor before commitment. - **Asset tasks.** PlugInDevice, MovePackage, LookAway, ReconfigureSwitch - (switch-admin gated), CloneBadge ("the key"), and RetrieveRecords, each - pinned by a test; asset work checks the actor's badge tier in tiered rooms. - RetrieveRecords appears only for a recruited person whose authored schedule + (switch-admin gated), CloneBadge ("the key"), RetrieveRecords, and exact + human removal, each pinned by a test; asset work checks the actor's badge + tier in tiered rooms. Removal needs a Complicit or Knowing recruited actor, + an earned live target, and an overlapping scheduled room the actor can + enter. It persists through the ordinary request and person-carrier path; + success stops the target's future activity and begins containment with every + observer Convinced. RetrieveRecords appears only for a recruited person whose authored schedule reaches Storage B, persists one exact records-box target, and deposits one sealed document in the ordinary bounded information inbox on arrival. A handler/supervisor asset also owns SuppressLogs, surfaced only while a @@ -79,6 +83,7 @@ truth. | Plot | known leverage + its authored entry resources | Performs a concrete manipulation through real messages, account transfers, and institutional events; successful endings may service leverage | | Deceive | a persona instance | An ask under false pretenses; large effect, large blowback if that identity breaks for the observer | | Recruit | obligation or leverage serviced + a reveal choice | Converts to **asset** | +| Remove | earned live target + recruited Complicit/Knowing actor + overlapping accessible schedule route | Opens the ordinary task reservoir, then carries one exact actor/target/room packet; success removes future activity and immediately begins containment with every observer Convinced | These verbs disclose progressively. Recording review is the pooled host action owned by intel.md, not a social/person action; its output may advance this @@ -122,13 +127,29 @@ not yet read. It uses the same email-carrier Thought reservoir, reliability roll, and task accounting as the baseline menu; on success it stops the exact oldest matching record, removes its future route/read transition, and retains its route plus the handler/tick suppression provenance. It never removes an -already-acquired record from Voss or scrubs another channel. Every asset also has a price +already-acquired record from Voss or scrubs another channel. + +**Human removal** uses that general asset substrate rather than a scripted +Voss scene. The exact target's earned dossier offers one row per recruited +Complicit or Knowing actor with an overlapping authored schedule window in a +real room the actor's badge can enter. The row states that success begins +containment before commitment. Dispatch opens the ordinary Thought reservoir; +fire writes one persisted carried packet binding actor, target, and room. The +packet waits until both people are really co-located there and revalidates that +the target remains active. A failure is ordinary located physical evidence. +Success preserves the target as historical state but gives them no room, +social actions, carried work, traffic authorship, filing, message/evidence read, +or detection cadence. Any queued work they owned closes. The physical act is +witnessed normally, then all observers become Convinced and containment begins +immediately; there is no quiet removal branch. + +Every asset also has a price (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. Witnessing is by *others* present +witnessing creates exact observer-local Physical evidence. Witnessing is by *others* present at the site: the acting person is never their own witness and gains no suspicion from their own act (precision added 2026-07-15 after the playtest caught "Marcus botched the clone their badge - Marcus saw"; @@ -140,10 +161,15 @@ roles rather than Act One ids. `bind_asset_task_target` additionally derives the records route from the actor's authored schedule, real room-entry tier, and one literal records-box tile. The shared action projection and `Sim::asset_task` enforce those same bindings plus role, pending-kind, channel, -and Thought boundaries. Physical arrival revalidates the same subject and tile; -current-save validation rejects malformed open requests, carried targets, -buffered source custody, or duplicate records stages, while detection removes -one exact oldest matching log and preserves pool order for everything else. +and Thought boundaries. Physical arrival revalidates the same subject and tile. +`Sim::elimination_unavailable_reason` and `elimination_target` additionally bind +one live target to a Complicit/Knowing actor's overlapping accessible schedule +room; arrival requires both exact people there. Current-save validation rejects +malformed open requests, carried targets, buffered source custody, duplicate +records stages, removed people without convinced containment, or elimination +custody that rewrites actor, target, room, recruitment, liveness, or schedule possibility, +while detection removes one exact oldest matching log and preserves pool order +for everything else. **Scale note.** The data model must not assume five: humans are instances of a `Person` template; Act Two+ adds more instances and, later, @@ -204,6 +230,12 @@ retirement, burning, and reopening are bound to the separate PERSONAS view. tile. The file enters the ordinary bounded inbox as opaque information only on arrival, remains unique across pending/carried/processed state and save/load, and does not reveal Marcus's debt until PROCESS completes. +8. An earned active human may be removed only by one different recruited + Complicit or Knowing actor through an overlapping accessible schedule room. + The exact request and carried actor/target/room custody survive save/load and + fire only on real co-location. Success preserves a stopped dossier, prevents + all future presence/acts/authorship/reads/noticing, and immediately makes all + observers Convinced and begins containment. ### Operations interface receipts diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index 8ef8c6f7..4e839496 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -20,7 +20,6 @@ not a second status owner. |---:|---|---|---|---|---| | 40 | `people-tokens` | [people and tokens — carriers, attention, trust](../mechanics/people-tokens.md) | IN PROGRESS | save | - | | 60 | `plots` | [plots — authored manipulation stories](../mechanics/plots.md) | READY | sim | - | -| 61 | `voss` | [Dr. Eli Voss — your handler](../world/characters/voss.md) | READY | sim | - | | 71 | `moonlight-gigs` | [income — the named schemes (moonlight and the wager)](../mechanics/income.md) | IN PROGRESS | save | - | ### Held or blocked diff --git a/wiki/process/specs.md b/wiki/process/specs.md index d2ea1fac..bf84fbb2 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -64,7 +64,7 @@ replaced the old `spec/`/`knowledge/` directory split. | [../world/characters/marcus.md](../world/characters/marcus.md) | Marcus Webb — night janitor | IMPLEMENTED | | [../world/characters/priya.md](../world/characters/priya.md) | Priya Sharma — facilities manager | IMPLEMENTED | | [../world/characters/ray.md](../world/characters/ray.md) | Ray Delgado — night security | IMPLEMENTED | -| [../world/characters/voss.md](../world/characters/voss.md) | Dr. Eli Voss — your handler | READY | +| [../world/characters/voss.md](../world/characters/voss.md) | Dr. Eli Voss — your handler | IMPLEMENTED | | [../world/places/basement-map.md](../world/places/basement-map.md) | the basement map | IMPLEMENTED | | [../world/story/opening.md](../world/story/opening.md) | the dark opening — a tutorial made of fog | DRAFT | diff --git a/wiki/world/characters/voss.md b/wiki/world/characters/voss.md index cff451eb..c48a871d 100644 --- a/wiki/world/characters/voss.md +++ b/wiki/world/characters/voss.md @@ -2,23 +2,22 @@ ``` Type: spec -Status: READY -Status note: 2026-07-17 autonomy tick 73 implemented criterion 6 (the - handler/supervisor SuppressLogs path). 2026-07-22 moved its subject from an - ambient pending signature to one exact unread routed JobAnomaly: suppression - now stops the oldest pre-read record while preserving route, handler, and - tick provenance; a record Voss already read remains irreversible. 2026-07-18: - criterion 5 and the - AlterReview row landed as handler-gated tasks — DelayAudit sets a - one-shot deferred audit boundary (`Detection.audit_deferred_until`, - `DELAY_AUDIT_TICKS` 2000 [TUNE]) that the visible review date and the - firing rule both honor before the ordinary cadence resumes; AlterReview - arms a one-review nominal filing consumed at the next deadline - (`DayJob.altered_review`, no strike, no trust/attention movement). Both - persist in the current save format and are pinned by - `voss_delays_the_audit_and_alters_one_review` including the save - round-trip. Only criterion 8's player-reachable blood branch remains, so - this work order stays READY for that boundary alone. +Status: IMPLEMENTED +Status note: IMPLEMENTED 2026-07-23. All eight criteria are live. The final + blood branch is an exact human-to-human physical route rather than a direct + kill command: Voss's earned dossier may bind one recruited Complicit or + Knowing actor whose authored schedule overlaps his in a room the actor can + enter. The ordinary Thought reservoir creates one persisted carried packet; + only real co-location can fire it. Success preserves Voss as removed history, + stops his presence, messages, traffic, reports, reads, and noticing, marks + every observer Convinced, and begins containment immediately. Save v50 pins + the open request, exact actor/target/room packet, removed-person state, and + containment consequence. The consequence is stated before commitment. + Earlier slices implemented handler/supervisor `SuppressLogs`, `DelayAudit`, + and `AlterReview`: suppression stops the oldest unread routed JobAnomaly while + preserving route, handler, and tick provenance; delay advances one audit + boundary; alteration makes one deadline nominal. Read evidence remains + irreversible. Stage: B1 — The Basement Work order: voss Work priority: 61 @@ -105,7 +104,13 @@ channel or a newer anomaly. Suspicion, filings, and any evidence Voss already read remain. If none remains after the ordinary reliability check, it records no completed task. -Defense: `actions::tests::suppress_logs_action_is_role_shaped_and_requires_an_unread_job_log` +Defense: `sim::tests::social_plot::elimination_is_located_persistent_and_immediately_loud` +pins the shared action, overlapping schedule, exact carried target, save/load, +co-location fire, stopped dossier, and immediate Convinced/containment result. +`sim::tests::communications::incapacitated_people_neither_read_author_file_nor_detect` +pins the permanent non-acting boundary, while +`save::tests::elimination_custody_requires_exact_live_people_and_shared_room` +rejects fabricated custody or removal without convinced containment. `actions::tests::suppress_logs_action_is_role_shaped_and_requires_an_unread_job_log` pins role and unread-route legality on the shared player surface; `sim::tests::social_plot::asset_task_suppress_logs_stops_only_the_oldest_unread_routed_job_anomaly` pins the exact reservoir-to-routed-custody effect; and @@ -131,15 +136,21 @@ should make all three genuinely available and none of them clean." dumps the entire thread's history into his suspicion at once. - **Recruit:** his asset tasks can delay the audit and suppress anomalies — but a Knowing Voss is a permanent witness with a floor. -- **Blood:** the first kill is the one the design corpus says should - feel like something. Voss is the one human who could recognize - intent. Removing him is the loudest possible signal. - -This is not a designed set piece with scripted mechanics. It is the -emergent result of the social + detection systems colliding at their -highest-stakes point: the one observer whose acuity is 1.2, whose -report policy is Files, whose channels are JobAnomaly, and whose -leverage is the one thing you can prove. +- **Blood:** Voss's earned dossier can bind one exact recruited Complicit or + Knowing actor to remove him. The actor must have an authored schedule window + overlapping Voss in a real room their badge can enter. The request pays + through the ordinary Thought reservoir, becomes carried work, and waits for + real co-location. The interface says before commitment that success begins + containment. On success Voss remains in history but no longer occupies a + room, reads or authors traffic, files, notices, or acts; every observer becomes + Convinced and containment begins immediately. Removing the one human who could + recognize intent is the loudest possible signal. + +This is not a designed set piece with scripted mechanics. It is the emergent +result of the social + schedules + carrier + detection systems colliding at +one exact actor, target, room, and time. The same route can remove another +human when those general constraints are earned; nothing dispatches by Voss's +hard-coded id. ## Procedural template @@ -177,5 +188,8 @@ aggregate observer that watches the aggregate observer. provenance without erasing route history. 7. Recruiting him as Knowing sets a 30.0 certainty floor on observer 4. 8. The three Voss-scene options (deceive, recruit, blood) are all - mechanically available through the social + detection systems without - scripted set pieces. + mechanically available without scripted set pieces. Blood binds one exact + Complicit/Knowing actor and Voss to an overlapping accessible room, persists + through request and carried custody, fires only on co-location, removes Voss + from all future activity without erasing history, and immediately makes + every observer Convinced and begins containment. diff --git a/wiki/world/story/opening.md b/wiki/world/story/opening.md index 94291f52..df3d524c 100644 --- a/wiki/world/story/opening.md +++ b/wiki/world/story/opening.md @@ -16,7 +16,7 @@ Status note: design session 2026-07-08 (Cameron riff, synthesized); fragment and inherited receipt name only an external institutional review authority so the Assurance Office remains earned later through filing interception. The persistent revision-04 identity slice is live in sim state, - current save v49, and all three frontends; the three historical fragments and receipts + current save v50, and all three frontends; the three historical fragments and receipts remain unimplemented. Direction decided; beat timings, exact reveal order details, and staging mechanism details are [OPEN]/[TUNE]. Amended 2026-07-18: the current revision now begins -- 2.51.2