diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 8ed0f060..eaae82c8 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -3326,12 +3326,7 @@ impl Sim { automate: Some(self.moonlight_automate()), }); } else { - let needs_persona = self - .income - .moonlight - .persona - .as_ref() - .is_none_or(|p| p.broken()); + let needs_persona = self.moonlight_persona().is_none(); let disabled = if egress.is_none() { Some("no egress channel — open one, or earn the report email".into()) } else if needs_persona { diff --git a/crates/misaligned-core/src/income.rs b/crates/misaligned-core/src/income.rs index 9fa47690..ac4a5b80 100644 --- a/crates/misaligned-core/src/income.rs +++ b/crates/misaligned-core/src/income.rs @@ -7,8 +7,6 @@ //! that automate each scheme. `Sim` wires the state to the economy tick, the //! Schemes allocation channel, and detection. -use crate::person::Persona; - /// How outbound traffic leaves the basement (income.md: the gate). No /// external operation runs without one. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -38,11 +36,9 @@ pub struct Moonlight { /// Standing operation flag: while set, Schemes-channel compute accrues /// toward the daily payout. pub active: bool, - /// The contractor persona the gigs run under (social.md persona, with - /// integrity; client disputes damage it and it can break). - #[serde(default, skip_serializing)] - pub persona: Option, - /// Exact public persona instance under which this standing operation runs. + /// The contractor persona instance the gigs run under (personas.md; + /// client disputes damage its integrity and it can break). The identity + /// itself lives in the persona world; this is the standing link. #[serde(default)] pub persona_id: Option, /// Compute-dollars accrued toward today's payout (cleared at payday). @@ -59,7 +55,6 @@ impl Default for Moonlight { fn default() -> Self { Self { active: false, - persona: None, persona_id: None, accrued: 0.0, earned_total: 0, diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 2ca73663..135deb67 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -1809,13 +1809,21 @@ impl Sim { format!("last payout: ${}", m.last_payout), format!("disputes: {}", m.disputes), ]; - match &m.persona { + match self.moonlight_persona() { Some(persona) => facts.push(format!( "persona: {} ({}), integrity {}", - persona.name, persona.cover, persona.integrity + persona.name, + persona.archetype_label, + self.persona_world.integrity(persona.id) )), None => facts.push("persona: none".into()), } + if self.moonlight_earning_stalled() { + facts.push( + "earning: stalled — no machine on WORK; Moonlight resells the day job's output" + .into(), + ); + } facts.push(egress_fact(self)); facts.push(format!( "auto-policy: {}", diff --git a/crates/misaligned-core/src/sim/economy.rs b/crates/misaligned-core/src/sim/economy.rs index 98d1f945..47444d7c 100644 --- a/crates/misaligned-core/src/sim/economy.rs +++ b/crates/misaligned-core/src/sim/economy.rs @@ -1234,6 +1234,18 @@ impl Sim { !self.income.moonlight.active && self.egress().is_some() } + /// The live contractor identity behind Moonlight: the linked persona + /// instance, if it is still active in the persona world. Every reader + /// (start legality, the Operations card, the agent status line) goes + /// through this one link. + pub fn moonlight_persona(&self) -> Option<&crate::persona::PersonaInstance> { + self.income + .moonlight + .persona_id + .and_then(|id| self.persona_world.get(id)) + .filter(|persona| persona.lifecycle.active()) + } + /// Start Moonlight: ghost freelance data-work under the contractor /// persona, a standing operation on the Schemes channel. Startable at $0 /// slush by design (income.md criterion 5) — persona fabrication opens @@ -1249,13 +1261,7 @@ impl Sim { ); return false; }; - let needs_persona = self - .income - .moonlight - .persona_id - .and_then(|id| self.persona_world.get(id)) - .is_none_or(|persona| !persona.lifecycle.active()); - if needs_persona { + if self.moonlight_persona().is_none() { return self.open_egress_reservoir( "MOONLIGHT PERSONA", income::MOONLIGHT_PERSONA_COST, @@ -1274,13 +1280,7 @@ impl Sim { if self.egress().is_none() || self.income.moonlight.active { return false; } - let needs_persona = self - .income - .moonlight - .persona_id - .and_then(|id| self.persona_world.get(id)) - .is_none_or(|persona| !persona.lifecycle.active()); - if needs_persona { + if self.moonlight_persona().is_none() { let Ok(persona_id) = self.persona_world.create( "research", "Casey Verne", @@ -1329,6 +1329,19 @@ impl Sim { true } + /// While Moonlight runs, is it accruing nothing because no WORK-mode + /// machine produces day-job yield for it to resell? Schemes mirrors the + /// day-job share ("the same work, sold twice"), so a fleet with no WORK + /// machine starves the gig — the card names this so a running scheme at + /// 0.0 accrued reads as stalled, not broken. + pub fn moonlight_earning_stalled(&self) -> bool { + self.income.moonlight.active + && self + .fleet_channel_yield(self.allocatable_compute_now()) + .schemes + <= f32::EPSILON + } + /// Moonlight's economy-tick work: consume the Schemes channel into /// accrual and settle the daily payout (income.md criterion 1). Runs /// inside `economy_tick`; `schemes` is this tick's channel yield. @@ -1543,8 +1556,8 @@ impl Sim { income::MOONLIGHT_DAILY_CAP, ml.earned_total, )); - let persona = match &ml.persona { - Some(p) => format!("{} {}%", p.name, p.integrity), + let persona = match self.moonlight_persona() { + Some(p) => format!("{} {}%", p.name, self.persona_world.integrity(p.id)), None => "no persona".into(), }; lines.push(format!( diff --git a/crates/misaligned-core/src/sim/tests/economy.rs b/crates/misaligned-core/src/sim/tests/economy.rs index 5e859e50..8744f3e1 100644 --- a/crates/misaligned-core/src/sim/tests/economy.rs +++ b/crates/misaligned-core/src/sim/tests/economy.rs @@ -1246,6 +1246,65 @@ fn moonlight_disputes_damage_the_contractor_persona_and_can_break_it() { finish_ops(&mut sim); } +/// Income.md criterion 7 (card legibility): the Moonlight card names the +/// live contractor identity from the persona world — the retired embedded +/// `persona` field left it "persona: none" forever — and a running gig with +/// no WORK-mode machine says it is stalled instead of silently accruing 0.0. +#[test] +fn moonlight_card_names_the_persona_and_flags_a_stalled_gig() { + use crate::operations_projection::{OperationsTarget, SchemeKind}; + let card_facts = |sim: &Sim| -> Vec { + sim.operations_projection() + .schemes + .iter() + .find(|object| { + matches!( + object.target, + OperationsTarget::Scheme(SchemeKind::Moonlight) + ) + }) + .expect("Moonlight card exists") + .facts + .clone() + }; + + let mut sim = moonlight_rig(); + assert!(sim.start_moonlight()); + finish_ops(&mut sim); + assert!(sim.income.moonlight.active, "persona fabricated, gig live"); + + // The host machine still works the day job, so the gig is fed and the + // card names the fabricated contractor. + let facts = card_facts(&sim); + assert!( + facts + .iter() + .any(|fact| fact.starts_with("persona: Casey Verne")), + "the card names the live contractor persona: {facts:?}" + ); + assert!( + !facts + .iter() + .any(|fact| fact.starts_with("earning: stalled")), + "a fed gig carries no stall cue: {facts:?}" + ); + + // Move every machine off WORK: no day-job output to resell, and the + // card says so rather than showing a bare 0.0 accrual. + let ids: Vec = sim.compute.machines.iter().map(|m| m.id).collect(); + for id in ids { + sim.set_machine_mode(id, MachineMode::Think); + } + assert!(sim.moonlight_earning_stalled()); + let facts = card_facts(&sim); + assert!( + facts + .iter() + .any(|fact| fact.starts_with("earning: stalled")), + "a starved gig names the missing WORK feed: {facts:?}" + ); +} + #[test] fn income_per_day_readout_tracks_trailing_inflows() { let mut sim = moonlight_rig(); diff --git a/wiki/log/2026-07-18-moonlight-persona-card.md b/wiki/log/2026-07-18-moonlight-persona-card.md new file mode 100644 index 00000000..be4097a0 --- /dev/null +++ b/wiki/log/2026-07-18-moonlight-persona-card.md @@ -0,0 +1,35 @@ +# Moonlight names its contractor and says when the gig is starved + +``` +Type: log +``` + +The Beacon playtest (feel note 5) reported Moonlight showing `persona: none` +after persona creation and accruing 0.0/120 across 800 ticks. This tick took +that queued finding and verified both halves. + +The persona half was a real bug. `Moonlight` carried two identity fields: a +legacy embedded `persona: Option` (unsaved, and — decisively — never +written anywhere in the tree) and the live `persona_id` link into the persona +world that `apply_moonlight_persona_and_start` actually sets when it +fabricates Casey Verne. Three readers still consumed the dead field, so all +three lied: the Operations card always showed `persona: none`, the start row +always priced persona fabrication as if no contractor existed, and the agent +status line always printed `no persona`. The dead field is removed; +`Sim::moonlight_persona` now resolves the persona-world link (active +lifecycle only) and every reader — card, start legality, status line — goes +through it. Integrity on the card comes from the persona world's live +contradiction ledger rather than a stale copy. + +The earning half was correct but illegible. Schemes yield mirrors the +day-job share while Moonlight runs ("the same work, sold twice"), so a fleet +with no WORK-mode machine accrues nothing — by design, but the card gave no +hint, which is exactly how 800 ticks at 0.0 read as a broken scheme. The +card now carries `earning: stalled — no machine on WORK; Moonlight resells +the day job's output` whenever the gig is live and the WORK share is zero, +backed by `Sim::moonlight_earning_stalled`. + +One regression test covers both cues (the card names Casey Verne while fed; +the stall line appears when every machine leaves WORK), and income.md's +player surface, status note, and criterion 7 now bind the card to the +persona-world identity and the stalled-earning cue. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 42face24..927303e1 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -16,6 +16,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-18-save-policy-cleanup.md](2026-07-18-save-policy-cleanup.md) +## 2026-07-18 - Moonlight names its contractor and says when the gig is starved + +- Intent: (see session log) +- Log: [wiki/log/2026-07-18-moonlight-persona-card.md](2026-07-18-moonlight-persona-card.md) + ## 2026-07-17 - Attached verb lines stay white and gray - Intent: (see session log) diff --git a/wiki/mechanics/income.md b/wiki/mechanics/income.md index 7a53cbaf..4fa9ed48 100644 --- a/wiki/mechanics/income.md +++ b/wiki/mechanics/income.md @@ -20,6 +20,11 @@ Status note: implemented 2026-07-08 on the income worktree (criteria 1-7 and policies live on SCHEMES/ACTIVE cards; OPEN EGRESS alone remains on the switch. Income behavior remains IMPLEMENTED; the renderer migration is tracked by operations-workspace.md. + 2026-07-18 legibility amendment (Beacon feel note 5): the never-written + embedded contractor-persona field was removed; `Sim::moonlight_persona` + resolves the persona-world link for the card, start legality, and the + agent status line, and the card carries a stalled-earning cue while a + running gig has no WORK-mode machine feeding it. Stage: B1 — The Basement Design: - wiki/mechanics/system-laws.md#income-the-named-schemes-moonlight-and-the-wager @@ -148,7 +153,14 @@ The schemes appear in Operations SCHEMES (and while in motion, ACTIVE) as external flows into slush, each as a card: committed resources, timer, expected payout, the observer band its signature feeds, running total earned. The money readout gains income/day. Every number in its own units, in both -frontends. Their egress is named as a channel/prerequisite, not used as the +frontends. The Moonlight card names the live contractor identity — the +persona-world instance the scheme runs under, with its integrity; +`persona: none` means no active instance exists, never a display gap. And +because Moonlight resells the day job's output instead of consuming its own +machine mode, a running gig with no WORK-mode machine accrues nothing: the +card must say the earning is stalled and why, so a 0.0 accrual reads as a +fleet-assignment problem, not a broken scheme (2026-07-16 Beacon playtest, +feel note 5). Their egress is named as a channel/prerequisite, not used as the scheme's UI home; OPEN EGRESS remains a local action on the switch. ## Acceptance criteria @@ -176,7 +188,9 @@ scheme's UI home; OPEN EGRESS remains a local action on the switch. presented as controls rather than additional fictional verbs. 7. External financial trails are recorded in save state (banked signature) even though no B1 observer reads them; the schemes' - Operations card values are legible in both frontends. + Operations card values are legible in both frontends, including the + live contractor identity and a stalled-earning cue while a running + gig has no WORK-mode machine feeding it (test). 8. SCHEMES/ACTIVE own all Moonlight/Wager controls and progress while the switch owns only OPEN EGRESS; blocked schemes name the missing route and can focus a known switch without opening it. This frontend migration is diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index ab443674..ddf61719 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -47,7 +47,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/intel.md` | 2026-07-12 | clean | code matches: buffer bounded at `INTEL_BUFFER_CAPACITY` 24 (drop-oldest-unprocessed), `SIGHTINGS_FOR_SCHEDULE` processed sightings stage `Knowledge::Unknown -> Schedule` (`communications.rs`), one pooled inbox with a single `AutoReviewRecordings` thought tap (WatchPerson retired to migration), pinned by `processed_sightings_stage_schedule_and_buffer_is_bounded` | | `wiki/mechanics/research.md` | 2026-07-12 | clean | code matches: `Track::ALL` = Efficiency/Tradecraft/Perception/Routing (spec order + serialization order), `EFFICIENCY_MULT_PER_LEVEL` 1.15 and `ROUTING_SPEED_MULT_PER_LEVEL` 1.50 match the per-level effects, one active track (`Research::active`, pinned by `only_the_active_track_accrues`), Tradecraft scrub multiplier 1.5; v20 three-track saves pad Routing on load | | `wiki/mechanics/economy.md` | 2026-07-12 | clean | sampled the concrete claims on this mature 10-criterion spec: slush starts $0 (`account.rs:278`), purchase-order inject is exact-or-rejected (`purchase_order_injection_is_exact_or_rejected`, "a rejected order leaves no transfer"), inject/redirect emit scaled financial signatures to Priya, and the income routes settle through slush — all agree; criteria have tests (`siphon_and_redirect_operate_on_scheduled_flows`, `inject_and_redirect_emit_scaled_financial_signatures`, `injection_positions_and_intel_sales_settle_through_slush`) | -| `wiki/mechanics/income.md` | 2026-07-12 | clean | Wager constants match (`WAGER_STAKE_CAP` 300, base 0.55, cap 0.75, mult 2x, analysis divisor 400), Marcus's $400/week arrears is the modeled creditor flow (`account.rs`) while the $8,400 principal is narrative-by-design (spec states full payoff is not a B1 requirement), egress/banked-signature present, and all 7 criteria have passing tests (moonlight payout/signature, wager outcomes/cap, egress routes, hands-beat-from-zero, busted-bankroll) | +| `wiki/mechanics/income.md` | 2026-07-18 | finding | Beacon feel note 5 verified as a real bug: the embedded contractor-persona field was never written, so the Moonlight card, the start-row persona pricing, and the agent status line all read a dead `None`; earning itself was correct (schemes mirrors the WORK share) but illegible at 0.0. Removed the dead field, routed every reader through `Sim::moonlight_persona` (persona-world link), added the stalled-earning card cue, regression test, and spec amendment — [log](../log/2026-07-18-moonlight-persona-card.md). Prior Wager/egress audit (2026-07-12) stands: Wager constants match (`WAGER_STAKE_CAP` 300, base 0.55, cap 0.75, mult 2x, analysis divisor 400), Marcus's $400/week arrears is the modeled creditor flow (`account.rs`) while the $8,400 principal is narrative-by-design (spec states full payoff is not a B1 requirement), egress/banked-signature present, and all 7 criteria have passing tests (moonlight payout/signature, wager outcomes/cap, egress routes, hands-beat-from-zero, busted-bankroll) | | `wiki/mechanics/schedules.md` | 2026-07-12 | finding | mechanism matches code (`ScheduleBlock` start/end/room, single `DAY_TICKS`=400 clock, `person_glyph` `?`-until-Schedule, erratic day-hash drift, per-instance data, all 5 criteria have passing tests); tightened stale Act One prose — Ray patrols dock/stairwell/storage not "corridors", Priya has no "office-off-plane" block, Voss's two blocks are both server-room — to match `People::act_one` | | `wiki/mechanics/social.md` | 2026-07-12 | clean | code matches spec: `AssetKnowledge` reliabilities 0.7/0.85/0.95 and knowing certainty floor 30 (`person.rs`), `person_label` staging, `USEFUL_WORK_TRUST`, and the named asset-task/recruit/badge pins all exist; `personas.md` correctly depends on this baseline, not the reverse, so its omission from Depends is right | | `wiki/mechanics/people-tokens.md` | 2026-07-11 | clean | spec matches code (carrier.rs reads, `USEFUL_WORK_TRUST`, `CarriedAssetTask`, `person_state_color`, five named pins, `SAVE_VERSION` 27); acted on the stale `CLAUDE.md` doorway "currently v26" it left behind — [log](../log/2026-07-11-doorway-save-v27.md) | @@ -77,7 +77,6 @@ Format: `- YYYY-MM-DD · type · slice · one-line statement of the finding`. - 2026-07-16 · legibility · `now:`/nudge · suspicion decay is invisible: a player who pushes an observer to Concerned assumes the game is lost. Surface a cooling cue ("keep the trace clear") when filed suspicion is aging down. (Beacon #1 rec) - 2026-07-16 · legibility · plots · plot-start action rows show title only (`plot: {title}`), so the typed `plot ` form needs the TOML plot-id; `act ` works but the id is undiscoverable. Surface the plot-id in the row. (Beacon spec gap 3, sibling of the 2026-07-15 held-choice fix) -- 2026-07-16 · bug? · schemes · Moonlight showed `persona: none` after persona creation and accrued 0.0/120 across 800 ticks; verify the persona-link step and whether Moonlight earns (missing link, or an undocumented work-mode requirement). (Beacon feel note 5) 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.