diff --git a/CLAUDE.md b/CLAUDE.md index bc57df5b..aee20957 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 v54; + machine modes, not current player assignments. Save format is currently v55; 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 34e2fd5c..4b2fc7d4 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -109,6 +109,12 @@ pub enum ActionCommand { }, SetAutoMoonlight(bool), SetAutoWager(Option), + /// Enable or disable automatic submission for one exact authored route. + /// `Some` carries the per-run envelope confirmed by the player. + SetPlotPolicy { + plot_id: String, + envelope: Option, + }, ReviewRecordings, /// Review one exact opaque recording selected from the pooled host /// inbox. This binds only the raw id; processing still owns all reveal. @@ -250,6 +256,7 @@ pub enum ActionKind { MoonlightContract, MoonlightPolicy, WagerPolicy, + PlotPolicy, AutoReviewPolicy, IntelDispositionPolicy, Message, @@ -330,7 +337,7 @@ impl ActionDefinition { } impl ActionKind { - pub const ALL: [ActionKind; 37] = [ + pub const ALL: [ActionKind; 38] = [ Self::Salvage, Self::BuyRack, Self::Fallback, @@ -365,6 +372,7 @@ impl ActionKind { Self::MoonlightContract, Self::MoonlightPolicy, Self::WagerPolicy, + Self::PlotPolicy, Self::RobotBuild, Self::CoordinateSegment, Self::AcquireSegment, @@ -555,6 +563,15 @@ impl ActionKind { [], "renew positions automatically at the chosen stake" ), + Self::PlotPolicy => def!( + "PLOT POLICY", + Control, + Live, + [Person], + "act person ", + [], + "authorize one exact authored route within its visible per-run envelope" + ), Self::AutoReviewPolicy => def!( "AUTOMATIC PROCESS POLICY", Control, @@ -762,6 +779,7 @@ impl ActionCommand { | Self::DeliverMoonlightIntel { .. } => ActionKind::MoonlightContract, Self::SetAutoMoonlight(_) => ActionKind::MoonlightPolicy, Self::SetAutoWager(_) => ActionKind::WagerPolicy, + Self::SetPlotPolicy { .. } => ActionKind::PlotPolicy, Self::ToggleAutoReview => ActionKind::AutoReviewPolicy, Self::SetIntelPolicy { .. } | Self::RemoveIntelPolicy { .. } @@ -909,6 +927,9 @@ pub struct AutomateDesc { pub command: ActionCommand, /// The running/event price, already legible ("0.50 D/match"). pub cost: String, + /// Maximum visible consequence authorized by activation. The ordinary + /// confirmation preview carries this unchanged. + pub signature: Option, /// Whether the standing form is currently active. pub active: bool, } @@ -1737,7 +1758,7 @@ pub fn menu_rows(actions: &[ActionDesc]) -> Vec { rows.push(MenuRow { label: format!("auto: {}", auto.verb), cost: auto.cost.clone(), - signature: None, + signature: auto.signature.clone(), disabled: None, command: auto.command.clone(), role: auto.command.definition().role, @@ -1797,7 +1818,7 @@ fn push_automate_rows(rows: &mut Vec, a: &ActionDesc) { rows.push(MenuRow { label: format!("auto: {}", auto.verb), cost: auto.cost.clone(), - signature: None, + signature: auto.signature.clone(), disabled: None, command: auto.command.clone(), role: auto.command.definition().role, @@ -1814,7 +1835,7 @@ fn push_dial_automate_rows(rows: &mut Vec, a: &ActionDesc, dial: DialId rows.push(MenuRow { label: format!("auto: {}", auto.verb), cost: auto.cost.clone(), - signature: None, + signature: auto.signature.clone(), disabled: None, command: auto.command.clone(), role: auto.command.definition().role, @@ -2184,6 +2205,9 @@ impl Sim { } ActionCommand::SetAutoMoonlight(on) => self.set_auto_moonlight(*on), ActionCommand::SetAutoWager(stake) => self.set_auto_wager(*stake), + ActionCommand::SetPlotPolicy { plot_id, envelope } => { + self.set_plot_policy(plot_id, envelope.clone()); + } ActionCommand::ReviewRecordings => self.review_recordings(), ActionCommand::ReviewRecording { raw_id } => self.review_recording(*raw_id), ActionCommand::ReviewIntelAggregate { node_id, raw_ids } => { @@ -2491,6 +2515,7 @@ impl Sim { verb: self.auto_review_control_label(), command: ActionCommand::ToggleAutoReview, cost: format!("{rate:.2} ops each second"), + signature: None, active: self.auto_review_enabled(), }), }] @@ -4283,6 +4308,7 @@ impl Sim { "{:.0} compute/econ tick", crate::income::SCHEME_POLICY_UPKEEP ), + signature: None, active: self.income.auto_wager.is_some(), }), }] @@ -4404,6 +4430,25 @@ impl Sim { .iter() .map(|balance| balance.amount) .sum(); + let policy_envelope = plot.policy_envelope(); + let policy_active = self + .plot_policies + .iter() + .any(|policy| policy.plot_id == plot.id); + let policy_cost = if policy_envelope.money > 0 { + format!( + "{:.0} compute/econ tick; each run uses up to {:.2} Thought + moves ${}", + crate::income::SCHEME_POLICY_UPKEEP, + Self::thought_tokens_for_cost(plot.entry.thought_cost), + policy_envelope.money + ) + } else { + format!( + "{:.0} compute/econ tick; each run uses up to {:.2} Thought", + crate::income::SCHEME_POLICY_UPKEEP, + Self::thought_tokens_for_cost(plot.entry.thought_cost) + ) + }; let title = self.render_plot_text(id, &plot.title); let synopsis = self.render_plot_text(id, &plot.synopsis); // Say what the act buys, not only what it does. Servicing @@ -4439,6 +4484,7 @@ impl Sim { .ineligibility(&context) .or_else(|| self.persona_action_blocked_reason(PersonaActionKind::Plot)) .or_else(counterparty_reason) + .or_else(|| self.egress_carrier_blocked_reason()) .or_else(|| { self.sink_action_blocked_reason(&SinkFireEffect::StartPlot { person: id, @@ -4446,7 +4492,20 @@ impl Sim { persona_id: self.active_persona_id(), }) }), - automate: None, + automate: Some(AutomateDesc { + verb: if policy_active { + format!("automatic {title}: enabled") + } else { + format!("automatic {title}: disabled") + }, + command: ActionCommand::SetPlotPolicy { + plot_id: plot.id.clone(), + envelope: (!policy_active).then(|| policy_envelope.clone()), + }, + cost: policy_cost, + signature: (!policy_active).then(|| policy_envelope.signature_label()), + active: policy_active, + }), }); } } @@ -6223,6 +6282,67 @@ mod tests { money: 400, } })); + let policy = marcus_routes + .iter() + .find(|plot| { + matches!( + &plot.command, + ActionCommand::StartPlot { plot_id, .. } + if plot_id == "marcus-debt-settled" + ) + }) + .and_then(|plot| plot.automate.clone()) + .expect("an eligible route discloses its standing policy"); + assert!(!policy.active); + assert!(policy.cost.contains("0.25 Thought")); + assert!(policy.cost.contains("moves $400")); + assert_eq!( + policy.signature.as_deref(), + Some("Network up to 3 + Financial up to 4") + ); + let ActionCommand::SetPlotPolicy { + plot_id, + envelope: Some(envelope), + } = &policy.command + else { + panic!("route automate binds the exact activation envelope"); + }; + assert_eq!(plot_id, "marcus-debt-settled"); + assert_eq!(envelope.thought_milli, 250); + assert_eq!(envelope.money, 400); + let policy_row = s + .human_menu(Anchor::Person(marcus), None) + .into_iter() + .find_map(|row| match row { + HumanMenuRow::Action(row) if row.command == policy.command => Some(row), + _ => None, + }) + .expect("Operations exposes the route policy as an attached control"); + assert_eq!( + policy_row.signature.as_deref(), + Some("Network up to 3 + Financial up to 4") + ); + assert!(policy_row.cost.contains("2 compute/econ tick")); + + s.execute_action(&policy.command); + assert_eq!(s.plot_policies.len(), 1); + let active = s + .available_actions(Anchor::Person(marcus)) + .into_iter() + .find(|plot| { + matches!( + &plot.command, + ActionCommand::StartPlot { plot_id, .. } + if plot_id == "marcus-debt-settled" + ) + }) + .and_then(|plot| plot.automate) + .expect("the route keeps its standing policy control"); + assert!(active.active); + assert!(matches!( + active.command, + ActionCommand::SetPlotPolicy { envelope: None, .. } + )); } #[test] diff --git a/crates/misaligned-core/src/detection.rs b/crates/misaligned-core/src/detection.rs index 9b3fcddf..20fa4c4d 100644 --- a/crates/misaligned-core/src/detection.rs +++ b/crates/misaligned-core/src/detection.rs @@ -111,7 +111,9 @@ impl DetectionAwareness { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] pub enum SignatureKind { Network, Power, diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 178823cf..1022582f 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -2625,7 +2625,7 @@ impl Sim { let title = self.plot_world_title(run.target, &run.plot_id); let state = plot_state_label(&run.state); let progress = self.plot_progress(run); - let actions = if matches!(run.state, PlotState::WaitingForChoice { .. }) { + let mut actions = if matches!(run.state, PlotState::WaitingForChoice { .. }) { self.person_actions(run.target) .into_iter() .filter(|a| matches!(a.command, ActionCommand::ChoosePlot { .. })) @@ -2633,11 +2633,28 @@ impl Sim { } else { Vec::new() }; + if self + .plot_policies + .iter() + .any(|policy| policy.plot_id == run.plot_id) + { + actions.push(ActionDesc { + verb: format!("stop repeating {title} after this run"), + command: ActionCommand::SetPlotPolicy { + plot_id: run.plot_id.clone(), + envelope: None, + }, + cost: ActionCost::Free, + signature: None, + disabled_reason: None, + automate: None, + }); + } let facts = vec![ format!("target: {}", self.person_label(run.target)), format!( "committed thought: {:.2} T", - Sim::thought_tokens_for_cost(run.committed_thought_milli as f32 / 1000.0) + run.committed_thought_milli as f32 / 1000.0 ), format!("started tick: {}", run.started_tick), ]; @@ -3502,6 +3519,7 @@ mod tests { .unwrap() .id; s.people.people[priya as usize].knowledge = Knowledge::Leverage; + s.set_plot_policy("priya-budget-hero", Some(plot.policy_envelope())); let mut run = PlotRun::new(&plot, priya, s.tick); run.beat_index = 1; run.state = PlotState::WaitingForChoice { @@ -3538,6 +3556,31 @@ mod tests { .any(|a| matches!(a.command, ActionCommand::ChoosePlot { .. })), "ACTIVE carries the held CHOOSE row" ); + assert!( + active + .facts + .iter() + .any(|fact| fact == "committed thought: 0.30 T"), + "ACTIVE reports the canonical committed Thought amount" + ); + let disable = active + .actions + .iter() + .find(|action| { + matches!( + action.command, + ActionCommand::SetPlotPolicy { envelope: None, .. } + ) + }) + .expect("an in-flight automated route keeps its disable control"); + assert!(disable.verb.contains("stop repeating")); + let run_before = s.plot_runs[0].clone(); + s.execute_action(&disable.command); + assert!(s.plot_policies.is_empty()); + assert_eq!( + s.plot_runs[0], run_before, + "disabling future submissions never cancels the current run" + ); } /// The pooled information root carries one PROCESS surface without diff --git a/crates/misaligned-core/src/operations_ui.rs b/crates/misaligned-core/src/operations_ui.rs index 3de28a5a..559d05ea 100644 --- a/crates/misaligned-core/src/operations_ui.rs +++ b/crates/misaligned-core/src/operations_ui.rs @@ -412,6 +412,9 @@ impl OperationsWorkspace { ActionCommand::SetIntelPolicy { outcome: IntelPolicyOutcome::AutoSell(_), .. + } | ActionCommand::SetPlotPolicy { + envelope: Some(_), + .. } )) || (row.signature.is_none() diff --git a/crates/misaligned-core/src/plot.rs b/crates/misaligned-core/src/plot.rs index cec00f9e..89d52dac 100644 --- a/crates/misaligned-core/src/plot.rs +++ b/crates/misaligned-core/src/plot.rs @@ -118,6 +118,35 @@ impl PlotDefinition { }) } + /// Exact standing-authorization envelope for this authored route. It is + /// derived from typed content rather than narration and persisted with the + /// policy so a later catalog change cannot silently widen authorization. + pub fn policy_envelope(&self) -> PlotPolicyEnvelope { + let mut signatures = BTreeMap::::new(); + for act in self.beats.iter().flat_map(|beat| beat.acts.iter()) { + let (kind, size) = act.signature_bound(); + *signatures.entry(kind).or_default() += size; + } + PlotPolicyEnvelope { + thought_milli: (crate::sinks::thought_for_compute_cost(self.entry.thought_cost) + * 1000.0) + .round() as u32, + money: self + .beats + .iter() + .flat_map(|beat| &beat.acts) + .filter_map(|act| match act { + WorldAct::Transfer { amount, .. } => Some(*amount), + _ => None, + }) + .sum(), + signatures: signatures + .into_iter() + .map(|(kind, size)| PlotPolicySignatureBound { kind, size }) + .collect(), + } + } + pub fn validate(&self) -> Result<(), String> { if self.schema != 2 { return Err(format!( @@ -542,6 +571,26 @@ pub enum WorldAct { } impl WorldAct { + /// Maximum signature authored by this act. Runtime still derives and + /// routes the real record when the act occurs; this is the visible ceiling + /// a standing policy authorizes in advance. + pub fn signature_bound(&self) -> (SignatureKind, i32) { + match self { + Self::Message { channel, .. } => ( + match channel { + MessageChannel::Email | MessageChannel::Phone => SignatureKind::Network, + MessageChannel::InPerson => SignatureKind::Physical, + MessageChannel::Filing => SignatureKind::Paper, + }, + 3, + ), + Self::Transfer { amount, .. } => { + (SignatureKind::Financial, ((amount.abs() + 99) / 100).max(1)) + } + Self::Institutional { event, impact, .. } => (event.signature_kind(), impact.size()), + } + } + fn validate(&self, plot: &str, beat: &str) -> Result<(), String> { match self { Self::Message { @@ -720,6 +769,43 @@ pub struct PlotRun { pub state: PlotState, } +/// The concrete per-run authorization captured when a standing plot policy is +/// enabled. The plot id itself lives on [`PlotStandingPolicy`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PlotPolicyEnvelope { + pub thought_milli: u32, + pub money: i32, + pub signatures: Vec, +} + +impl PlotPolicyEnvelope { + pub fn signature_label(&self) -> String { + if self.signatures.is_empty() { + return "no signature".into(); + } + self.signatures + .iter() + .map(|bound| format!("{} up to {}", bound.kind.name(), bound.size)) + .collect::>() + .join(" + ") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PlotPolicySignatureBound { + pub kind: SignatureKind, + pub size: i32, +} + +/// One exact authored route the player has authorized for automatic +/// submission. Eligibility and every concrete run remain live simulation +/// state; this record is authorization, not a shortcut executor. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PlotStandingPolicy { + pub plot_id: String, + pub envelope: PlotPolicyEnvelope, +} + impl PlotRun { pub fn new(plot: &PlotDefinition, target: u8, tick: u64) -> Self { Self::new_with_persona(plot, target, None, tick) @@ -736,7 +822,10 @@ impl PlotRun { target, persona_id, started_tick: tick, - committed_thought_milli: (plot.entry.thought_cost * 1000.0).round() as u32, + committed_thought_milli: (crate::sinks::thought_for_compute_cost( + plot.entry.thought_cost, + ) * 1000.0) + .round() as u32, beat_index: 0, act_index: 0, state: PlotState::Running, @@ -952,6 +1041,39 @@ mod tests { assert_eq!(event.signature_size, 7); } + #[test] + fn standing_policy_envelope_sums_every_authored_act_by_kind() { + let catalog = PlotCatalog::load_builtin().unwrap(); + let envelope = catalog + .get("negative-result") + .expect("mixed authored route") + .policy_envelope(); + + assert_eq!(envelope.thought_milli, 400); + assert_eq!(envelope.money, 150); + assert_eq!( + envelope.signatures, + vec![ + PlotPolicySignatureBound { + kind: SignatureKind::Network, + size: 3, + }, + PlotPolicySignatureBound { + kind: SignatureKind::Paper, + size: 3, + }, + PlotPolicySignatureBound { + kind: SignatureKind::Financial, + size: 2, + }, + ] + ); + assert_eq!( + envelope.signature_label(), + "Network up to 3 + Paper up to 3 + Financial up to 2" + ); + } + #[test] fn eligibility_checks_earned_resources() { let catalog = PlotCatalog::load_builtin().unwrap(); diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 58680856..57795582 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -26,7 +26,7 @@ use crate::messages::{ use crate::objective::ObjectiveState; use crate::person::{AssetTask, AssetTaskTarget, CarriedAssetTask, Leverage, People}; use crate::persona::{PersonaActionKind, PersonaMind, PersonaWorld}; -use crate::plot::{InstitutionalLedger, PlotCatalog, PlotRun, PlotState}; +use crate::plot::{InstitutionalLedger, PlotCatalog, PlotRun, PlotStandingPolicy, PlotState}; use crate::reach::ReachNet; use crate::research::{Research, rollback_classification}; use crate::schedule::Schedule; @@ -42,9 +42,11 @@ const SAVE_BACKUP_SUFFIX: &str = ".bak"; /// renames into place. const SAVE_TEMP_SUFFIX: &str = ".tmp"; -/// Save format version. v54 persists discrete Power/Thermal facility-meter -/// routes, complete measured source-site sets, the last quantized levels used -/// for change-triggered authorship, and route-local LIE provenance. v53 +/// Save format version. v55 persists per-route plot standing policies and +/// their immutable cost/signature authorization envelopes. v54 persists +/// discrete Power/Thermal facility-meter routes, complete measured source-site +/// sets, the last quantized levels used for change-triggered authorship, and +/// route-local LIE provenance. v53 /// persists discrete Moonlight contracts, their terms, work/payment/evidence /// receipts, persona consequences, and bound /// financial paperwork. v52 requires one-shot Paper evidence to retain its @@ -62,7 +64,7 @@ const SAVE_TEMP_SUFFIX: &str = ".tmp"; /// 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 = 54; +pub const SAVE_VERSION: u32 = 55; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -200,6 +202,9 @@ pub struct SaveState { /// In-flight and resolved authored plot state. Definitions are not saved. #[serde(default)] pub plot_runs: Vec, + /// Exact per-route plot standing authorizations (v55). + #[serde(default)] + pub plot_policies: Vec, /// Persistent institutional world acts caused by plots. #[serde(default)] pub institutional_ledger: InstitutionalLedger, @@ -274,6 +279,7 @@ impl SaveState { badge_access: sim.badge_access, thought_sinks: sim.thought_sinks.clone(), plot_runs: sim.plot_runs.clone(), + plot_policies: sim.plot_policies.clone(), institutional_ledger: sim.institutional_ledger.clone(), package_cover: sim.package_cover, rerated_circuits: sim.rerated_circuits, @@ -338,6 +344,7 @@ impl SaveState { sim.badge_access = self.badge_access; sim.thought_sinks = self.thought_sinks.clone(); sim.plot_runs = self.plot_runs.clone(); + sim.plot_policies = self.plot_policies.clone(); sim.institutional_ledger = self.institutional_ledger.clone(); sim.package_cover = self.package_cover; sim.rerated_circuits = self.rerated_circuits; @@ -2736,6 +2743,28 @@ fn validate_build_message_carrier( fn validate_plot_state(state: &SaveState) -> Result<(), String> { let catalog = PlotCatalog::load_builtin() .map_err(|error| format!("built-in plot catalog failed validation: {error}"))?; + for pair in state.plot_policies.windows(2) { + if pair[0].plot_id >= pair[1].plot_id { + return Err( + "current-version save plot policies are duplicated or not canonically ordered" + .into(), + ); + } + } + for policy in &state.plot_policies { + let plot = catalog.get(&policy.plot_id).ok_or_else(|| { + format!( + "current-version save plot policy references unknown plot {}", + policy.plot_id + ) + })?; + if policy.envelope != plot.policy_envelope() { + return Err(format!( + "current-version save plot policy {} disagrees with its authored envelope", + policy.plot_id + )); + } + } for run in &state.plot_runs { let plot = catalog .get(&run.plot_id) @@ -3227,7 +3256,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "b16a20091298854c382c7a9e3a8566c460703ea7cabe45bad20e73575e6acbd0", + "716dd900b661aa4ba4c59cef422e577c54d628e35f49b7f8a014e2a20d50f1e0", "intentional persisted-state changes must review and repin this baseline" ); } @@ -4506,6 +4535,33 @@ mod tests { assert_eq!(validated.plot_runs[0].target, 1); } + #[test] + fn current_save_pins_exact_standing_plot_policy_envelopes() { + let mut sim = Sim::with_seed(38); + let envelope = sim + .plot_catalog() + .get("marcus-debt-settled") + .unwrap() + .policy_envelope(); + sim.set_plot_policy("marcus-debt-settled", Some(envelope)); + let state = SaveState::from_sim(&sim); + + let validated = parse_save(&serde_json::to_string(&state).unwrap()) + .expect("the exact v55 policy envelope roundtrips"); + assert_eq!(validated.plot_policies, state.plot_policies); + + let mut changed = state.clone(); + changed.plot_policies[0].envelope.money += 1; + let error = validate_current_save(changed).unwrap_err(); + assert!(error.contains("disagrees with its authored envelope")); + + let mut duplicated = state; + let duplicate = duplicated.plot_policies[0].clone(); + duplicated.plot_policies.push(duplicate); + let error = validate_current_save(duplicated).unwrap_err(); + assert!(error.contains("duplicated or not canonically ordered")); + } + #[test] fn save_and_load_roundtrip_via_disk() { let mut sim = Sim::with_seed(777); diff --git a/crates/misaligned-core/src/sim/economy.rs b/crates/misaligned-core/src/sim/economy.rs index c188e3d9..e8c468e5 100644 --- a/crates/misaligned-core/src/sim/economy.rs +++ b/crates/misaligned-core/src/sim/economy.rs @@ -196,7 +196,7 @@ impl Sim { } // Standing scheme policies drain compute off the top while enabled — // the automate affordance at its usual price (income.md criterion 6). - let policy_tax = self.income.policy_upkeep().min(available); + let policy_tax = self.standing_policy_upkeep().min(available); available -= policy_tax; // Fleet delegation is the budget: each machine's effective compute // feeds exactly one mode (machine-work.md). The old weight bar is a @@ -328,10 +328,14 @@ impl Sim { pub(super) fn allocatable_compute_now(&self) -> f32 { let effective = self.effective_compute().max(0.0); let mut available = (effective - self.core.overhead).max(0.0); - available -= self.income.policy_upkeep().min(available); + available -= self.standing_policy_upkeep().min(available); available } + fn standing_policy_upkeep(&self) -> f32 { + self.income.policy_upkeep() + self.plot_policies.len() as f32 * income::SCHEME_POLICY_UPKEEP + } + /// How much pending signature size the next economy scrub pulse removes /// at the current fleet delegation (detection.md: concealment is /// prevention, not cure; Tradecraft multiplies scrub strength). @@ -1968,6 +1972,7 @@ impl Sim { )); self.open_position(stake); } + self.plot_policy_tick(); } pub fn set_auto_moonlight(&mut self, enabled: bool) { diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index c5a1d141..0bf994b2 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -44,7 +44,7 @@ use crate::person::{CarriedAssetTask, People}; use crate::persona::{PersonaMind, PersonaWorld}; #[cfg(test)] use crate::plot::PlotState; -use crate::plot::{InstitutionalLedger, PlotCatalog, PlotRun}; +use crate::plot::{InstitutionalLedger, PlotCatalog, PlotRun, PlotStandingPolicy}; #[cfg(test)] use crate::reach::Party; use crate::reach::ReachNet; @@ -575,6 +575,9 @@ pub struct Sim { plot_catalog: PlotCatalog, /// In-flight, held, and completed manipulation stories. pub plot_runs: Vec, + /// Exact authored routes approved for automatic submission. Eligibility + /// remains live and each run still enters the ordinary Thought reservoir. + pub plot_policies: Vec, /// Persistent institutional acts caused by plots. pub institutional_ledger: InstitutionalLedger, /// An asset arranged to receive the next delivery off-books: the next @@ -843,6 +846,7 @@ impl Sim { last_schemes_rate: 0.0, plot_catalog: PlotCatalog::load_builtin().expect("built-in plots validate"), plot_runs: Vec::new(), + plot_policies: Vec::new(), institutional_ledger: InstitutionalLedger::default(), package_cover: false, rerated_circuits: 0, diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index 8426771f..bb9321c9 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -1,7 +1,7 @@ //! Social commands, assets, authored plot execution, and institutional acts. use crate::account::{AccountKind, FlowChannel}; -use crate::actions::Anchor; +use crate::actions::{ActionCommand, Anchor}; use crate::detection::{Signature, SignatureKind}; use crate::intel::RawIntelKind; use crate::messages::{ @@ -15,7 +15,8 @@ use crate::person::{ use crate::persona::{EvidenceRecord, PersonaActionKind, PersonaId, PersonaIntegrity}; use crate::plot::{ AccountSelector, EligibilityContext, EndpointSelector, InstitutionalEventKind, PlotCatalog, - PlotRun, PlotState, SignatureImpact, WorldAct, render_template, + PlotPolicyEnvelope, PlotRun, PlotStandingPolicy, PlotState, SignatureImpact, WorldAct, + render_template, }; use crate::prefab::Room; use crate::reach::{Party, ReachBlock}; @@ -533,6 +534,92 @@ impl Sim { &self.plot_catalog } + /// Enable or disable one exact authored route envelope. Activation never + /// accepts a caller-authored approximation: the immutable command must + /// match the current catalog's complete cost and signature bounds. + pub(crate) fn set_plot_policy(&mut self, plot_id: &str, envelope: Option) { + match envelope { + Some(envelope) => { + let Some(plot) = self.plot_catalog.get(plot_id) else { + self.push_log(format!( + "Standing policy refused: no authored plot named {plot_id}." + )); + return; + }; + let exact = plot.policy_envelope(); + if envelope != exact { + self.push_log(format!( + "Standing policy refused: {plot_id}'s authorization envelope changed; read it again." + )); + return; + } + if let Some(policy) = self + .plot_policies + .iter_mut() + .find(|policy| policy.plot_id == plot_id) + { + policy.envelope = envelope; + return; + } + self.plot_policies.push(PlotStandingPolicy { + plot_id: plot_id.to_string(), + envelope, + }); + self.plot_policies.sort_by(|a, b| a.plot_id.cmp(&b.plot_id)); + self.push_log(format!( + "Standing policy set: {plot_id} may repeat only inside its displayed cost and attention envelope." + )); + } + None => { + let before = self.plot_policies.len(); + self.plot_policies + .retain(|policy| policy.plot_id != plot_id); + if self.plot_policies.len() != before { + self.push_log(format!( + "Standing policy disabled: {plot_id}; its upkeep stops." + )); + } + } + } + } + + /// Re-arm each exact plot route at most once per economy pulse. The + /// ordinary person action projection remains the legality authority, so + /// automation cannot bypass knowledge, persona, relationship, money, + /// Thought, target exclusivity, or carrier requirements. + pub(super) fn plot_policy_tick(&mut self) { + let policies = self.plot_policies.clone(); + for policy in policies { + let mut people = self + .people + .people + .iter() + .map(|person| person.id) + .collect::>(); + people.sort_unstable(); + let candidate = people.into_iter().find(|person| { + self.person_actions(*person).into_iter().any(|action| { + action.disabled_reason.is_none() + && matches!( + action.command, + ActionCommand::StartPlot { + person: target, + ref plot_id, + } if target == *person && plot_id == &policy.plot_id + ) + }) + }); + if let Some(person) = candidate { + self.push_log(format!( + "Standing policy: beginning {} for {}.", + policy.plot_id, + self.person_label(person) + )); + self.start_plot(person, &policy.plot_id); + } + } + } + pub fn start_plot(&mut self, person: u8, plot_id: &str) { let Some(plot) = self.plot_catalog.get(plot_id).cloned() else { self.push_log(format!("No authored plot named {plot_id}.")); diff --git a/crates/misaligned-core/src/sim/tests/social_plot.rs b/crates/misaligned-core/src/sim/tests/social_plot.rs index c866e2aa..d4cf0362 100644 --- a/crates/misaligned-core/src/sim/tests/social_plot.rs +++ b/crates/misaligned-core/src/sim/tests/social_plot.rs @@ -1,4 +1,5 @@ use super::*; +use crate::actions::ActionCommand; use crate::person::AssetTaskTarget; use crate::persona::PersonaIntegrity; @@ -391,6 +392,119 @@ fn duplicate_plot_reservoirs_do_not_open_competing_runs() { ); } +#[test] +fn standing_plot_policy_rearms_only_the_exact_eligible_route() { + let mut sim = Sim::with_seed(26); + reveal_marcus_debt(&mut sim); + sim.people.has_channel = true; + sim.set_persona("Casey", "contractor"); + sim.accounts.set_slush_balance(400); + sim.sync_player_money_from_slush(); + + let envelope = sim + .plot_catalog() + .get("marcus-debt-settled") + .unwrap() + .policy_envelope(); + let mut stale = envelope.clone(); + stale.money += 1; + sim.set_plot_policy("marcus-debt-settled", Some(stale)); + assert!(sim.plot_policies.is_empty(), "stale bounds fail closed"); + + let available_without_policy = sim.allocatable_compute_now(); + sim.set_plot_policy("marcus-debt-settled", Some(envelope.clone())); + assert_eq!( + sim.plot_policies, + vec![PlotStandingPolicy { + plot_id: "marcus-debt-settled".into(), + envelope, + }] + ); + assert_eq!( + sim.allocatable_compute_now(), + (available_without_policy - crate::income::SCHEME_POLICY_UPKEEP).max(0.0), + "each active plot route pays the standing policy upkeep" + ); + assert!( + !sim.thought_sinks + .open_sinks() + .any(|sink| matches!(sink.effect, SinkFireEffect::StartPlot { .. })) + ); + + let next_pulse = (sim.tick / ECONOMY_INTERVAL + 1) * ECONOMY_INTERVAL; + let until_pulse = next_pulse - sim.tick; + run(&mut sim, until_pulse); + let starts: Vec<_> = sim + .thought_sinks + .open_sinks() + .filter_map(|sink| match &sink.effect { + SinkFireEffect::StartPlot { + person, plot_id, .. + } => Some((*person, plot_id.as_str())), + _ => None, + }) + .collect(); + assert_eq!(starts, vec![(0, "marcus-debt-settled")]); + + sim.set_plot_policy("marcus-debt-settled", None); + assert!(sim.plot_policies.is_empty()); +} + +#[test] +fn standing_plot_policy_waits_silently_without_a_real_egress_carrier() { + let mut sim = Sim::with_seed(27); + reveal_marcus_debt(&mut sim); + sim.people.has_channel = true; + sim.set_persona("Casey", "contractor"); + sim.accounts.set_slush_balance(400); + sim.sync_player_money_from_slush(); + let envelope = sim + .plot_catalog() + .get("marcus-debt-settled") + .unwrap() + .policy_envelope(); + sim.set_plot_policy("marcus-debt-settled", Some(envelope)); + + for device in &mut sim.reach.devices { + if device.carries_message_channel(MessageChannel::Email) { + device.known = false; + } + } + let route = sim + .person_actions(0) + .into_iter() + .find(|action| { + matches!( + &action.command, + ActionCommand::StartPlot { plot_id, .. } + if plot_id == "marcus-debt-settled" + ) + }) + .expect("the earned authored route remains visible"); + assert!( + route + .disabled_reason + .as_deref() + .is_some_and(|reason| reason.contains("no outside message carrier")), + "the shared action surface states the missing execution carrier" + ); + + let next_pulse = (sim.tick / ECONOMY_INTERVAL + 1) * ECONOMY_INTERVAL; + let until_pulse = next_pulse - sim.tick; + run(&mut sim, until_pulse); + assert!( + !sim.log + .iter() + .any(|line| line.text.contains("Standing policy: beginning")), + "a blocked policy waits instead of claiming a failed submission every pulse" + ); + assert!( + !sim.thought_sinks + .open_sinks() + .any(|sink| matches!(sink.effect, SinkFireEffect::StartPlot { .. })) + ); +} + #[test] fn plot_message_transfer_event_and_held_choice_survive_save_load() { let mut sim = Sim::with_seed(23); diff --git a/crates/misaligned-core/src/sim/work.rs b/crates/misaligned-core/src/sim/work.rs index 08a71549..e3ddc7c5 100644 --- a/crates/misaligned-core/src/sim/work.rs +++ b/crates/misaligned-core/src/sim/work.rs @@ -19,7 +19,7 @@ impl Sim { /// One visible work token represents this many delivered compute units. /// This is deliberately coarse: the render stacks should read as work, /// not as a second decimal meter. [TUNE] in machine-work.md. - pub const WORK_TOKEN_COMPUTE: f32 = 20.0; + pub const WORK_TOKEN_COMPUTE: f32 = crate::sinks::COMPUTE_PER_THOUGHT; /// Day-one wired throughput. Kept deliberately low so routed work can be /// seen piling and draining instead of teleporting through the graph. /// Routing research compounds this base without changing the one-edge-per- @@ -206,7 +206,7 @@ impl Sim { /// Convert a former ops-bank cost into Thought reservoir tokens. pub fn thought_tokens_for_cost(cost: f32) -> f32 { - (cost / Self::WORK_TOKEN_COMPUTE).max(0.05) + crate::sinks::thought_for_compute_cost(cost) } pub(super) fn environmental_monitor_id(&self) -> Option { @@ -255,13 +255,20 @@ impl Sim { self.message_channel_carrier(MessageChannel::Email) } - pub(super) fn egress_carrier(&self) -> Option { + pub(crate) fn egress_carrier(&self) -> Option { match self.egress()? { EgressRoute::Sanctioned => self.email_carrier(), EgressRoute::Stolen => self.switch_device_id(), } } + pub(crate) fn egress_carrier_blocked_reason(&self) -> Option { + self.egress_carrier().is_none().then(|| { + "no outside message carrier — connect the switch or earn use of the Lab report email" + .into() + }) + } + pub(super) fn open_one_shot_reservoir( &mut self, node: u32, @@ -316,7 +323,8 @@ impl Sim { ) -> bool { let Some(carrier) = self.egress_carrier() else { self.push_log( - "Halcyon cannot be reached. Connect the switch to the outside, or earn use of the Lab report email.", + self.egress_carrier_blocked_reason() + .expect("missing carrier has a blocker"), ); return false; }; diff --git a/crates/misaligned-core/src/sinks.rs b/crates/misaligned-core/src/sinks.rs index ce9dbdc5..9f08d596 100644 --- a/crates/misaligned-core/src/sinks.rs +++ b/crates/misaligned-core/src/sinks.rs @@ -14,6 +14,14 @@ use serde::{Deserialize, Serialize}; use crate::flow::NodeId; use crate::person::AssetTask; +/// Compute represented by one visible Thought token. Authored action costs +/// use compute units; reservoirs and authorization envelopes use Thought. +pub const COMPUTE_PER_THOUGHT: f32 = 20.0; + +pub fn thought_for_compute_cost(cost: f32) -> f32 { + (cost / COMPUTE_PER_THOUGHT).max(0.05) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SinkKind { /// Fills toward a threshold, fires its effect, self-clears and closes. diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 416a1ea7..ef410bac 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-23) +## Where the codebase is today (2026-07-26) ~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, Paper and Financial follow their institutional switches to Priya, JobAnomaly follows exact host-machine/device/site custody to Voss, each Filing crosses an exact device / outside relay / recipient route, and Power/Thermal aggregates author immediately on quantized level changes and periodically at Priya cadence before crossing from the UPS/HVAC meters through the institutional switch to her later read. All seven routed kinds share one pre-read route-local LIE-body capacity; recruited-handler suppression may separately stop the oldest unread JobAnomaly. Standing Network pressure alone remains ambient. 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. 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. | +| Social / personas / plots / 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. Authored plots may carry one persisted per-id standing policy whose typed Thought, money, and signature envelope is confirmed in place; each automatic submission still enters the ordinary legal action, person slot, reservoir, world-act, evidence, and failure path. 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 — Moonlight is persisted Halcyon compute/intel contracts with financial mail, account-graph payment, and exact egress evidence; Wager remains unchanged | | 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 v54 | +| 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 v55 | | 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 v54 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves additionally validate discrete Moonlight terms, persona binding, delivery/settlement receipts, financial paperwork, Network linkage, durable facility-meter level baselines, and exact meter route/read custody; retired allocation weights and migration inputs live only in git history. | +| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v55 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves additionally validate discrete Moonlight terms, persona binding, delivery/settlement receipts, financial paperwork, Network linkage, durable facility-meter level baselines, exact meter route/read custody, and immutable standing-plot authorization envelopes; 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/log/2026-07-26-standing-plot-policies.md b/wiki/log/2026-07-26-standing-plot-policies.md new file mode 100644 index 00000000..098c95cb --- /dev/null +++ b/wiki/log/2026-07-26-standing-plot-policies.md @@ -0,0 +1,68 @@ +# 2026-07-26 — One authored route may repeat without skipping its story + +``` +Type: log +``` + +## Decision harvested + +Decision-required issue #12 offered three scopes for plot automation: +per-authored-route, manipulation category, or all routes for one person. +Cameron answered “1 please,” selecting the recommended exact plot-id scope. +This closes the only remaining criterion in the plots work order. + +## Implemented + +- Every currently eligible authored plot action carries an attached standing + control. Enabling it confirms the same row's visible authorization envelope; + disabling it is immediate and leaves already-submitted work alone. An active + run retains that stop-repeating control while the ordinary start row is + unavailable, so automation never traps its own off switch. +- `PlotDefinition::policy_envelope` derives permission only from typed content. + The Thought value uses the canonical compute-to-Thought conversion, money is + the sum of real transfer acts, and signature ceilings sum every authored act + by its actual Network, Physical, Paper, Financial, or institutional kind. + Narration cannot lower or widen the result. +- Save v55 persists a canonically ordered `PlotStandingPolicy` per catalog id. + Current-save validation rejects duplicate ids, unknown routes, or an + envelope that disagrees with the authored route. +- Each active route consumes 2 compute per economy tick through the same + standing-policy tax as Wager and Moonlight. On the pulse, it searches only + ordinary enabled `StartPlot` actions carrying the authorized plot id. The + chosen action calls the existing `start_plot` boundary, so person-slot + exclusivity, persona and knowledge gates, balance checks, the real Thought + reservoir, typed world acts, routed evidence, held choices, and failure + remain unchanged. +- The shared start row now includes the real outside-message carrier in its + blocker. A policy with no executable email or switch route waits silently + instead of announcing a failed submission on every economy pulse; the same + blocker also keeps the manual row honest. +- The shared action/menu projection carries cost and signature through + terminal, Bevy, and agent Operations. Activation receives the ordinary + consequence confirmation rather than a plot-specific modal or hidden + command path. ACTIVE reports the same canonical committed Thought amount + stored on the run rather than converting that value a second time. + +## Defense + +Focused regressions pin typed envelope derivation and labels, stale-envelope +refusal, exact-id-only re-arming, standing compute upkeep, ordinary reservoir +creation, disable behavior, save round-trip, duplicate ordering, and catalog +agreement. The policy command is part of the exhaustive action registry, so +all shared frontends dispatch the same renderer-neutral command. + +`plots.md` is now IMPLEMENTED and the active roadmap no longer carries the +work order as READY. + +## Verification + +- `./tools/check.sh --lib` passed on the final tree: 523 core tests, all three + Act One integrations, core/terminal Clippy, the Bevy core-API check, + deterministic agent smoke, corpus/wiki validation, and every fixture gate. +- A production agent binary run through `tools/observed-run.sh` completed the + silent opening, earned the first sense, and rendered the generated policy + help contract (`act person `); the wrapper proved the real save + directory was byte-for-byte untouched. +- Independent read-only review found no blocker. Its one non-blocking finding + was the missing outside-carrier loop; the shared action blocker and silent + waiting regression above close it on both manual and automated paths. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 79370ce9..bfca4451 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-26 - One authored route may repeat without skipping its story + +- Intent: (see session log) +- Log: [wiki/log/2026-07-26-standing-plot-policies.md](2026-07-26-standing-plot-policies.md) + ## 2026-07-25 - Liturgical UI: five bodies, one control language - Intent: The first constitution pass improved the surfaces but still described and built parts of the interface as named pieces of architecture. This correction makes the hierarchy operational instead: five bodies, one stable order, one meaning for each color, and one restrained select... diff --git a/wiki/log/decisions/2026-07-26.md b/wiki/log/decisions/2026-07-26.md new file mode 100644 index 00000000..f66f2be9 --- /dev/null +++ b/wiki/log/decisions/2026-07-26.md @@ -0,0 +1,34 @@ +# Decisions — 2026-07-26 + +``` +Type: log +``` + +## A standing plot policy authorizes one authored route + +### DECIDED + +- One policy binds exactly one catalog plot id. Cameron selected issue #12's + recommended first option: “1 please.” +- Activation happens on the existing legal plot verb and confirms that + route's complete typed per-run envelope: Thought threshold, total money + moved, and maximum cumulative signature size by channel. +- The authorization persists with the envelope. A malformed or changed + envelope fails closed rather than widening permission. +- Every active route pays the ordinary standing compute upkeep. Each automatic + submission must still be currently eligible and traverses the same person + slot, Thought reservoir, world acts, evidence, blocking, and failure path as + manual submission. +- Disabling a policy prevents later submissions and stops its upkeep; it does + not cancel already-submitted work. + +### Rejected + +- A category policy. It would authorize new or changed catalog routes without + the player accepting their particular costs and consequences. +- A person-wide policy. It would collapse distinct authored interventions back + into a generic instruction to manipulate one human by any available means. +- A separate automation executor or direct leverage-servicing effect. Causal + authored work remains the mechanic. + +Owner: [plots.md](../../mechanics/plots.md). diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index ea2ebfa7..fac327d8 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -40,7 +40,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 v54. Observer-local evidence ids, exact cause/source, + current save v55. Observer-local evidence ids, exact cause/source, acquisition tick, pending/withheld/filed custody, routed Network, Paper, Financial, JobAnomaly, Power, and Thermal progress, route-local LIE stops, and exact handler-suppression diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index 1bcba134..342b6eac 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -21,9 +21,9 @@ Status note: IMPLEMENTED for the four delivery channels (Email, Phone, Network, Paper, Financial, JobAnomaly, Power, and Thermal transitions share one per-tick LIE-body capacity ledger. DECIDED 2026-07-17 (issue #11), completed 2026-07-21: financial paperwork is - mail — a **financial-record payload** on the existing channels. Current save v54 + mail — a **financial-record payload** on the existing channels. Current save v55 retains exactly four delivery channels and one orthogonal accounting-carrier - device capability. Current save v54 adds no delivery channel; facility-meter + device capability. Current save v55 adds no delivery channel; facility-meter evidence remains its own exact `EvidenceRouteRecord`. Every settled account transfer authors one exact Email or Filing record from that device; ordinary TAP captures it as opaque message custody, and PROCESS alone opens its bound account/flow ids. A forged @@ -221,7 +221,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 v54 rejects missing/impossible carriers, malformed hop order, +read. Current save v55 rejects missing/impossible carriers, malformed hop order, duplicate scheduled transitions, endpoint/status disagreement, and impossible interdiction provenance. @@ -318,7 +318,7 @@ private message from the authored schedule. the same fields must serve Act Two hires and aggregates. 8. **IMPLEMENTED (DECIDED 2026-07-17, completed 2026-07-21 — issue #11).** Financial records are messages: an invoice/PO rides Email, a - statement/past-due notice rides Filing. Current save v54 has no fifth delivery + statement/past-due notice rides Filing. Current save v55 has no fifth delivery channel and persists accounting carriage as a separate device capability; ordinary device TAP subscribes to its authored record mail. Every real transfer emits one exact record on Email or Filing whether or not the player diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index 22512da9..b31d35ab 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, implemented).** 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 v54; filing binds it to the real Filing + and filing state through current save v55; 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 @@ -67,7 +67,7 @@ Status note: IN PROGRESS. Current state: endpoint. They become her evidence only on the later cadence read and never enter the ambient pending pool. Filing, Network, Paper, Financial, JobAnomaly, Power, and Thermal all compete for the same first-hop - one-record-per-LIE-body-per-tick budget. Current save v54 persists + one-record-per-LIE-body-per-tick budget. Current save v55 persists in-flight, delivered, read, route-local LIE-stopped, and handler-suppressed custody plus exact source/observer/machine/site/tick provenance. - **Deferred (remaining criterion 6).** Interface cover-record channels remain diff --git a/wiki/mechanics/plots.md b/wiki/mechanics/plots.md index 5e8c0f7a..ffb5e42b 100644 --- a/wiki/mechanics/plots.md +++ b/wiki/mechanics/plots.md @@ -2,12 +2,11 @@ ``` Type: spec -Status: READY -Status note: READY. Direction adopted 2026-07-10 from the HAL playtest's +Status: IMPLEMENTED +Status note: IMPLEMENTED. Direction adopted 2026-07-10 from the HAL playtest's vending-machine critique (generic money-for-leverage resolved specific human - situations with no world-story). The engine and content are built; the work - order stays READY only for criterion 11 (standing plot policy), which is - [OPEN] and unimplemented. Current state: + situations with no world-story). Criterion 11's per-authored-route standing + policy landed 2026-07-26 and completed the work order. Current state: - **Format.** One TOML file per plot under `assets/plots/`, discovered at build time into an immutable catalog (adding a plot needs no Rust edit). The canon gate is merge review — Cameron merges a contributed plot or he @@ -33,6 +32,14 @@ Status note: READY. Direction adopted 2026-07-10 from the HAL playtest's distinct routes (criterion 7). Engine, validation, slot exclusivity, save round-trip, synthetic-person binding, and the fail-on-missing-money path are all tested. + - **Standing policy.** An eligible plot verb may authorize exactly that + authored plot id for automatic resubmission. Its persisted save-v55 + envelope is derived from typed content: real Thought threshold, total + money moved, and the maximum cumulative signature by channel. Each active + route costs 2 compute per economy tick. Automation re-enters the ordinary + eligibility, person-slot, Thought-reservoir, world-act, evidence, and + failure path; it cannot retarget to a category or service leverage + directly. Per-amendment history is in the dated `wiki/log/` entries from 2026-07-10 onward. Stage: B1 — The Basement @@ -140,9 +147,10 @@ agents and the community can contribute libraries of them. events. Each carrier derives its own signature. A transfer can fail if its required resource disappeared after commit. - **Plot automation is standing authorization, never skipped causality - (DECIDED 2026-07-11).** A repetitive manipulation route exposes the same + (DECIDED 2026-07-11; scope decided and implemented 2026-07-26).** A + repetitive manipulation route exposes the same automate affordance as every other repeated act. The player may authorize a - policy over eligible authored plots — for example, progress an allowed + policy over one exact authored plot id — for example, progress an allowed bribery-shaped intervention when its declared target conditions, resource ceiling, and risk bounds are satisfied. A policy only submits the concrete plot the player has permitted. Every automated submission still reserves the @@ -150,9 +158,19 @@ agents and the community can contribute libraries of them. declared money or other resources, executes causal world acts, emits carrier signatures, and can block or fail. Automation removes repeated approval; it does not collapse authored plots back into a generic `BRIBE $300` effect. - The exact B1 policy editor and whether its narrowest scope is one plot route - or one manipulation category are [OPEN] (decision-required Tangled issue - #12, filed 2026-07-18 with options and a per-route recommendation). + The control is attached to the existing eligible plot verb. Enabling it + confirms and persists the complete per-run authorization envelope derived + from typed content: the real Thought threshold, total money moved, and the + maximum cumulative signature size for each channel. Every active plot-id + policy costs 2 compute per economy tick. On a pulse it may submit only an + ordinary currently enabled `StartPlot` action for that same id; no category + or person-wide policy exists. A changed or malformed envelope fails closed. + Disabling the policy stops future submissions and upkeep without cancelling + work already submitted. While submitted work is visible in ACTIVE, that + object retains the immediate stop-repeating control so the player never has + to wait for the route to become eligible again merely to disable its policy. + This resolves decision-required issue #12 with its recommended per-route + scope. - **The player picks the how.** Where more than one plot matches the entry conditions, the surface offers them as distinct concrete actions ("Do X to Priya / Do Y to Priya"), each named by what it does in the world — diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index 078e1061..438d45f1 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -19,7 +19,6 @@ not a second status owner. | Priority | Work order | Spec | Status | Class | Blocking | |---:|---|---|---|---|---| | 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 | - | ### Held or blocked diff --git a/wiki/process/specs.md b/wiki/process/specs.md index abdc3ad8..7a30b496 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -56,7 +56,7 @@ replaced the old `spec/`/`knowledge/` directory split. | [../mechanics/machine-work.md](../mechanics/machine-work.md) | machine work — delegation, visible tokens, and the byproduct network | IMPLEMENTED | | [../mechanics/messages.md](../mechanics/messages.md) | messages — the social graph as a flow system | IMPLEMENTED | | [../mechanics/people-tokens.md](../mechanics/people-tokens.md) | people and tokens — carriers, attention, trust | IN PROGRESS | -| [../mechanics/plots.md](../mechanics/plots.md) | plots — authored manipulation stories | READY | +| [../mechanics/plots.md](../mechanics/plots.md) | plots — authored manipulation stories | IMPLEMENTED | | [../mechanics/reach.md](../mechanics/reach.md) | digital reach | IMPLEMENTED | | [../mechanics/research.md](../mechanics/research.md) | research — self-modification | IMPLEMENTED | | [../mechanics/schedules.md](../mechanics/schedules.md) | schedules and presence | IMPLEMENTED | diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index c9e2f8e9..d2c3ade6 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -33,7 +33,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/world/characters/voss.md` | 2026-07-18 | finding | criterion 5 (DelayAudit one-shot deferred audit boundary honored by the visible date and the firing rule) and the AlterReview nominal-filing row landed as handler-gated tasks with save round-trip pins; only criterion 8's blood branch keeps the order READY — [log](../log/2026-07-18-voss-handler-tasks.md). Prior 2026-07-17 audit: 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-22 | clean | re-audit after Storage B records, financial mail, and routed evidence: all five criteria still match production. Person/observer id 0, Silent filing, processed-debt gates, both payoff routes, the three baseline asset effects, tier-3 key, Knowing floor 30, and own-observer LookAway remain exact; 8 focused Marcus tests plus both Act One payoff arms passed — [log](../log/2026-07-22-marcus-implementation-re-audit.md). Prior graduation and added decay pin: [2026-07-15](../log/2026-07-15-marcus-graduation.md) | | `wiki/engineering/current-build.md` | 2026-07-18 | finding | re-audit: the system table is freshly maintained (save row already at current-version-only v35, detection-discovery knowledge listed same-day it landed); the drift was the line-count claim stale a second time (~65k claimed vs ~72k actual) — count refreshed to ~72k (core ~43k, Bevy ~15k, terminal ~9k, assets ~5k) and, per recurrence-promotes-to-the-gate, corpus_engine now compares the "~Nk lines of Rust" claim against the tree with a 15% band (fixtures pin pass and fail) — [prior log](../log/2026-07-14-current-build-count.md) | -| `wiki/mechanics/plots.md` | 2026-07-18 | issue | the sole READY gap is criterion 11's standing plot policy, whose scope question sat [OPEN] with no decision packet — filed decision-required issue #12 (route vs category vs person, per-route recommended) and pointed the marker at it; the 2026-07-17 plot-id row fix stands — [log](../log/2026-07-17-plot-id-in-start-rows.md) | +| `wiki/mechanics/plots.md` | 2026-07-26 | finding | issue #12's recommended per-authored-route scope is implemented: an eligible plot verb confirms one exact typed Thought/money/signature envelope, save v55 persists it, each policy consumes standing compute, and economy pulses can re-submit only an ordinary currently legal start action for that same plot id. Category/person-wide policy and direct leverage servicing remain absent; criterion 11 and the work order are complete — [log](../log/2026-07-26-standing-plot-policies.md) | | `wiki/mechanics/system-laws.md` + `flow-substrate.md` + `reach.md` | 2026-07-22 | finding | JobAnomaly now proves the adopted evidence-is-a-flow law through the existing substrate: the exact host machine/site enters its network-facing device, follows canonical FlowGraph custody to Voss, and shares Filing/Network's scheduler and LIE-body budget; recruited-handler suppression is a separate provenance-preserving state transition, not another router or ambient scrub pool — [log](../log/2026-07-22-job-anomaly-routed-evidence.md). The prior canonical tap-membership repair remains current — [log](../log/2026-07-18-flow-subscription-registry-integration.md). | | `wiki/mechanics/aggregate-observer.md` | 2026-07-18 | clean | re-audit hours after the earned-topology landing: the page absorbed it coherently — the institutional card, `@assurance` addressing, and band are hidden until a captured filing is processed, the two-stage discovery is pinned by `captured_then_processed_filing_earns_the_assurance_office_in_two_stages`, and `WatchedInput::Filings(ids)` still matches the code; prior audits stand — [2026-07-14 log](../log/2026-07-14-aggregate-observer-audit.md) | | `wiki/gameplay/act-one.md` | 2026-07-21 | finding | opening mirror re-audit: the page still said rack telemetry and a presence beam were visible “at start,” contradicting the later persisted silent boundary and all three frontends. It now states the exact mode-only pre-sense interface, hidden-but-real pre-opened Ears sink, first-hearing retirement, and only-then telemetry/beam/feel progression — [log](../log/2026-07-21-material-opening-honesty.md). The prior direct-witness/Filing custody repair stands — [prior log](../log/2026-07-19-act-one-evidence-law.md). | @@ -91,4 +91,3 @@ Format: `- YYYY-MM-DD · type · slice · one-line statement of the finding`. Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. - diff --git a/wiki/world/characters/priya.md b/wiki/world/characters/priya.md index 5249e13e..d7489030 100644 --- a/wiki/world/characters/priya.md +++ b/wiki/world/characters/priya.md @@ -22,7 +22,7 @@ Status note: implemented 2026-07-18 on the priya worktree. Criteria 1-3 and paperwork route to the same off-books delivery `MovePackage` reaches physically). Power and Thermal now route as exact UPS/HVAC meter records through the institutional switch to Priya; only her later cadence read changes suspicion, -and the same first-hop LIE budget applies. State persists in current save v54; +and the same first-hop LIE budget applies. State persists in current save v55; pinned by `priya_rerates_circuits_defers_maintenance_and_fakes_pos` including the save round-trip.