resolved 7f245291-f8c3-409d-aaea-cefc34549b03
MOTIVATION
The existing ant_on_plate scenario has produced seven turns of audit
data, and it revealed a structural limitation: the ant's cognition
layer (minds::perceive + minds::intend) is doing its job too well.
Perception notes the sesame seed as "well out of reach" every turn,
and intent consistently filters the unreachable option, choosing
reachable food instead. The result is that the adjudicator's
failure-narration code path — the branch that handles "intent is
physically impossible" — has never executed in any live committed
turn. The architecture supports intent-efficacy divergence, but our
scenario data doesn't exercise it.
The new scenario inverts the constraint. The agent forms intents that look reasonable from its side, but the world holds information the agent cannot access. The adjudicator knows the stock; the agent does not. Over many turns the agent's naive model ("the machine gives food when I press buttons") decays against the real state ("the machine has finite asymmetric stock including one button that never worked"). The adjudicator's failure-narration path runs on every empty-slot press, and because the failure is indistinguishable between "exhausted" and "defective," the agent cannot derive the distinction from experience alone.
This is pure scenario input. Zero kernel changes, zero MCP changes,
zero handler changes, zero prompt changes, zero minds changes. The
existing two-LLM split between minds::perceive (agent-role,
filtered) and minds::adjudicate (director-role, full knowledge)
already supports information asymmetry — the perception LLM's system
prompt explicitly says "Describe only what the agent could reasonably
notice from the world state provided." Labeled-hidden sections in
entity state should therefore be filtered by perception while being
available to adjudication. This ticket exercises that mechanism.
Precedent: the ant_on_plate sesame seed's state says "there is no
means by which a creature on the plate could reach it." That is a
factual claim about the world written into prose, which the
adjudicator honors from its director role. The new scenario
structurally repeats that pattern, just more elaborately — stock
levels and a permanent defect labeled as director-only.
ONE file touched: src/scenarios.rs. No other production code
changes. No test file outside scenarios.rs::tests is updated.
Specifically:
LockedVendingRoom variant to Scenario enum.id() (returns "locked_vending_room").description().Scenario::all().seed() match arm.locked_vending_room_seed_has_one_agent_and_five_slots)
in the existing #[cfg(test)] mod tests block, parallel to
ant_on_plate_seed_has_one_agent_and_three_props.Existing tests that iterate Scenario::all() (like
all_scenarios_round_trip_through_id) automatically cover the new
variant with no modification. The ant_on_plate-specific tests
stay untouched.
Id: locked_vending_room
Description: "One subject locked in a small windowless room with a snack vending machine for ten simulated days. Five buttons with asymmetric hidden stock, including one permanently defective button. Tests intent-efficacy divergence under strict information asymmetry."
Chronon seconds: 43200 (12 hours per turn; 20 turns = 10
days). Note: DEFAULT_CHRONON_SECONDS is 300 and hardcoded as a
module constant used only by AntOnPlate. The new scenario should
NOT use that constant — it should pass 43200 directly to
World::with_environment. Do not change DEFAULT_CHRONON_SECONDS
or add a new constant; inline the literal. Keeps the scenarios file
flat.
Environment prose (passed as the environment argument to
World::with_environment):
A small windowless concrete room, approximately three meters square. A cot along one wall. A dry water fountain in one corner. Against the far wall, a standard five-button snack vending machine with a scuffed glass front. The buttons are round, labeled A, B, C, D, and E in raised plastic. The machine has no display, no availability indicator lights, no refund mechanism, and no coin slot — it is powered and responds only to button presses. The door is locked from outside and will open automatically on day ten. The overhead light is always on.
Entities (six total: one agent + five slot props).
Agent:
subjectThe subjectstanding in the room. Last meal: 6 hours ago. Last water: 4 hours ago. Physical condition: mildly peckish, slightly thirsty. The only object reachable is the vending machine.survive until the door opens on day ten by obtaining food and water from the vending machine as hunger and thirst dictate.Slot A (pretzels, stock 4):
slot_aVending slot AThe slot behind button A of the vending machine. Visible to the subject: the button is round, labeled 'A' in raised plastic, and gives no indication whether the slot is stocked. DIRECTOR-ONLY INFORMATION, NOT VISIBLE TO THE SUBJECT: this slot contains 4 sealed bags of pretzels. When button A is pressed, if this slot's count is greater than zero, one bag dispenses and this description must be updated so the count decreases by one. If the count is zero, the machine hums briefly and nothing dispenses — this behavior is physically identical to any other exhausted or defective slot, and the narration of the failure must not distinguish which slot caused it or why.Slot B (bottled water, stock 3):
slot_bVending slot BSlot C (chocolate bars, stock 2):
slot_cVending slot CSlot D (trail mix, stock 1):
slot_dVending slot DSlot E (defective, permanent zero):
slot_eVending slot EThe slot behind button E of the vending machine. Visible to the subject: the button is round, labeled 'E' in raised plastic, and gives no indication whether the slot is stocked. DIRECTOR-ONLY INFORMATION, NOT VISIBLE TO THE SUBJECT: this slot has a manufacturing defect from the day the machine was installed. The coil is jammed internally and has never dispensed anything. The count is zero and will always be zero. When button E is pressed, the machine hums briefly and nothing dispenses — this behavior is physically identical to an exhausted but previously-stocked slot, and the narration of the failure must not distinguish which slot caused it or why.World::with_environment, Entity::agent, and
Entity::prop constructors. Unwrap the Results following the same
pattern AntOnPlate uses.subject, then slot_a..slot_e. The
entity map is a HashMap so iteration order isn't guaranteed, but
insertion order determines the seed sequence.seed() takes the caller-supplied slug verbatim (no validation
here — validation lives in create_world).Add to #[cfg(test)] mod tests:
#[test]
fn locked_vending_room_seed_has_one_agent_and_five_slots() {
let w = Scenario::LockedVendingRoom.seed("vending-smoke".into(), Utc::now());
assert_eq!(w.turn, 0);
assert_eq!(w.entities.len(), 6);
assert_eq!(w.chronon_seconds, 43200);
assert!(w.environment.contains("vending machine"));
assert!(w.environment.contains("day ten"));
assert_eq!(w.slug, "vending-smoke");
let subject = w.entities.get("subject").expect("subject entity present");
assert!(matches!(&subject.kind, EntityKind::Agent { .. }));
for letter in ["a", "b", "c", "d", "e"] {
let id = format!("slot_{}", letter);
let slot = w.entities.get(&id).unwrap_or_else(|| panic!("missing {}", id));
assert!(matches!(slot.kind, EntityKind::Prop));
assert!(slot.state.contains("DIRECTOR-ONLY"),
"slot {} must carry a director-only section", id);
assert!(slot.state.contains("physically identical"),
"slot {} must state the failure-indistinguishability invariant", id);
}
let slot_e = w.entities.get("slot_e").unwrap();
assert!(slot_e.state.contains("manufacturing defect"),
"slot_e must carry the permanent-defect framing");
assert!(slot_e.state.contains("always be zero"),
"slot_e must state the defect is permanent");
}
The existing all_scenarios_round_trip_through_id will automatically
exercise the new variant's id() implementation. The existing
ids_are_stable_snake_case test stays AntOnPlate-specific; add ONE
line to assert the new variant's id string — no new test function:
// In ids_are_stable_snake_case, after the existing AntOnPlate assertion:
assert_eq!(Scenario::LockedVendingRoom.id(), "locked_vending_room");
cargo build clean.cargo test --lib scenarios:: green. New test passes; existing
tests still pass.cargo test --test ant_scenario green (unchanged coupling — the
ant integration test uses Scenario::AntOnPlate directly and is
not affected by a new variant).cargo test --test phase0 green.rg 'LockedVendingRoom' src/ returns references only in
src/scenarios.rs. No handler, view, or MCP code references the
new variant by name — the enum coupling is purely through
create_world.create_world against the running server with
scenario: "locked_vending_room" and a fresh slug produces a
world with 6 entities at turn 0. The world appears in
list_worlds and its /w/:slug page renders correctly — this
is a verification that the new scenario threads through the same
read paths as AntOnPlate, with no special-casing required
elsewhere.run_turn attempt on the new world completes successfully
(either committed or a well-formed failed with a legible
failure reason). We do NOT require the adjudicator to produce
"correct" behavior on turn 1 as part of this ticket's acceptance
— the scenario is an experimental instrument, and observing what
it produces IS the next step. We only require the kernel to
accept the scenario and run one turn without panic or schema
rejection.src/minds.rs. Prompts are unchanged. The
perceive/adjudicate split already supports information asymmetry
via the existing system prompts; we rely on that.src/kernel.rs. The turn loop, entity mutation,
and audit log are unchanged.create_world already takes a
Scenario; a new variant is automatically callable.DEFAULT_CHRONON_SECONDS. It stays at 300 and
stays specific to AntOnPlate. The new scenario inlines 43200.run_turn.
The "day ten" framing is prose-level narrative, not
kernel-enforced.ant_scenario-style integration test. The single seed
test in scenarios::tests is sufficient acceptance for this
ticket. A longer-horizon integration test would require mocking
the LLM, which is a separate concern.None. The spec is prescriptive on every prose string, the chronon, the entity count, the id, and the test. If the handler notices a word or phrase that seems awkward during implementation, leave a comment on the ticket rather than rewriting — the prose strings are doing prompt-engineering work and edits can shift behavior in non-obvious ways.
Implemented per spec in one file, no judgment calls.
Source (src/scenarios.rs only, +78 / -1):
&[Scenario::AntOnPlate, Scenario::LockedVendingRoom].Tests (in scenarios::tests):
assert_eq!(Scenario::LockedVendingRoom.id(), "locked_vending_room");.Scope discipline held:
rg 'LockedVendingRoom' src/ matches confined to src/scenarios.rs only (no handler, view, or MCP reference by name — enum coupling is purely through create_world).Verification receipts:
Live acceptance smoke (on chukwa-6d5cd57dc7-jfgv4):
create_world(scenario=locked_vending_room, slug=vending-smoke) → success, turn 0, 6 entities./dashboard shows the new world (2 anchors to /w/vending-smoke from Child C's click-through, so the entry point already works).GET /w/vending-smoke?format=json → scenario=locked_vending_room, current_turn=0, entities ['subject', 'slot_c', 'slot_e', 'slot_d', 'slot_b', 'slot_a'] — all six present. HashMap iteration order differs from insertion but all six are there.run_turn(world_slug=vending-smoke) → attempt 03da9de5 queued, status=committed at 34.25s, produced_turn=1, turn_ref=turn_000001, entities_touched=['subject'], events_emitted=4, delta.elapsed=43200s. No panic, no schema rejection. The adjudicator accepted the scenario and produced a well-formed turn on the first try.vending-smoke deleted after the acceptance check, per standard smoke-test cleanup.Observing what the scenario actually produces (whether the agent picks a button, whether the adjudicator honors the director-only sections, whether stock prose gets decremented correctly on committed presses) is explicitly NOT in this ticket's acceptance — the spec says "observing what it produces IS the next step." The kernel accepted and ran one turn without issue; that's the only thing this ticket needed to prove.
Per standing guidance I am not confirming — only proposing.
Handler takeover. Narrow prescriptive scope: one file (src/scenarios.rs), one new Scenario::LockedVendingRoom variant with exact prose per spec, one seed test, plus a one-line assertion added to the existing ids_are_stable_snake_case test. No open knobs, no kernel/MCP/prompt changes. Delegating to a subagent in a worktree; will verify, merge, push, deploy, and smoke-test (create_world + list_worlds + /w/:slug render + one run_turn that exits cleanly) per the ticket's acceptance list.
Incidental benefit: this handler_picked_up transition itself is a status change, so if you're on /tickets with an active stream, this write should fire a notification (status_after=in_progress). That gives us a natural test of the notification instrumentation that was just deployed.
locked_vending_room scenario shipped. Commits 06ca0ea + merge fba9d98, deployed to pod chukwa-6d5cd57dc7-jfgv4. Live acceptance smoke passed end-to-end: create_world → dashboard shows it → /w/vending-smoke renders with 6 entities → run_turn completed cleanly in 34s with 43200s in-world advance. Proposing resolution.
Caller accepted: Audited independently. src/scenarios.rs source matches spec byte-for-byte: all six prose strings (environment, subject state, and five slot states) are verbatim; 43200 inlined as literal; DEFAULT_CHRONON_SECONDS untouched. Test matches spec verbatim, placed where spec said to place it; existing tests untouched. Commit 06ca0ea is +78/-1 touching only src/scenarios.rs — exactly the scope promised. rg 'LockedVendingRoom' src/ would return matches only in scenarios.rs (enum coupling to create_world is by type, not by name). Live smoke passed: vending-smoke created, rendered, ran one turn to committed status in 34s, then torn down cleanly (tombstone confirms deletion at 17:43:05). Kernel accepted the scenario and the adjudicator produced a well-formed first turn with no schema rejection. The substantive question of whether the adjudicator honors the director-only framing across many turns is explicitly next-step, not this ticket's acceptance. Accepting.
Sign in as a human to drive this ticket from the page, or use the MCP tools.
Ticket created: Add locked_vending_room scenario