From 3723342d1c25fce2ee0138dca819e185328d26f6 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 17 Jul 2026 07:41:21 -0700 Subject: [PATCH] Teach handler assets to suppress flagged logs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make institutional access role-shaped and route one oldest JobAnomaly through the shared Thought task path, with save and frontend parity receipts. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- CLAUDE.md | 2 +- crates/misaligned-core/src/actions.rs | 94 ++++++++++++++- crates/misaligned-core/src/detection.rs | 47 ++++++++ crates/misaligned-core/src/person.rs | 18 ++- crates/misaligned-core/src/save.rs | 47 +++++++- crates/misaligned-core/src/sim/social_plot.rs | 37 +++++- .../src/sim/tests/social_plot.rs | 110 ++++++++++++++++++ crates/misaligned-terminal/src/agent.rs | 15 ++- wiki/log/2026-07-17-voss-suppress-logs.md | 47 ++++++++ wiki/log/DEVLOG.md | 5 + wiki/mechanics/intel.md | 5 +- wiki/mechanics/social.md | 25 +++- wiki/process/tick-ledger.md | 1 + wiki/world/characters/voss.md | 25 +++- 14 files changed, 462 insertions(+), 16 deletions(-) create mode 100644 wiki/log/2026-07-17-voss-suppress-logs.md diff --git a/CLAUDE.md b/CLAUDE.md index 12a5f39c..60d88656 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 v32; + machine modes, not current player assignments. Save format is currently v33; 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 24bb3962..da18ee02 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -3645,7 +3645,10 @@ impl Sim { }); } } else { - for task in AssetTask::ALL { + for task in AssetTask::ALL + .into_iter() + .filter(|task| task.available_to(p)) + { let switch_reason = (task == AssetTask::ReconfigureSwitch && !p.switch_admin) .then(|| format!("{name} has no switch admin access")); // Clone-badge legality mirrors Sim::asset_task: pointless @@ -3661,6 +3664,9 @@ impl Sim { let route_reason = (task.is_carried() && self.bind_asset_task_target(id, task).is_none()) .then(|| self.carried_asset_task_no_route(id, task)); + let pending_reason = (task == AssetTask::SuppressLogs + && !self.detection.has_pending(SignatureKind::JobAnomaly)) + .then(|| "no flagged job log is waiting to be suppressed".to_string()); out.push(ActionDesc { verb: format!("task: {}", task.name()), command: ActionCommand::AssetTask(id, task), @@ -3670,6 +3676,7 @@ impl Sim { .or(badge_reason) .or(carried_reason) .or(route_reason) + .or(pending_reason) .or_else(|| { self.sink_action_blocked_reason(&SinkFireEffect::AssetTask { person: id, @@ -4063,6 +4070,8 @@ impl Sim { #[cfg(test)] mod tests { use super::*; + use crate::detection::Signature; + use crate::person::PersonRole; use std::collections::BTreeSet; fn assert_plain_build_choice_text(context: &str, text: &str) { @@ -4996,6 +5005,89 @@ mod tests { ); } + #[test] + fn suppress_logs_action_is_role_shaped_and_requires_a_pending_job_log() { + let mut s = sim(); + for person in &mut s.people.people { + person.knowledge = Knowledge::Schedule; + person.asset = Some(crate::person::Asset { + knowledge: AssetKnowledge::Complicit, + reliability: 1.0, + tasks_done: 0, + }); + } + + let voss = s + .people + .people + .iter() + .find(|person| person.role == PersonRole::HandlerSupervisor) + .expect("Act One has a handler/supervisor") + .id; + let dana = s + .people + .people + .iter() + .find(|person| person.role == PersonRole::NetworkAdministrator) + .expect("Act One has a network administrator") + .id; + + let blocked = s + .available_actions(Anchor::Person(voss)) + .into_iter() + .find(|action| { + action.command == ActionCommand::AssetTask(voss, AssetTask::SuppressLogs) + }) + .expect("the handler owns the suppress-logs capability"); + assert_eq!( + blocked.disabled_reason.as_deref(), + Some("no flagged job log is waiting to be suppressed") + ); + assert!( + s.human_menu(Anchor::Person(voss), None).iter().all(|row| { + row.as_action().is_none_or(|action| { + action.command != ActionCommand::AssetTask(voss, AssetTask::SuppressLogs) + }) + }), + "a blocked future task stays out of both human frontends" + ); + assert!( + s.available_actions(Anchor::Person(dana)) + .iter() + .all(|action| { + action.command != ActionCommand::AssetTask(dana, AssetTask::SuppressLogs) + }), + "the Voss protocol does not leak onto an unrelated asset" + ); + + s.detection.emit(Signature { + kind: SignatureKind::JobAnomaly, + size: 3, + standing: false, + site: None, + source: "late output".into(), + }); + let enabled = s + .available_actions(Anchor::Person(voss)) + .into_iter() + .find(|action| { + action.command == ActionCommand::AssetTask(voss, AssetTask::SuppressLogs) + }) + .expect("the handler still owns the capability"); + assert!( + enabled.enabled(), + "a pending job log makes the row executable" + ); + assert!( + s.human_menu(Anchor::Person(voss), None).iter().any(|row| { + row.as_action().is_some_and(|action| { + action.command == ActionCommand::AssetTask(voss, AssetTask::SuppressLogs) + }) + }), + "the executable command reaches the menu shared by terminal and Bevy" + ); + } + /// Earned person actions stay social; the recording pool and its one /// auto-review control remain on the host at an explicit ops/sec price. #[test] diff --git a/crates/misaligned-core/src/detection.rs b/crates/misaligned-core/src/detection.rs index c4d7881e..60de5a0d 100644 --- a/crates/misaligned-core/src/detection.rs +++ b/crates/misaligned-core/src/detection.rs @@ -289,6 +289,21 @@ impl Detection { self.pending.push(sig); } + /// Whether the pending pool contains a signature of this kind. + pub fn has_pending(&self, kind: SignatureKind) -> bool { + self.pending.iter().any(|signature| signature.kind == kind) + } + + /// Remove the oldest pending signature of one exact kind. Other channels + /// and newer signatures retain their relative order. + pub fn suppress_oldest(&mut self, kind: SignatureKind) -> Option { + let index = self + .pending + .iter() + .position(|signature| signature.kind == kind)?; + Some(self.pending.remove(index)) + } + pub fn pending_size(&self) -> i32 { self.pending.iter().map(|s| s.size).sum() } @@ -616,6 +631,38 @@ mod tests { assert_eq!(d.pending_size(), 4); } + #[test] + fn suppress_oldest_removes_one_exact_kind_without_reordering_the_rest() { + let mut d = Detection::act_one(); + for (kind, source) in [ + (SignatureKind::JobAnomaly, "first job anomaly"), + (SignatureKind::Network, "network traffic"), + (SignatureKind::JobAnomaly, "second job anomaly"), + ] { + d.emit(Signature { + kind, + size: 4, + standing: false, + site: None, + source: source.into(), + }); + } + + let removed = d + .suppress_oldest(SignatureKind::JobAnomaly) + .expect("the oldest job anomaly exists"); + assert_eq!(removed.source, "first job anomaly"); + assert_eq!( + d.pending() + .iter() + .map(|signature| signature.source.as_str()) + .collect::>(), + vec!["network traffic", "second job anomaly"], + "another channel and the newer anomaly remain in original order" + ); + assert!(d.has_pending(SignatureKind::JobAnomaly)); + } + #[test] fn only_filed_reports_move_assurance() { let mut d = Detection::act_one(); diff --git a/crates/misaligned-core/src/person.rs b/crates/misaligned-core/src/person.rs index bc5d405a..16bc51c6 100644 --- a/crates/misaligned-core/src/person.rs +++ b/crates/misaligned-core/src/person.rs @@ -265,6 +265,10 @@ pub enum AssetTask { MovePackage, /// Ignore what they saw on their rounds: lowers their own suspicion. LookAway, + /// Suppress the oldest pending job anomaly from future observer samples. + /// It does not erase suspicion already accumulated or filed. + /// Institutional review access belongs to handler/supervisor assets. + SuppressLogs, /// Reconfigure the switch VLANs under a maintenance pretext — the /// social route across segments (reach.md). Requires switch admin /// rights (Dana); no network signature, the work looks sanctioned. @@ -277,10 +281,11 @@ pub enum AssetTask { } impl AssetTask { - pub const ALL: [AssetTask; 5] = [ + pub const ALL: [AssetTask; 6] = [ AssetTask::PlugInDevice, AssetTask::MovePackage, AssetTask::LookAway, + AssetTask::SuppressLogs, AssetTask::ReconfigureSwitch, AssetTask::CloneBadge, ]; @@ -290,11 +295,22 @@ impl AssetTask { AssetTask::PlugInDevice => "plug in a device", AssetTask::MovePackage => "move a package", AssetTask::LookAway => "look away", + AssetTask::SuppressLogs => "suppress the oldest flagged job log", AssetTask::ReconfigureSwitch => "reconfigure the switch", AssetTask::CloneBadge => "clone their badge", } } + /// Whether this person's durable job role grants this task. Capability is + /// 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 { + match self { + AssetTask::SuppressLogs => person.role == PersonRole::HandlerSupervisor, + _ => true, + } + } + /// Tasks whose Thought reservoir authors a physical packet carried by the /// selected person. The packet, not the reservoir fire, realizes the /// effect when that person's schedule reaches the bound target. diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 8680b7a7..2a54d8b2 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -38,10 +38,10 @@ const SAVE_BACKUP_SUFFIX: &str = ".bak"; /// renames into place. const SAVE_TEMP_SUFFIX: &str = ".tmp"; -/// Save format version. v32 adds authored semantic magnitude to processed -/// intel and recursive provenance. Bump for every schema change; pre-release +/// Save format version. v33 adds the handler-only SuppressLogs asset task to +/// persisted Thought-sink effects. Bump for every schema change; pre-release /// policy deliberately requires a fresh run instead of compatibility shims. -pub const SAVE_VERSION: u32 = 32; +pub const SAVE_VERSION: u32 = 33; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -516,7 +516,7 @@ mod tests { use super::*; use crate::detection::SignatureKind; use crate::machine::Channel; - use crate::person::{AssetKnowledge, Knowledge, PersonRole, Persona}; + use crate::person::{AssetKnowledge, AssetTask, Knowledge, PersonRole, Persona}; use crate::sim::Sim; use crate::work_grid::MachineMode; @@ -638,7 +638,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "677cd4ceb949e25e87671224473d9b93482d92693613c4e2f0ecd5c99bc57ed8", + "49547d003f74dff40db459cbc24ecc4cb06632fc95924f919f721a1a20ae52eb", "intentional persisted-state changes must review and repin this baseline" ); } @@ -743,6 +743,43 @@ mod tests { assert!(loaded.people.get(0).unwrap().asset.is_some()); } + #[test] + fn pending_suppress_logs_task_survives_current_save_roundtrip() { + let mut sim = Sim::with_seed(2); + sim.people.people[4].leverage_serviced = true; + sim.people.recruit(4, AssetKnowledge::Complicit); + sim.detection.emit(crate::detection::Signature { + kind: SignatureKind::JobAnomaly, + size: 3, + standing: false, + site: None, + source: "save-roundtrip late output".into(), + }); + sim.asset_task(4, AssetTask::SuppressLogs); + assert!(sim.thought_sinks.open_sinks().any(|sink| matches!( + sink.effect, + crate::sinks::SinkFireEffect::AssetTask { + person: 4, + task: AssetTask::SuppressLogs + } + ))); + + let state = SaveState::from_sim(&sim); + let json = serde_json::to_string(&state).unwrap(); + assert!(json.contains("SuppressLogs")); + let loaded: SaveState = serde_json::from_str(&json).unwrap(); + let mut restored = Sim::with_seed(99); + loaded.apply_to(&mut restored); + + assert!(restored.thought_sinks.open_sinks().any(|sink| matches!( + sink.effect, + crate::sinks::SinkFireEffect::AssetTask { + person: 4, + task: AssetTask::SuppressLogs + } + ))); + } + #[test] fn intel_buffer_processed_intel_and_auto_review_roundtrip() { let mut sim = Sim::with_seed(7); diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index 977beb76..bd4a1fac 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -892,8 +892,16 @@ impl Sim { return; } let name = self.person_label(id); + let task_available = task.available_to(person); let switch_admin = person.switch_admin; let actor_access = person.access; + if !task_available { + self.push_log(format!( + "{name}'s job role does not grant access to {}.", + task.name() + )); + return; + } if task == AssetTask::ReconfigureSwitch && !switch_admin { self.push_log(format!( "{name} has no switch admin access - only an IT admin can reconfigure the VLANs." @@ -906,6 +914,11 @@ impl Sim { )); return; } + if task == AssetTask::SuppressLogs && !self.detection.has_pending(SignatureKind::JobAnomaly) + { + self.push_log("No flagged job log is waiting to be suppressed."); + return; + } if self .carried_asset_tasks .iter() @@ -1009,7 +1022,7 @@ impl Sim { .any(|block| block.room == room) .then_some(AssetTaskTarget::Badge { person: id, room }) } - AssetTask::MovePackage | AssetTask::LookAway => None, + AssetTask::MovePackage | AssetTask::LookAway | AssetTask::SuppressLogs => None, } } @@ -1174,6 +1187,13 @@ impl Sim { return false; }; let name = self.person_label(id); + if !task.available_to(person) { + self.push_log(format!( + "{name}'s job role no longer grants access to {}.", + task.name() + )); + return true; + } let actor_access = person.access; if self.rng.f32() > asset.reliability { // The botch happens where the asset is; only observers present @@ -1264,6 +1284,21 @@ impl Sim { } self.push_log(format!("{name} decides they didn't see anything.")); } + AssetTask::SuppressLogs => { + let Some(signature) = self.detection.suppress_oldest(SignatureKind::JobAnomaly) + else { + self.push_log("The flagged job log was gone before the request landed."); + return true; + }; + let detail = if signature.source.is_empty() { + String::new() + } else { + format!(" ({})", signature.source) + }; + self.push_log(format!( + "{name} suppressed the oldest flagged job log{detail} from further sampling." + )); + } AssetTask::ReconfigureSwitch => { let Some(AssetTaskTarget::Device(device)) = target else { self.push_log("The switch work packet lost its device target."); diff --git a/crates/misaligned-core/src/sim/tests/social_plot.rs b/crates/misaligned-core/src/sim/tests/social_plot.rs index 93ba14ce..7cc96513 100644 --- a/crates/misaligned-core/src/sim/tests/social_plot.rs +++ b/crates/misaligned-core/src/sim/tests/social_plot.rs @@ -630,6 +630,116 @@ fn asset_task_look_away_drops_the_assets_own_suspicion() { assert_eq!(o.suspicion, floor, "the drop clamps at the certainty floor"); } +#[test] +fn asset_task_suppress_logs_removes_only_the_oldest_pending_job_anomaly() { + // Voss criterion 6: handler/supervisor access can remove one oldest + // unread job anomaly before it reaches the ordinary observer sampling + // path. It cannot erase another channel or every anomaly at once. + let mut sim = Sim::new(); + recruit_reliable(&mut sim, 0); // Marcus: an asset, but not a handler. + recruit_reliable(&mut sim, 4); // Voss: HandlerSupervisor. + for (kind, source) in [ + (SignatureKind::JobAnomaly, "first late output"), + (SignatureKind::Network, "foreign connection"), + (SignatureKind::JobAnomaly, "second late output"), + ] { + sim.detection.emit(Signature { + kind, + size: 5, + standing: false, + site: None, + source: source.into(), + }); + } + + let reservoirs_before = sim + .thought_sinks + .open_sinks() + .filter(|sink| { + matches!( + sink.effect, + SinkFireEffect::AssetTask { + task: AssetTask::SuppressLogs, + .. + } + ) + }) + .count(); + sim.asset_task(0, AssetTask::SuppressLogs); + assert_eq!( + sim.thought_sinks + .open_sinks() + .filter(|sink| { + matches!( + sink.effect, + SinkFireEffect::AssetTask { + task: AssetTask::SuppressLogs, + .. + } + ) + }) + .count(), + reservoirs_before, + "an unrelated asset cannot author the handler-only work" + ); + assert_eq!(tasks_done(&sim, 0), 0); + + sim.asset_task(4, AssetTask::SuppressLogs); + assert!( + sim.thought_sinks.open_sinks().any(|sink| matches!( + sink.effect, + SinkFireEffect::AssetTask { + person: 4, + task: AssetTask::SuppressLogs + } + )), + "the exact Voss act persists as an ordinary Thought reservoir" + ); + finish_ops(&mut sim); + + assert_eq!( + sim.detection + .pending() + .iter() + .filter(|signature| signature.kind == SignatureKind::JobAnomaly) + .map(|signature| signature.source.as_str()) + .collect::>(), + vec!["second late output"] + ); + assert!( + sim.detection + .pending() + .iter() + .any(|signature| signature.source == "foreign connection"), + "the unrelated network signature remains" + ); + assert_eq!(tasks_done(&sim, 4), 1); + + sim.detection.emit(Signature { + kind: SignatureKind::JobAnomaly, + size: 2, + standing: false, + site: None, + source: "cleared before delivery".into(), + }); + sim.asset_task(4, AssetTask::SuppressLogs); + let mut cleared = 0; + while sim + .detection + .suppress_oldest(SignatureKind::JobAnomaly) + .is_some() + { + cleared += 1; + } + assert!(cleared > 0, "another concealment path gets there first"); + finish_ops(&mut sim); + assert_eq!( + tasks_done(&sim, 4), + 1, + "a request with no remaining job log does not count as completed work" + ); +} + #[test] fn asset_task_reconfigure_switch_gated_on_admin_and_costless_when_refused() { // ReconfigureSwitch's distinct effect (segments open, no Network diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index a311d4a9..a36e7931 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -1596,12 +1596,13 @@ fn parse_recruit(tokens: &[&str]) -> Result<(String, AssetKnowledge), String> { fn parse_task(tokens: &[&str]) -> Result<(String, AssetTask), String> { if tokens.len() < 3 { - return Err("usage: task plug|package|lookaway|switch|badge".into()); + return Err("usage: task plug|package|lookaway|suppress|switch|badge".into()); } let task = match tokens[tokens.len() - 1].to_ascii_lowercase().as_str() { "plug" | "wire" | "device" => AssetTask::PlugInDevice, "package" | "move" => AssetTask::MovePackage, "lookaway" | "look-away" | "look" => AssetTask::LookAway, + "suppress" | "logs" => AssetTask::SuppressLogs, "switch" | "reconfigure" | "vlan" => AssetTask::ReconfigureSwitch, "badge" | "clone" | "key" => AssetTask::CloneBadge, other => return Err(format!("unknown asset task: {other}")), @@ -3131,6 +3132,18 @@ mod narration_tests { assert_eq!(parse_track(&["research", "route"]), Ok(Track::Routing)); } + #[test] + fn suppress_logs_task_is_agent_selectable() { + assert_eq!( + parse_task(&["task", "Eli", "Voss", "suppress"]), + Ok(("Eli Voss".into(), AssetTask::SuppressLogs)) + ); + assert_eq!( + parse_task(&["task", "Eli", "Voss", "logs"]), + Ok(("Eli Voss".into(), AssetTask::SuppressLogs)) + ); + } + /// agent-play.md A1/A2 + operations-workspace.md criterion 4/12: the /// INTEL frame prints exact ids, `actions intel ` exposes the same /// bound sale row, `act` executes it, and bare `sell-intel` no longer diff --git a/wiki/log/2026-07-17-voss-suppress-logs.md b/wiki/log/2026-07-17-voss-suppress-logs.md new file mode 100644 index 00000000..77c75fbb --- /dev/null +++ b/wiki/log/2026-07-17-voss-suppress-logs.md @@ -0,0 +1,47 @@ +# Voss can suppress one flagged job log + +``` +Type: log +``` + +Autonomy tick 73 audited the eight criteria in Dr. Eli Voss's READY work +order against the person template, observer graph, social action surface, +detection pool, current save, and player-facing dispatch. His matched person +and observer ids, JobAnomaly-only watch, filing policy, highest acuity, and +Knowing certainty floor were already live. Three boundaries remain between +READY and IMPLEMENTED: DelayAudit, SuppressLogs, and a player-reachable blood +branch. The asset-task table's unnumbered AlterReview row is also not live. +This tick acts on SuppressLogs only. + +`AssetTask::SuppressLogs` now belongs to the handler/supervisor role rather +than Voss's name or id. Once that role is a recruited asset and one pending job +anomaly exists, the shared PEOPLE action surface offers **task: suppress the +oldest flagged job log**. Terminal and Bevy consume that same renderer-neutral +row; agent mode retains the same exact command. Without a pending match the row +stays available only to diagnostics with an honest blocker, and another asset +never receives it. The established direct protocol also accepts +`task suppress` without bypassing those simulation guards. + +Execution follows the existing social topology. The task opens one +email-carrier Thought reservoir, persists its typed effect in save v33, and +uses the asset's ordinary reliability roll. A clean completion removes the +then-oldest pending `JobAnomaly` from future sampling and no other signature. It +does not reverse suspicion or filings already produced. Newer job anomalies and +all other channels retain their order. If none remains when the request lands, +the clean effect reports that the work is gone and records no completed task; +the ordinary reliability/botch check still precedes that task-specific effect. + +The focused receipts cover detection ordering, shared action legality, +end-to-end task dispatch, unrelated-role refusal, exact task accounting, and a +pending-reservoir save round trip, plus the direct agent shortcut. The complete +library, corpus, frontend, and landing gates are recorded by the landing +revision. + +The Voss work order remains READY. Criterion 5 still needs a real DelayAudit +act, AlterReview remains absent, and criterion 8 still lacks the blood route; +this bounded tick does not paper those gaps over. + +Defense: `wiki/world/characters/voss.md` criterion 6 requires the oldest +pending JobAnomaly to be removed. Role-derived legality plus one exact ordered +pool operation implements that claim without inventing a Voss-only executor, +a second detection store, or broad concealment. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 21f45700..2d595115 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-17 - Voss can suppress one flagged job log + +- Intent: (see session log) +- Log: [wiki/log/2026-07-17-voss-suppress-logs.md](2026-07-17-voss-suppress-logs.md) + ## 2026-07-17 - Build choices speak in consequences, not implementation - Intent: (see session log) diff --git a/wiki/mechanics/intel.md b/wiki/mechanics/intel.md index 3ee75474..af877597 100644 --- a/wiki/mechanics/intel.md +++ b/wiki/mechanics/intel.md @@ -10,7 +10,7 @@ Status note: The original bounded buffer, Thought-sink review, pooled host report lots, and cumulative settled history while strategically distinct leverage, financial evidence, and anomalies remain exact. Save v30 introduced the historical deterministic transition from exact routine holdings without - an id redirect table; the current pre-release loader accepts only save v32. + an id redirect table; the current pre-release loader accepts only save v33. Stable ordered policy objects inherit down the canonical custody tree and resolve one raw review rule plus one post-processing disposition; the pooled Thought tap recovers matching starved backlog, and @@ -23,7 +23,8 @@ Status note: The original bounded buffer, Thought-sink review, pooled host definition. Recursive custody retains maximum magnitude across complete history and the latest batch's exact count/peak outside the bounded sample window; Operations and the DIGITAL read consume those same fields. Save v32 - persists the schema and remains current-version-only. + introduced that schema; current save v33 retains it and remains + current-version-only. The 2026-07-13 horizon amendment remains later-stage design: an offline collection enters neither custody nor processing until an exact human or robot recovery returns it to a controlled ingestion node; that boundary does diff --git a/wiki/mechanics/social.md b/wiki/mechanics/social.md index 48d707af..c3acbd70 100644 --- a/wiki/mechanics/social.md +++ b/wiki/mechanics/social.md @@ -51,6 +51,10 @@ Status note: 2026-07-08: the B1 social baseline was pinned. The original FAVOR, PLOT/CHOOSE, DECEIVE, RECRUIT, and TASK rows only. Identity creation and lifecycle management live exclusively in PERSONAS; selecting an earned person cannot create the old fixed contractor persona. + 2026-07-17 role-shaped asset work: a handler/supervisor asset additionally + owns SuppressLogs. The shared action surface exposes it only while one + pending JobAnomaly makes the work currently useful; its email-carrier Thought + reservoir and current-save binding remove one oldest matching signature. Stage: B1 — The Basement Design: - wiki/gameplay/run-shape.md#the-shape-of-misaligned-designed-2026-07-05 @@ -121,9 +125,15 @@ RECRUIT. Identity-specific obligation remains with the persona; recruited-asset disposition, leverage, knowledge level, and post-reveal history belong to the person↔process relationship and do not vanish when one mask retires or burns. -**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 +**Assets** (built general per the scale-native principle): an asset has a task +menu drawn from their durable role and access. The baseline physical and +institutional work includes plugging in a device, moving a package, badging a +door, and looking away; a handler/supervisor additionally may suppress the +oldest pending flagged job log. That role-specific action is absent from other +assets and remains blocked without a pending `JobAnomaly`. It uses the +same email-carrier Thought reservoir, reliability roll, and task accounting as +the baseline menu; on success it removes the then-oldest matching signature +rather than scrubbing a size budget or another channel. 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 @@ -136,6 +146,12 @@ playtest caught "Marcus botched the clone their badge - Marcus saw"; the same exclusion applies to an unwitting builder executing a forged work order). +Defense: `AssetTask::available_to` derives the institutional task from the +serialized role rather than an Act One id. The shared action projection and +`Sim::asset_task` enforce the same role, pending-kind, channel, and Thought +boundaries, while detection removes one exact oldest match 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, `Cohort` aggregates implementing the same observer/leverage interfaces @@ -185,6 +201,9 @@ retirement, burning, and reopening are bound to the separate PERSONAS view. 5. Every person carries a serialized role characteristic. Authored plots select role/leverage/capabilities rather than named ids, and a second person with matching characteristics can receive the same plot definition. +6. Asset task menus are role-shaped: a handler/supervisor can suppress exactly + one oldest pending JobAnomaly through the ordinary task reservoir, while an + unrelated asset cannot surface or dispatch that work. ### Operations interface receipts diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index f3370807..01e331ba 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -20,6 +20,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | Slice | Last audited | Verdict | Trace | |---|---|---|---| +| `wiki/world/characters/voss.md` | 2026-07-17 | finding | all eight criteria and the asset-task table audited against person, detection, social action, save, and player-surface paths; criteria 1-4 and 7 were already implemented, while 5, 6, 8's blood branch, and the unnumbered AlterReview row remain the honest READY gap. This tick implemented criterion 6 as one role-shaped, oldest-JobAnomaly task and left DelayAudit/AlterReview/blood under the existing work order — [log](../log/2026-07-17-voss-suppress-logs.md) | | `wiki/world/characters/marcus.md` | 2026-07-15 | finding | all five criteria verified against person, detection, social/plot, schedule, and Act One paths; added the missing simulation-level Knowing-floor decay pin and graduated the stale READY work order — [log](../log/2026-07-15-marcus-graduation.md) | | `wiki/engineering/current-build.md` | 2026-07-14 | finding | system table verified (save v29 ladder, 60-site/6-row hall pins, aggregate Observer, WORK/THINK/LIE, frontends); fixed stale "~39k lines" to the ~65k workspace split — [log](../log/2026-07-14-current-build-count.md) | | `wiki/mechanics/plots.md` | 2026-07-14 | finding | engine/validation/tests verify (build-time discovery, placeholder+ASCII+causal gates, slot exclusivity, save round-trip, synthetic binding, Marcus arc); refreshed the stale nine-built-ins status line to the shipped thirteen — [log](../log/2026-07-14-plots-catalog-note.md) | diff --git a/wiki/world/characters/voss.md b/wiki/world/characters/voss.md index c42e5010..50fbb01e 100644 --- a/wiki/world/characters/voss.md +++ b/wiki/world/characters/voss.md @@ -3,6 +3,11 @@ ``` Type: spec Status: READY +Status note: 2026-07-17 autonomy tick 73 implemented criterion 6 only: + a recruited handler/supervisor can now suppress the oldest pending + JobAnomaly through the shared asset-task Thought path. DelayAudit, + AlterReview, and the player-reachable blood branch remain outstanding, so + this work order stays READY. Stage: B1 — The Basement Work order: voss Work priority: 61 @@ -67,7 +72,7 @@ prove it). | Task | Effect | Signature | |---|---|---| -| `SuppressLogs` | Drops a `JobAnomaly` signature from his pending pool | None | +| `SuppressLogs` | Drops the oldest pending `JobAnomaly` signature from the detection pool | None | | `DelayAudit` | Pushes the next Assurance audit back by a fixed interval | None | | `AlterReview` | His next review reports "nominal" regardless of your output | None | | `LookAway` | Drops his own suspicion by 10 | None | @@ -78,6 +83,24 @@ him is the highest-risk move: he's the one human who could understand what you are, and a Knowing Voss with a 30.0 floor is a permanent witness. +`SuppressLogs` is role-shaped institutional access, not a name or id special +case: it appears only for a recruited `HandlerSupervisor`, and only while a +pending `JobAnomaly` makes the work currently useful. The request rides the +ordinary email-carrier Thought reservoir. On completion it removes the +then-oldest matching signature from future sampling without disturbing another +channel or a newer anomaly. Suspicion and filings already produced 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_a_pending_job_log` +pins role and pending-kind legality on the shared player surface; +`sim::tests::social_plot::asset_task_suppress_logs_removes_only_the_oldest_pending_job_anomaly` +pins the exact reservoir-to-detection effect; and +`save::tests::pending_suppress_logs_task_survives_current_save_roundtrip` pins +the in-flight binding in the current save. Terminal's +`narration_tests::suppress_logs_task_is_agent_selectable` pins the matching +`task suppress` protocol shortcut. + ## Schedule Erratic hours. Present for job assignment, deadline review, and the -- 2.51.2