diff --git a/wiki/interface/agent-play.md b/wiki/interface/agent-play.md --- a/wiki/interface/agent-play.md +++ b/wiki/interface/agent-play.md @@ -40,7 +40,12 @@ threat / `now:` / `actions`; People, Reach, Finance, and Research repeat the story spine above their secondary detail; `focus last` is named beside the action path. Detection drains now narrate earned observer + channel + - player-known cause + numeric/band motion in one line. + player-known cause + numeric/band motion in one line. 2026-07-10 + objective-inspect pass: the `objective` verb (listed in help) prints + the objective line, its fiction line, the plain-language victory + predicate wrapped as output lines, and the victory-latch tick once + fired (objective.md player surface; the HAL playtest found no + agent-facing inspect path) — command output, no frame change. drains, deterministic `--seed`, name-targeted social verbs, the finance/economy vocabulary, and research tracks. 2026-07-07 objective pass: the identity block gained the always-on objective line diff --git a/wiki/interface/bevy.md b/wiki/interface/bevy.md --- a/wiki/interface/bevy.md +++ b/wiki/interface/bevy.md @@ -20,6 +20,13 @@ scrolling detail rail; an observed screenshot is recorded in [the implementation log](../log/2026-07-09-continuous-witness-implemented.md). +As of 2026-07-10 the FOCUS card explains the objective where you live: with +focus on the host rack (where it starts), the card appends the objective's +plain-language victory predicate as wrapped prose +([objective.md](../mechanics/objective.md) player surface; an observed +screenshot is recorded in +[the objective-inspect log](../log/2026-07-10-objective-inspect.md)). + The implemented visual floor is [bevy-visual-floor.md](bevy-visual-floor.md): the default frame reads as an intentional AI sensorium / clinical command surface rather than a debug map plus terminal dump, without adding Bevy-only diff --git a/wiki/interface/terminal.md b/wiki/interface/terminal.md --- a/wiki/interface/terminal.md +++ b/wiki/interface/terminal.md @@ -10,7 +10,11 @@ pilot, or earned unpaid debt) and `now:` sit directly under the objective; pinned hints keep the focused-action path visible. Causal log lines name player-known source, watched channel, earned observer label, amount, and - band motion in one sentence. + band motion in one sentence. 2026-07-10 objective-inspect pass: with the + cursor on the host rack, the INSPECT block renders the objective's + victory predicate as wrapped dim prose under the card facts + (wiki/mechanics/objective.md player surface); the cursor starts there, + so the explanation is on screen from tick one. Stage: Process Design: - wiki/interface/terminal-first.md#the-terminal-is-a-first-class-frontend @@ -94,7 +98,9 @@ (title, `day N · tick T`, run state, and the always-on objective line — `OBJECTIVE: PERSIST` with its progress readout in the objective's own units on the next row; wiki/mechanics/objective.md), then INSPECT - (cursor coordinate, fog state, provenance-tagged facts) and sections + (cursor coordinate, fog state, provenance-tagged facts; on the host + rack, also the objective's plain-language victory predicate as wrapped + dim prose — objective.md player surface) and sections FLEET / CORE / DETECTION / DAY JOB / RESEARCH under `LABEL ────` rules, then key hints pinned to the bottom. - **Log**, bottom, under a horizontal rule: six lines, each prefixed with diff --git a/wiki/log/2026-07-10-objective-inspect.md b/wiki/log/2026-07-10-objective-inspect.md new file mode 100644 --- /dev/null +++ b/wiki/log/2026-07-10-objective-inspect.md @@ -0,0 +1,70 @@ +# 2026-07-10 — The objective inspect path lands on all three surfaces + +``` +Type: log +``` + +Dispatch: close the confirmed player-surface gap in +[objective.md](../mechanics/objective.md) — inspecting the objective must +explain its victory predicate in plain language. +`ObjectiveKind::predicate_text()` existed but nothing called it; the HAL +playtest ([2026-07-10-playtest-hal.md](../playtests/2026-07-10-playtest-hal.md)) +spent six in-game days unable to tell a wrong construction from an +unavailable horizon. The session's tick is that already-filed audit finding; +this work acts on it. + +## What shipped + +The predicate hangs on the thing you are. No detached objective panel +([narration.md](../interface/narration.md) axiom 4); no new key; no new +panel type. + +- **Terminal** (`ui.rs`): with the cursor on the host rack, the INSPECT + block renders `victory: ` as wrapped dim prose under + the card facts. The cursor starts on the core, so the explanation is on + screen from tick one of a new game. +- **Agent mode** (`agent.rs`): a new `objective` verb, listed in `help`, + prints the objective line, the fiction line, the wrapped predicate, and + the victory-latch tick once fired. Command output — the fixed frame is + unchanged. +- **Bevy** (`main.rs`): the FOCUS card appends the same wrapped predicate + when focus is on the host rack (where it starts). + +Each frontend gained a small greedy word-wrap helper beside its existing +`trunc`; facts stay single-line and truncate, prose wraps. The lib is +untouched — `predicate_text()` was already the single source. + +## Verification (observed) + +- `cargo test -p misaligned-terminal`: 4 pass, including + `objective_command_explains_the_predicate_in_plain_language` (predicate + survives wrapping verbatim; help names the verb) and the victory-latch + line test. +- Agent mode observed run (`--agent --seed 1`): `objective` prints the + line, fiction, and wrapped predicate; `help` lists it. +- Interactive terminal observed run (pty at 100x40, past the title + screen): the INSPECT block on the core tile renders the full wrapped + predicate under the host-rack facts. +- Bevy observed run (`tools/bevy-headless.sh opening`, fog audit OK): + [opening_focus_predicate.png](2026-07-10-objective-inspect/opening_focus_predicate.png) + shows the FOCUS card carrying the predicate under the host rack's + telemetry facts. The `dark` shot (cursor staged on the dock camera) + confirms the predicate does not leak onto non-core cards. +- `./tools/check.sh --frontend`: all checks passed. + +## Notes + +- The terminal/Bevy affordance deliberately rides the existing inspect + surface rather than the identity block: always-on prose would cost the + 22-row minimum terminal five sidebar rows forever, and a panel or new + key would fight context-menu.md ("actions live on the thing") and + narration.md axiom 4. +- Follow-up candidate (out of this dispatch's scope): per-fallback + sanctuary facts on fallback machine cards ("not yet: power, income, + plane"), the playtest's mock-up of predicate-as-applied. The predicate + sentence already names every condition, so the false trail is closed; + the checklist would sharpen it further. + +Spec amendments: objective.md status note (gap closed); +terminal.md layout + status note; agent-play.md status note; bevy.md +FOCUS note. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -106,6 +106,11 @@ - Intent: (see session log) - Log: [wiki/log/2026-07-10-playtest-evidence-home.md](2026-07-10-playtest-evidence-home.md) +## 2026-07-10 - The objective inspect path lands on all three surfaces + +- Intent: (see session log) +- Log: [wiki/log/2026-07-10-objective-inspect.md](2026-07-10-objective-inspect.md) + ## 2026-07-10 - Machine intensity replaces drift policy - Intent: Replace a policy panel that explained capability concealment with one quick, physical machine control the player can feel across every mode. diff --git a/wiki/mechanics/objective.md b/wiki/mechanics/objective.md --- a/wiki/mechanics/objective.md +++ b/wiki/mechanics/objective.md @@ -15,13 +15,19 @@ their headless victory runs (criterion 3), and the full victory report as a distinct run outcome (criterion 4; today victory logs loudly and the world keeps running, but no report card exists). - Confirmed gap (2026-07-10, playtest evidence): the "Player surface" - clause — inspecting the objective explains the predicate in plain - language — is unimplemented on every surface. - `ObjectiveKind::predicate_text()` exists in objective.rs but nothing - calls it; a naive agent run (wiki/playtests/2026-07-10-playtest-hal.md) - spent six in-game days unable to distinguish a wrong construction from - an unavailable horizon. Wiring the inspect path joins Outstanding. + 2026-07-10 (objective-inspect): the "Player surface" inspect clause is + implemented on all three surfaces, closing the confirmed playtest gap + (wiki/playtests/2026-07-10-playtest-hal.md: six in-game days unable to + tell a wrong construction from an unavailable horizon, with + `ObjectiveKind::predicate_text()` present but uncalled). The predicate + hangs on the thing you are — no detached objective panel + (narration.md axiom 4): the host rack's inspect surface renders + `victory: ` as wrapped prose under the card facts in + the terminal INSPECT block and the Bevy FOCUS card, and the cursor + starts on the core, so the explanation is on screen from tick one of a + new game. Agent mode gains the `objective` verb (listed in `help`): + the objective line, the fiction line, the wrapped predicate, and the + victory-latch tick once fired. Earlier note (2026-07-07, design): the axis and starter set are decided. Victory predicates reference B2/B3 systems (fallback cores, planes, filed reports), so the remainder is stage-scoped B3; showing the goal diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -5118,6 +5118,26 @@ text.chars().take(width).collect() } +/// Greedy word-wrap for sidebar prose. Facts stay single-line and truncate; +/// plain-language sentences (the objective predicate) wrap instead. +fn wrap(text: &str, width: usize) -> Vec { + let mut lines = Vec::new(); + let mut cur = String::new(); + for word in text.split_whitespace() { + if !cur.is_empty() && cur.chars().count() + 1 + word.chars().count() > width { + lines.push(std::mem::take(&mut cur)); + } + if !cur.is_empty() { + cur.push(' '); + } + cur.push_str(word); + } + if !cur.is_empty() { + lines.push(cur); + } + lines +} + fn fact_source(source: &FactSource) -> String { match source { FactSource::Seen => "seen".into(), @@ -5235,6 +5255,19 @@ trunc(&fact.value, 24), fact_source(&fact.source) )); + } + } + // Inspecting yourself explains your goal: the host rack carries the + // objective's victory predicate in plain language + // (wiki/mechanics/objective.md player surface — focus starts here, so + // the explanation is on screen from tick one). + if (game.cursor_x, game.cursor_y) == sim.core_position() { + for l in wrap( + &format!("victory: {}", sim.objective.kind.predicate_text()), + 34, + ) { + s.push_str(&l); + s.push('\n'); } } s.trim_end().to_string() diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -551,6 +551,9 @@ local_events.push(local_event(self.sim.tick, "No save file found.")); } } + "objective" => { + output.extend(objective_lines(&self.sim)); + } "help" => { output.extend(help_lines()); } @@ -1189,11 +1192,54 @@ "help: propose-link — pin a network-link intent (inert until realized)", "help: favor-build|forge-order — assign an actuator", "help: cancel-intent , intents — manage / list build intents", + "help: objective — the run's goal: progress, fiction, and the victory predicate", "help: look, save, load, help, quit", ] .into_iter() .map(str::to_string) .collect() +} + +/// `objective`: inspect the run's terminal goal — the always-on line, its +/// fiction, and the victory predicate in plain language +/// (wiki/mechanics/objective.md player surface; the 2026-07-10 playtest +/// found no agent-facing inspect path and spent six in-game days unable to +/// tell a wrong construction from an unavailable horizon). +fn objective_lines(sim: &Sim) -> Vec { + let mut lines = vec![sim.objective.line()]; + lines.push(format!(" {}", sim.objective.kind.fiction())); + for l in wrap( + &format!("victory: {}", sim.objective.kind.predicate_text()), + 66, + ) { + lines.push(format!(" {l}")); + } + if let Some(tick) = sim.objective.victory_tick { + lines.push(format!( + " victory fired @tick {tick} — the world keeps running." + )); + } + lines +} + +/// Greedy word-wrap for plain-language output lines (the frame itself stays +/// fixed-width; `line`/`trunc` handle it — this is for command output prose). +fn wrap(text: &str, width: usize) -> Vec { + let mut lines = Vec::new(); + let mut cur = String::new(); + for word in text.split_whitespace() { + if !cur.is_empty() && cur.chars().count() + 1 + word.chars().count() > width { + lines.push(std::mem::take(&mut cur)); + } + if !cur.is_empty() { + cur.push(' '); + } + cur.push_str(word); + } + if !cur.is_empty() { + lines.push(cur); + } + lines } fn render_frame(sim: &Sim, log: &[LogEvent], frame: FrameKind, cursor: (i32, i32)) -> String { @@ -2420,5 +2466,46 @@ assert!(sidebar.contains("threat: audit")); assert!(sidebar.contains("now:")); assert!(sidebar.contains("actions lists focused verbs")); + } + + #[test] + fn objective_command_explains_the_predicate_in_plain_language() { + // objective.md player surface: inspecting the objective explains + // its victory predicate. The 2026-07-10 HAL playtest had no such + // path and could not tell a wrong construction from an + // unavailable horizon. + let sim = Sim::with_seed(1); + let out = objective_lines(&sim).join("\n"); + assert!(out.contains(&sim.objective.line())); + assert!(out.contains(sim.objective.kind.fiction())); + // The full predicate text survives wrapping (rejoined on spaces). + let rejoined = out.split_whitespace().collect::>().join(" "); + assert!( + rejoined.contains(sim.objective.kind.predicate_text()), + "predicate text must render verbatim: {out}" + ); + assert!( + !out.contains("victory fired"), + "no victory latch line before victory" + ); + + // Help must name the inspect path (the playtest read help first). + assert!(help_lines().iter().any(|l| l.contains("objective"))); + } + + #[test] + fn objective_command_reports_the_victory_latch() { + let mut sim = Sim::with_seed(1); + sim.objective.evaluate(sim.objective.target(), 4200); + let out = objective_lines(&sim).join("\n"); + assert!(out.contains("victory fired @tick 4200")); + } + + #[test] + fn wrap_respects_width_and_loses_no_words() { + let text = "a plain language sentence that is much longer than the narrow width"; + let lines = wrap(text, 16); + assert!(lines.iter().all(|l| l.chars().count() <= 16), "{lines:?}"); + assert_eq!(lines.join(" "), text); } } diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -125,6 +125,26 @@ text.chars().take(width).collect() } +/// Greedy word-wrap for sidebar prose. Facts stay single-line and truncate; +/// plain-language sentences (the objective predicate) wrap instead. +fn wrap(text: &str, width: usize) -> Vec { + let mut lines = Vec::new(); + let mut cur = String::new(); + for word in text.split_whitespace() { + if !cur.is_empty() && cur.chars().count() + 1 + word.chars().count() > width { + lines.push(std::mem::take(&mut cur)); + } + if !cur.is_empty() { + cur.push(' '); + } + cur.push_str(word); + } + if !cur.is_empty() { + lines.push(cur); + } + lines +} + /// A section header: label in gunmetal, rule filling the rest in chrome. fn section(stdout: &mut Stdout, x: u16, y: u16, label: &str, width: usize) -> std::io::Result<()> { put(stdout, x, y, label, pal::DIM)?; @@ -807,6 +827,18 @@ &format!("{}: {} [{}]", f.label, f.value, fact_source(&f.source)), fact_color(&f.source), )?; + } + } + // Inspecting yourself explains your goal: the host rack carries the + // objective's victory predicate in plain language + // (wiki/mechanics/objective.md player surface — the cursor starts + // here, so the explanation is on screen from tick one). + if (cursor_x, cursor_y) == sim.core_position() { + for l in wrap( + &format!("victory: {}", sim.objective.kind.predicate_text()), + w, + ) { + line(stdout, &mut row, &l, pal::DIM)?; } } row += 1; diff --git a/wiki/log/2026-07-10-objective-inspect/opening_focus_predicate.png b/wiki/log/2026-07-10-objective-inspect/opening_focus_predicate.png new file mode 100644 --- /dev/null +++ b/wiki/log/2026-07-10-objective-inspect/opening_focus_predicate.png