diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index d7f5d28d..9e886867 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -2192,7 +2192,12 @@ impl Sim { // Plot signatures are derived as each typed act occurs, // not asserted as one misleading up-front signature. signature: None, - disabled_reason: plot.ineligibility(&context), + disabled_reason: plot.ineligibility(&context).or_else(|| { + self.ops_action_blocked_reason(&OpsJobKind::StartPlot { + person: id, + plot_id: plot.id.clone(), + }) + }), automate: None, }); } @@ -2842,6 +2847,34 @@ mod tests { })); } + #[test] + fn queued_plot_disables_every_alternate_route_for_that_person() { + let mut s = sim(); + s.people.people[0].knowledge = Knowledge::Leverage; + s.people.has_channel = true; + s.people.persona = Some(crate::person::Persona::new("Sam", "contractor")); + s.accounts.set_slush_balance(1000); + + s.start_plot(0, "marcus-debt-settled"); + + let acts = s.available_actions(Anchor::Person(0)); + let plots: Vec<_> = acts + .iter() + .filter(|action| matches!(action.command, ActionCommand::StartPlot { person: 0, .. })) + .collect(); + assert_eq!(plots.len(), 2, "the authored alternatives stay legible"); + assert!(plots.iter().all(|plot| { + plot.disabled_reason + .as_deref() + .is_some_and(|reason| reason.contains("already queued")) + })); + assert_eq!( + s.operations_readout().jobs, + 1, + "the first selected route owns the person's pending plot slot" + ); + } + /// Executing the watch toggle through the dispatcher flips the /// standing watch (the automate entry is live, criterion 7). #[test] diff --git a/crates/misaligned-core/src/ops_jobs.rs b/crates/misaligned-core/src/ops_jobs.rs index 66cff98a..1065a0ed 100644 --- a/crates/misaligned-core/src/ops_jobs.rs +++ b/crates/misaligned-core/src/ops_jobs.rs @@ -86,6 +86,7 @@ impl OpsJobKind { (Self::ReviewRecording { raw_id: a, .. }, Self::ReviewRecording { raw_id: b, .. }) => { a == b } + (Self::StartPlot { person: a, .. }, Self::StartPlot { person: b, .. }) => a == b, _ => self == other, } } @@ -297,6 +298,37 @@ mod tests { ); } + #[test] + fn alternate_plots_for_one_person_share_a_single_pending_slot() { + let mut state = OperationsState::default(); + state.enqueue( + OpsJobKind::StartPlot { + person: 0, + plot_id: "marcus-debt-settled".into(), + }, + 1.0, + 0, + 1, + ); + + assert_eq!( + state.has_conflict(&OpsJobKind::StartPlot { + person: 0, + plot_id: "marcus-payroll-garnishment".into(), + }), + Some(1), + "picking a second route must not queue a concurrent plot for the same person" + ); + assert_eq!( + state.has_conflict(&OpsJobKind::StartPlot { + person: 1, + plot_id: "dana-ticket-zero".into(), + }), + None, + "different people may carry independent plots" + ); + } + #[test] fn visible_demand_must_cover_the_payload_ledger() { let mut state = OperationsState::default(); diff --git a/wiki/mechanics/plots.md b/wiki/mechanics/plots.md index 35310abe..b559ac33 100644 --- a/wiki/mechanics/plots.md +++ b/wiki/mechanics/plots.md @@ -25,6 +25,11 @@ Status note: direction adopted 2026-07-10 from Cameron's response to the HAL the authoring skill at .agents/skills/plot-author/SKILL.md (mirrored to .claude/skills/) encoding the contributor contract and the propose-on-a-branch canon-gate landing flow. + 2026-07-11 queue-integrity repair: a person has one plot slot from + Operations submission through ending. Selecting one authored route disables + every alternate route for that person while its docket is pending, so two + choices cannot become concurrent runs merely because neither docket has + completed yet. Stage: B1 — The Basement Work order: plots Work priority: 60 @@ -107,6 +112,12 @@ agents and the community can contribute libraries of them. terminal and Bevy frontends, and an `actions ` entry in agent mode. The guidance line may point at a held plot. No new modal dialog system is introduced. +- **One plot slot per person.** Submission reserves the target's slot before + Operations begins paying the docket. While that docket is pending, alternate + plots for the same person remain visible but disabled; once the run begins, + its progress or held choices replace the start rows. A different person may + carry an independent plot. The slot clears only when the pending submission + is rejected or the run reaches an ending. - **Templating.** Plot text carries `{target}` and `{persona}` placeholders filled at playout, so one authored plot serves multiple targets and personas where its leverage contract permits reuse.