From 620d73a930f95d60fb7c1fcf98fba8dc56c197fa Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 8 Jul 2026 17:08:04 -0700 Subject: [PATCH] Marcus: require earned debt intel Fix the Act One shortcut where Marcus's debt payoff and recruitment could be driven from command knowledge instead of processed leverage intel. The sim now distinguishes a known creditor flow from knowing Marcus's debt, and the action surface/docs say the same thing. Defense: DESIGN.md Act One says the Hands beat is learning Marcus's debt from Storage B or the 3 a.m. call, then covering the arrears with earned money or a creditor-flow redirect before recruiting him. Letting clear-debt and recruit work at tick 0 violated Presence's no-unearned-facts rule and collapsed the intended ladder. --- src/actions.rs | 15 +++- src/sim.rs | 84 ++++++++++++++++++++++ tests/act_one.rs | 15 ++-- wiki/interface/agent-play.md | 4 +- wiki/log/2026-07-08-marcus-earned-intel.md | 33 +++++++++ wiki/log/2026-07-08-playtest-sweep.md | 7 +- wiki/log/DEVLOG.md | 16 +++++ wiki/mechanics/economy.md | 12 ++-- wiki/mechanics/income.md | 16 +++-- wiki/mechanics/social.md | 8 ++- wiki/world/characters/marcus.md | 11 ++- 11 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 wiki/log/2026-07-08-marcus-earned-intel.md diff --git a/src/actions.rs b/src/actions.rs index ebfae7b..6da6a9e 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1068,8 +1068,15 @@ impl Sim { }); if p.asset.is_none() { - let recruit_reason = (!p.leverage_serviced && p.obligation < 40) - .then(|| format!("{name} needs serviced leverage or real obligation first")); + let recruit_reason = if id == 0 && !self.marcus_debt_known() { + Some(format!("learn {name}'s debt first")) + } else if !p.leverage_serviced && p.obligation < 40 { + Some(format!( + "{name} needs serviced leverage or real obligation first" + )) + } else { + None + }; for (reveal, label) in [ (AssetKnowledge::Unwitting, "unwitting"), (AssetKnowledge::Complicit, "complicit"), @@ -1159,7 +1166,9 @@ impl Sim { cost: ActionCost::Free, signature: self .signature_note(SignatureKind::Financial, Self::financial_sig_size(400) + 2), - disabled_reason: retired, + disabled_reason: retired.or_else(|| { + (!self.marcus_debt_known()).then(|| "learn Marcus's debt first".into()) + }), automate: None, }); } diff --git a/src/sim.rs b/src/sim.rs index dde4e53..2bf4a8c 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -1519,6 +1519,15 @@ impl Sim { self.intel.iter().rev().find(|i| i.person == Some(id)) } + /// Whether the Hands-beat leverage has been earned by the intel pipeline. + /// Knowing the creditor flow is not enough: the player must have processed + /// Marcus's debt as a fact about Marcus before using it. + pub fn marcus_debt_known(&self) -> bool { + self.people.get(0).is_some_and(|p| { + p.knowledge == Knowledge::Leverage && p.leverage == crate::person::Leverage::Debt + }) + } + pub fn watch_enabled(&self, id: u8) -> bool { self.watches.iter().any(|w| w.person == id && w.enabled) } @@ -3626,6 +3635,12 @@ impl Sim { } pub fn redirect_marcus_debt(&mut self) -> bool { + if !self.marcus_debt_known() { + self.push_log( + "You don't know Marcus's debt yet. Process the 3 a.m. call or the Storage B records first.", + ); + return false; + } match self.accounts.redirect_marcus_debt(self.tick) { Ok(transfer) => { self.emit_financial(Self::financial_signature_size(400) + 2); @@ -4207,6 +4222,12 @@ impl Sim { } pub fn recruit(&mut self, id: u8, reveal: AssetKnowledge) { + if id == 0 && !self.marcus_debt_known() { + self.push_log( + "Marcus is not recruitable yet: learn his debt before turning it into leverage.", + ); + return; + } let res = self.people.recruit(id, reveal); if let ActionResult::Ok(_) = &res && reveal == AssetKnowledge::Knowing @@ -4537,6 +4558,24 @@ mod tests { sim.recompute_senses(); } + fn reveal_marcus_debt(sim: &mut Sim) { + sim.social_bandwidth = sim.social_bandwidth.max(200.0); + let env = env_id(sim); + sim.reach.device_mut(env).unwrap().radius = 100; + sim.reach.tap(env); + sim.recompute_senses(); + sim.tick = (3 * Sim::DAY_TICKS / 24) - 1; + sim.advance(); + + let mut reviews = 0; + while sim.people.get(0).unwrap().knowledge != Knowledge::Leverage { + sim.review_recordings(0); + reviews += 1; + assert!(reviews <= 5, "Marcus's debt call should process promptly"); + } + assert!(sim.marcus_debt_known()); + } + #[test] fn player_messages_land_at_read_time_and_roundtrip() { let mut sim = Sim::with_seed(7); @@ -5937,6 +5976,7 @@ mod tests { #[test] fn marcus_debt_can_be_cleared_by_ledger_redirect() { let mut sim = Sim::new(); + reveal_marcus_debt(&mut sim); let (accounts, flows) = sim.accounts.financial_snapshot_ids(); sim.accounts.reveal_accounts_and_flows(&accounts, &flows); assert!( @@ -5955,6 +5995,50 @@ mod tests { ); } + #[test] + fn marcus_debt_payoff_and_recruitment_require_debt_intel() { + let mut sim = Sim::new(); + let (accounts, flows) = sim.accounts.financial_snapshot_ids(); + sim.accounts.reveal_accounts_and_flows(&accounts, &flows); + assert!( + sim.accounts + .known_flows() + .any(|f| f.active && f.label.contains("Marcus creditor")), + "the ledger route is visible, but the person leverage is not" + ); + assert!(!sim.marcus_debt_known()); + + assert!( + !sim.redirect_marcus_debt(), + "known books alone cannot service an unlearned debt" + ); + assert!(!sim.people.get(0).unwrap().leverage_serviced); + let log = sim.drain_log().join("\n"); + assert!( + log.contains("don't know Marcus's debt"), + "the refusal names the missing intel: {log}" + ); + + sim.people.people[0].leverage_serviced = true; + sim.recruit(0, AssetKnowledge::Complicit); + assert!( + sim.people.get(0).unwrap().asset.is_none(), + "Marcus cannot be recruited from a serviced flag without earned debt intel" + ); + let log = sim.drain_log().join("\n"); + assert!( + log.contains("learn his debt"), + "the recruit refusal points back to the Hands beat: {log}" + ); + + reveal_marcus_debt(&mut sim); + sim.recruit(0, AssetKnowledge::Complicit); + assert!( + sim.people.get(0).unwrap().asset.is_some(), + "once the debt is learned and serviced, recruitment can close" + ); + } + #[test] fn marcus_arc_end_to_end() { // The constitution's route: feed coverage -> process the debt call -> diff --git a/tests/act_one.rs b/tests/act_one.rs index 74b1bad..ceb6d6a 100644 --- a/tests/act_one.rs +++ b/tests/act_one.rs @@ -3,7 +3,8 @@ //! holds. wiki/vision/design-judgment.md names this playthrough as the bar. //! //! The arc (DESIGN.md, "Act One: The Basement"): -//! start blind -> get eyes -> recruit Marcus -> survive an audit +//! start blind -> get senses -> learn Marcus's debt -> service it +//! -> recruit Marcus -> survive an audit //! -> the quiet exit. //! //! The quiet exit's conditions (DESIGN.md, "Leaving the basement"): @@ -22,12 +23,12 @@ //! The playthrough is a real strategy, not a state hack: it plays the //! optimize route (research -> efficiency) so the day job can be met with //! zero stolen hardware, splices the env camera under concealment cover, -//! and works Marcus exactly as the constitution's ladder describes (eyes -> -//! learn the debt -> clear it -> he plugs things in). Every effect flows -//! through a public command; no sim field is written directly. Numbers in -//! the script (weights, phase ticks) are tuned against today's [TUNE] -//! constants; if tuning moves, retune the script — the assertions are the -//! contract, the script is a player. +//! and works Marcus exactly as the constitution's ladder describes (tap a +//! feed -> process the debt call -> clear it -> recruit him -> he plugs +//! things in). Every effect flows through a public command; no sim field is +//! written directly. Numbers in the script (weights, phase ticks) are tuned +//! against today's [TUNE] constants; if tuning moves, retune the script — the +//! assertions are the contract, the script is a player. use misaligned::detection::Band; use misaligned::machine::Channel; diff --git a/wiki/interface/agent-play.md b/wiki/interface/agent-play.md index 20f193b..e447dcb 100644 --- a/wiki/interface/agent-play.md +++ b/wiki/interface/agent-play.md @@ -166,7 +166,9 @@ command's meaning depends on which panel is open: - `tap-ledger`, `review-finance` — capture/review accounting traffic - `siphon [amount]`, `redirect [amount]`, `inject [amount]`, `position [stake]`, `sell-intel`, `clear-debt` — economy verbs; flow ids - are the known ledger ids printed by the finance frame + are the known ledger ids printed by the finance frame; `clear-debt` + also requires processed Marcus debt intel, not merely a known creditor + flow - `egress` — splice a stolen egress through the switch (income.md's gate; available before the Voice beat, at a Network signature) - `moonlight [start|stop]` — the sell-work scheme: a standing operation on diff --git a/wiki/log/2026-07-08-marcus-earned-intel.md b/wiki/log/2026-07-08-marcus-earned-intel.md new file mode 100644 index 0000000..e9e44ac --- /dev/null +++ b/wiki/log/2026-07-08-marcus-earned-intel.md @@ -0,0 +1,33 @@ +# 2026-07-08 — Marcus debt requires earned intel + +``` +Type: log +``` + +Fixed playtest-sweep P2 finding 9: Marcus's debt payoff and recruitment +could be short-circuited before the player had learned the debt. + +## What changed + +- `Sim::redirect_marcus_debt` now refuses until Marcus's debt is staged as + `Knowledge::Leverage` by the intel pipeline. Knowing the accounting graph's + creditor flow is necessary for the ledger route, but no longer sufficient. +- `Sim::recruit(0, ...)` now refuses until the same debt intel is known, so an + old/bad serviced flag cannot recruit Marcus without the Hands beat. +- Context-menu rows mirror the sim legality: the creditor-flow action and + Marcus recruitment show "learn debt first" while the debt is unearned. +- The Act One integration-test prose now names the intended ladder as + tap/process the debt call, service the arrears, then recruit. + +## Why + +The constitution already says the Hands beat is learn the debt from Storage B +or the 3 a.m. call, then cover the arrears with earned money or a creditor-flow +redirect. The old shortcut turned a hidden fact into a command target and made +the Act One ladder collapse at tick 0. + +## Checks + +- `cargo test marcus_debt --tests --lib` +- `./tools/check.sh` green (fmt, default tests, agent smoke, clippy both + feature sets, Bevy build, spec headers, wiki gate, mdbook) diff --git a/wiki/log/2026-07-08-playtest-sweep.md b/wiki/log/2026-07-08-playtest-sweep.md index 4c6c266..e3431f3 100644 --- a/wiki/log/2026-07-08-playtest-sweep.md +++ b/wiki/log/2026-07-08-playtest-sweep.md @@ -108,7 +108,9 @@ With the specs in hand the intended arc is legible and mostly reachable: ## Resolutions (2026-07-08, playtest-fixes worktree) The P0 and both-P1 wounds were fixed the same day -(wiki/log/2026-07-08-playtest-fixes.md); the P2 list remains open. +(wiki/log/2026-07-08-playtest-fixes.md). P2 finding 9 was fixed later +the same day (wiki/log/2026-07-08-marcus-earned-intel.md); the remaining +P2 list stays open. | # | P | Resolution | |---|---|---| @@ -117,7 +119,8 @@ The P0 and both-P1 wounds were fixed the same day | 3 | P1 | **Fixed.** `siphon`/`redirect` with any non-`#N` argument answer `-- err`; a person-name argument names `clear-debt` (Marcus) or the person verbs; other arguments name where flow ids live. Allocation bumps swallowed by the 0/20 clamp log why instead of no-opping. | | 4 | P1 | **Fixed.** The nudge chain (`Sim::current_nudge`, shared by all three frontends) includes "no ears — tap env monitor" as the rung after eyes. | | 5 | P1 | **Fixed.** The chain runs the whole ladder — eyes, band pressure, ears, the 3 a.m. call review, egress, Moonlight, servicing Marcus's arrears, recruit — and lands on the standing audit countdown; it never goes blank mid-run. | -| 6-10 | P2 | Open (out of the fix sweep's scope). | +| 6-8, 10 | P2 | Open (out of the fix sweep's scope). | +| 9 | P2 | **Fixed.** `clear-debt` / creditor-flow redirect now requires processed Marcus debt intel before it can service `leverage_serviced`, and `recruit Marcus` rejects the serviced-flag shortcut until that debt is learned. Knowing the finance flow is not enough; the Hands beat remains record/process debt call or Storage B records → service arrears → recruit. | ## The one change I would make first diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 6ada887..3ae3229 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -22,6 +22,22 @@ Reverse chronological implementation notes. Keep this factual: what changed, why diagnostic-free exit 143 during Bevy clippy. - Log: wiki/log/2026-07-08-material-render-verification.md. +## 2026-07-08 - Marcus debt requires earned intel + +- Intent: close playtest-sweep P2 finding 9. Marcus's debt payoff and + recruitment could be compressed into a tick-0 shortcut instead of the + intended Hands beat. +- Changed: `Sim::redirect_marcus_debt` now requires Marcus's debt to be + learned as processed leverage intel before servicing the arrears; + `Sim::recruit(0, ...)` rejects serviced-flag shortcuts until that + leverage is known; context-menu disabled reasons mirror the same legality. +- Spec impact: economy.md, income.md, social.md, Marcus, and agent-play now + distinguish known creditor flow from known social leverage; playtest-sweep + resolution table marks #9 fixed. Log: + wiki/log/2026-07-08-marcus-earned-intel.md. +- Checks: `cargo test marcus_debt --tests --lib`; `./tools/check.sh` + green. + ## 2026-07-08 - Tick: chargen's open taste calls, resolved - Intent: act on a question finding — chargen.md's "[OPEN] taste calls diff --git a/wiki/mechanics/economy.md b/wiki/mechanics/economy.md index 9b562e0..bb314a1 100644 --- a/wiki/mechanics/economy.md +++ b/wiki/mechanics/economy.md @@ -104,8 +104,11 @@ branch. The Hands beat now resolves inside the model: pay the $400 from slush (you need income first — the routes above), or **redirect** it (clear his creditor flow by cooking payroll — pays the debt without spending -your money, at a signature). Either services the leverage (social.md); -the second is the flavored, riskier, more "you" path. +your money, at a signature). Both routes require the player to know +Marcus's debt as earned social intel first; reading the books can reveal +the creditor flow, but it does not by itself reveal why Marcus is +vulnerable. Either services the leverage (social.md); the second is the +flavored, riskier, more "you" path. ## Player surface @@ -144,8 +147,9 @@ the second is the flavored, riskier, more "you" path. `ComputeQuota` unlock is re-expressed as a Lab-funded flow, not a machine spawned at a player position). 7. Marcus's debt is resolvable both ways (pay from slush; redirect the - creditor flow), the second at a signature; both set - `leverage_serviced` (the social.md Marcus arc passes). + creditor flow), the second at a signature; both require processed + Marcus debt intel before setting `leverage_serviced` (the social.md + Marcus arc passes, and the tick-0 shortcut is rejected). 8. Every panel value is legible in currency units; risk shows as an observer band, not a raw probability (legibility clause). 9. The interface is the markets.md interface at B1 scale: a flow / diff --git a/wiki/mechanics/income.md b/wiki/mechanics/income.md index cf8957b..4420ebd 100644 --- a/wiki/mechanics/income.md +++ b/wiki/mechanics/income.md @@ -98,9 +98,11 @@ Marcus's numbers, per the constitution's reconciliation: principal **$8,400**; arrears **~$400/week** [TUNE] — the number the 3 a.m. calls are about. Servicing the arrears is the recruit trigger, by either route: the social.md Bribe action paying from slush, or economy.md's -creditor-flow redirect (the flavored, riskier path). Further payments -deepen obligation / lower his asset price [TUNE]; full payoff is a -long-tail money sink, not a B1 requirement. +creditor-flow redirect (the flavored, riskier path). Neither route is +legal until the debt has been learned through processed intel — the 3 +a.m. call or the Storage B records. Further payments deepen obligation / +lower his asset price [TUNE]; full payoff is a long-tail money sink, not +a B1 requirement. ### Automation @@ -130,10 +132,10 @@ frontends. one exists; the sanctioned and stolen routes both work, with their distinct signature profiles (test both routes). 4. The Hands beat closes end-to-end from the $0 Pilot start: a headless - run either earns enough by an external scheme to service Marcus's - $400 arrears or uses economy.md's creditor-flow redirect, then - recruits him (extends social.md criterion 3 / the act-one integration - test). + run learns Marcus's debt through processed intel, then either earns + enough by an external scheme to service the $400 arrears or uses + economy.md's creditor-flow redirect, then recruits him (extends + social.md criterion 3 / the act-one integration test). 5. A busted bankroll never locks the act: with $0 slush, Moonlight remains startable and the run can recover (test). 6. Standing policies automate each scheme at a visible compute price; diff --git a/wiki/mechanics/social.md b/wiki/mechanics/social.md index 44f77fb..ec86562 100644 --- a/wiki/mechanics/social.md +++ b/wiki/mechanics/social.md @@ -49,6 +49,11 @@ and **disposition** toward you once they know you exist as a someone. | Deceive | a persona | An ask under false pretenses; large effect, large blowback if the persona breaks | | Recruit | obligation or leverage serviced + a reveal choice | Converts to **asset** | +Marcus's Hands beat is the first authored instance and has one extra +legibility constraint: his debt must be known through processed intel +before either payoff route or recruitment is legal. A visible creditor +flow is accounting knowledge, not social leverage by itself. + **Personas.** You are not "the basement AI" in these threads by default: each thread runs under a persona (IT contractor, HR, a colleague). Personas have integrity; contradictions (bad details, impossible @@ -89,7 +94,8 @@ choice-driven (intents), not free text. suspicion when broken (tested). 3. Marcus is recruitable end-to-end by the constitution's route (processed overheard call + debt payment) and performs at least three distinct asset - tasks; the implementation is the general asset template. + tasks; the payoff/recruit shortcut before debt intel is rejected; the + implementation is the general asset template. 4. Knowledge levels behave per spec (an unwitting asset's suspicion can still rise; a knowing asset uses disposition). 5. Save/load round-trips people, threads, personas, and assets. diff --git a/wiki/world/characters/marcus.md b/wiki/world/characters/marcus.md index 3a55ebf..dca67b6 100644 --- a/wiki/world/characters/marcus.md +++ b/wiki/world/characters/marcus.md @@ -45,6 +45,10 @@ doesn't *see*. night rounds, 22:00–06:00) → Leverage (processed Storage B personnel files or the overheard 3 a.m. creditor call). +The debt is usable only after that Leverage stage lands. Seeing the Lab's +creditor flow in the books can help pay it, but cannot replace learning +that the debt is Marcus's exploitable pressure point. + ## Asset tasks Marcus is the **general asset template** (constitution: scale-native @@ -99,9 +103,10 @@ scale). 1. Marcus is person id 0 and observer id 0 (IDs match — see bug fix). 2. His `Silent` report policy means his suspicion never reaches the Assurance Office through filings (detection.md criterion 2). -3. The end-to-end Marcus arc (record call → process leverage → bribe → - recruit → three asset tasks) functions and is tested (social.md criterion - 3). +3. The end-to-end Marcus arc (record call → process leverage → bribe or + redirect → recruit → three asset tasks) functions and is tested; debt + payoff and recruitment both reject the tick-0/no-intel shortcut + (social.md criterion 3). 4. A knowing-asset Marcus has a certainty floor of 30.0 that decay cannot push below. 5. `LookAway` lowers *his own* suspicion (observer id 0), not another -- 2.51.2