Spec: context menu — actions live on the thing #
Type: spec
Status: IMPLEMENTED
Status note: implemented 2026-07-07 on the context-menu worktree (all
seven criteria). The read-only `Sim::available_actions(anchor) ->
Vec<ActionDesc>` query lives in src/actions.rs as the single legality
source, flattened to `MenuRow`s by `menu_rows` and dispatched back
through existing commands by `Sim::execute_action`; no new sim behavior.
The terminal and Bevy open the menu at the focused tile (Enter/`a`,
right-click in Bevy); agent mode gains `actions [name|#flow]`.
Anchor verbs were demoted off the per-anchor keys and rail/panel button
rows in both frontends; only anchorless globals keep their keys
(pause, speed, save, view flip — DESIGN.md "Actions live on the
thing"). The allocation bar is a read-only fleet aggregate; mode
changes live on the machine. 2026-07-08 follow-up: the leftover panel-open keys `r`/`e`/`t`/`u`
and their modal panels are gone; research and drift-policy verbs live on
the host rack, known-flow verbs hang on the switch, and earned off-map
people hang on the host rack. Status (gap meter, known devices, slush,
roster summaries) stays on the rail.
2026-07-08 addendum implemented (event-to-anchor linking + the
empty-menu feedback pulse, criteria A1-A5 below) on the event-anchor
worktree: `LogEvent.anchor`, `Sim::anchor_position`,
`Sim::menu_empty_feedback`; terminal `;` + `»` markers, Bevy clickable
trace rows, agent `@anchor` suffixes + `focus last`.
2026-07-08 epistemic-honesty follow-up: person verbs and
signature-band observer strings in `ActionDesc` go through
`Sim::person_label` / `Sim::observer_label` (cursor.md criterion 5) —
role silhouettes until Schedule, authored names after.
Stage: B1 — The Basement
Constitution: "Actions live on the thing" (this spec's law),
"Work is somewhere" (the anchors actions attach to),
"Justification and legibility" (every entry shows cost and expected
signature before commit; illegal entries show why),
"Presence: the cursor and the senses" (the menu opens where attention
is), "The terminal is a first-class frontend" (parity),
"Automation as design language" (every automatable verb carries its
automate affordance in the same menu)
Depends on: cursor.md (focus/inspection), reach.md (device anchors),
day-job.md (attended work, the dial), economy.md (flow verbs),
intel.md (tap/process/watch verbs), social.md (person verbs)
Behavior #
The focused anchor — a tile, device, machine, person, resident job, or known flow — exposes its legal verbs as a context menu opened at the point of focus. The menu is the primary action surface; the right rail is status and telemetry only.
- One source of truth: the lib gains a read-only query,
Sim::available_actions(anchor) -> Vec<ActionDesc>, where anActionDesccarries: the verb, its Sim command, cost (compute / money / social pool), the expected signature (as the observer band it feeds, per the economy.md precedent), and — when the verb applies to the anchor type but is currently illegal — adisabled_reasonstring ("no egress channel", "not enough slush", "you can't see them"). Both frontends and agent mode render this list; no frontend duplicates legality logic. - Epistemic honesty: the query never returns a verb the player has
not earned — unearned anchors expose nothing, and provenance rules
(cursor.md) govern what the menu may name. Verb text that mentions a
person, and the observer string on an expected-signature band, use
Sim::person_label/Sim::observer_label— never an unearned authored name. Showing a disabled verb with its reason is for known possibilities; unknown ones are absent, not grayed. - Automation in place: any verb with a standing-policy form (attend -> standing dial, watch, scheme policies) shows its automate affordance as a submenu entry of the same verb, at its compute price.
- Globals stay global: pause, speed, save/load, and view flip keep their keys; they act on no anchor. The fleet aggregate bar is read-only. Everything anchor-specific — including research tracks, drift policy, known-flow verbs, person verbs, and machine mode delegation — lives on the focused anchor's menu. There are no panel-open keys.
Player surface #
- Terminal: cursor + Enter (or
a) opens a compact overlay list at the cursor; j/k or numbers select; Enter executes; esc closes. Entries renderverb cost [band]with disabled entries dimmed plus reason. The host rack's menu carries the day-job dial, research tracks, drift policy, and earned off-map people; a known device's tile (or the switch) carries digital and ledger/flow verbs; a visible person on a tile carries their social verbs. - Bevy: right-click (or Enter on the focused tile) opens the menu at
the pointer; same content, same order, click to execute; esc/click-away
closes. Bevy ASCII-folds the shared
MenuRow::lineseparators (·/—->|/-) because the embedded default font lacks those glyphs; selection is a solid amber wash with a leading>(terminal reverse-video parity). See wiki/interface/bevy.md. - Agent mode: an
actionscommand prints the ActionDescs for the current focus in a stable line format; execution stays the existing verbs. Documented in agent-play.md.
Acceptance criteria #
available_actionsreturns exactly the verbs executable on that anchor now, each with cost, expected signature band, and disabled_reason where applicable; unit tests cover a device, a person, the host rack's resident job, and a known flow.- The query respects fog and provenance: an unearned anchor exposes no verbs, and no entry names a fact the player lacks (test: unseen person, untapped accounting system).
- Terminal renders the menu at the cursor with selection, execution, dim+reason for disabled entries; every previously key-bound anchor action is reachable through it.
- Bevy renders the same menu on right-click/Enter with identical content and order.
- Agent mode
actionslists the same descriptors (stable format, documented); the act-one integration test still passes driven through existing verbs. - No per-anchor action buttons remain on the sidebar/rail in either
frontend; no panel-open keys (
r/e/t/u); global keys are unchanged and listed in README + key hints (pause, speed, save/load, view flip). Fleet modes are delegated on the machine. - Menu entries carrying an automate affordance show it in place with its compute price (day-job standing dial and one intel watch at minimum).
Addendum (2026-07-08): events carry you to the thing #
The follow-up the law implies: if verbs live on the anchor, the events about an anchor should carry your attention to it. Adopted with the 2026-07-08 DESIGN.md extension of "Actions live on the thing".
- Anchored log events.
Sim's log entry (LogEvent) gains an optionalanchor: Option<Anchor>— an additive field, no log rework. The emitter sets it only where it knows the thing: job/core events → the host rack's tile; device verbs (tap/splice/take/compromise/egress splice) → the device; heard events → the person while sight covers them, else the room hearing earned; processed-intel results → the same person rule; ledger lines on known flows → the flow; scheme paydays and wager settlements → the switch when they ride the stolen egress (the sanctioned route hides in legitimate traffic and anchors nowhere); audit/filing events → nothing (they are panel facts with no honest map location). - Epistemic honesty. An anchor never encodes unearned knowledge: a
heard-only person anchors to the room (its center tile), never to the
person or their tile.
Sim::anchor_position(anchor)is the one placement query — a person resolves only while sight covers them, a device at its blueprint-known tile, a flow nowhere (it lives on the ledger). Frontends never re-derive placement. - Focus links. Anchored lines are visibly marked and selectable:
terminal —
»marker;;focuses the most recent anchored event (repeat presses walk older ones): cursor jumps to the anchor, its menu opens; a flow anchor opens its menu without a cursor move. Bevy — the RECENT TRACE rows are buttons; anchored rows carry a>marker and read bone-bright; clicking one moves the cursor (the camera follows) and opens the menu. Agent mode — drained event lines carry a stable suffix@tile(x,y)/@device(id)/@person(id)/@flow(id), andfocus lastjumps the cursor and prints the anchor'sactions:lines (agent-play.md). An anchor the senses can no longer place is refused with a line, never silently. - The feedback pulse. Opening the menu on an anchor with zero rows
answers instead of staying silent — silence read as broken input
(repo-owner report, 2026-07-08).
Sim::menu_empty_feedback(anchor)is the one source: a seen tile answers "No actions here."; an unearned (heard/remembered/blueprint/unknown) tile stays silent — answering would confirm input landed on world the senses have not placed; panel anchors (device/person/flow are known by construction) always answer.
Addendum acceptance criteria #
A1. LogEvent carries anchor; job, device, heard, ledger, and scheme
events set it per the table above; audit events do not (unit tests
cover a job event, a device event, and a ledger flow event).
A2. A heard-only person event anchors to the room, never the person or
their tile, and anchor_position(Person) is None while unseen
(unit test).
A3. Terminal: anchored lines carry »; ; walks anchored events
newest-first, moving the cursor and opening the menu; unanchored
lines are inert.
A4. Bevy: trace rows are clickable; anchored rows are marked >;
clicking focuses the anchor and opens its menu; unanchored rows do
nothing.
A5. Agent mode: event lines carry the stable @anchor suffix and
focus last works, both documented in agent-play.md; the empty-menu
pulse answers on seen tiles and stays silent on unearned tiles in
all frontends (menu_empty_feedback unit test).