From 4ecfc62fc50d51ceaf785ebd8bb186536d2d0436 Mon Sep 17 00:00:00 2001 From: Zach Guo Date: Mon, 31 Aug 2026 19:12:20 +0800 Subject: [PATCH] docs(roadmap): five lines the radio follows next, and where to read them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The open issues had no main line through them, so the ones that mattered sat unread beside the ones that did not. This adds the direction layer the backlog was missing: ROADMAP.md, five ordered lines with what each is and what this round delivers, linked from the README's Status section. Two goals held at once this round — good enough that its author leaves it on, and runnable by someone who is not its author. Local TTS is recorded as out of scope. Line 0 absorbs issue #145 (the listener's first typed line is silently dropped): repro table, the suspected seam, and the done-when move into the roadmap and the issue closes, so the bug is somewhere it will be read. STATUS.md loses that pointer and gains one line saying direction lives in the roadmap and the card holds only the current focus. Peer review (codex gpt-5.6-sol, xhigh): 3 findings, 2 applied, 1 dismissed. --- README.md | 3 + ROADMAP.md | 188 ++++++++++++++++++++++++++++++++++++++++++++++++ specs/STATUS.md | 9 +-- 3 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 ROADMAP.md diff --git a/README.md b/README.md index efd264d..70c1de7 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ What remains is acceptance **by ear** — pacing over a real day, onboarding in a real terminal, how the steering feels — plus a few engineering debts. The live tracker is [`specs/STATUS.md`](specs/STATUS.md). +Where it goes next — the five lines of work this round, in order — is +[`ROADMAP.md`](ROADMAP.md). + ## Requirements - Node.js ≥ 24 and **pnpm** (`corepack enable pnpm`, or `brew install pnpm`) diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..809166a --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,188 @@ +# murmur — roadmap + +_Where the radio goes next, in order. Five lines; a line is deleted when it is +done, not archived. This is the **direction** layer: the current build focus +lives in [`specs/STATUS.md`](specs/STATUS.md). Where a line names issues, the +evidence and close condition live there; line 0 carries its own, having been +folded in from a closed issue._ + +_Last updated: 2026-08-31._ + +**This round has two goals at once, and they do not conflict:** murmur should +be good enough that its own author leaves it on, *and* runnable by someone who +is not its author. Local TTS is **explicitly out of scope** this round — the +hosted voice stays. + +| # | Line | What it is | This round delivers | Tracked as | +|---|---|---|---|---| +| 0 | Foundations | Land the work already written, and stop losing the listener's first line | A clean `main` and an input path that never drops a typed line | PRs in flight; §0 below | +| 1 | Sound like a DJ | Talk and music actually interleave, instead of alternating at boundaries | A track gets a lead-in, not a label; the host can speak over a ducked song | [#163](https://github.com/wine-fall/murmur/issues/163) + new | +| 2 | Say real things | The host gets real material — news, new releases, what is happening near the listener | An off-loop topic pool, weighted by the listener's language and timezone | new (absorbs [#44](https://github.com/wine-fall/murmur/issues/44)) | +| 3 | Pick well, play reliably | Candidates come from sources worth trusting, not from keyword soup | Dead stream probes down; picks back under the spec-04 budget | [#164](https://github.com/wine-fall/murmur/issues/164), [#149](https://github.com/wine-fall/murmur/issues/149) + new | +| 4 | Others can run it, and it does not rot | A second brain backend, and an eval track under the stochastic behavior | murmur runs without a Claude Code login; prompt regressions get caught by a test | [#89](https://github.com/wine-fall/murmur/issues/89), [#98](https://github.com/wine-fall/murmur/issues/98), [#80](https://github.com/wine-fall/murmur/issues/80), [#153](https://github.com/wine-fall/murmur/issues/153), [#102](https://github.com/wine-fall/murmur/issues/102) | + +Lines 1 and 2 are the ones that change what murmur *is*. Line 0 comes first +because every by-ear judgement above it is worthless while the first thing the +listener says disappears. + +--- + +## 0. Foundations + +**Land what is already written.** Several PRs sit green and unmerged. Two of +them — the clock-and-progress enrichment and the beat-grounding fix — were +built by separate sessions against the same seam: both add a time field and a +music-state field to `ContextPack` and both touch `src/contracts.ts`, +`src/director.ts`, `src/prompts.ts` and the same three test files. They must be +**reconciled into one change before either merges**. The suggested base is the +one with the four-state music union and the anti-fabrication rules, taking the +play-progress arithmetic and the spec amendments from the other. + +**The first line a listener types in a session is silently dropped.** Carried +here from issue #145, which is closed in favour of this line. + +It never echoes, never reaches the steer turn, and leaves no dev-log trace. The +second line behaves normally. Reproduced 3/3 on 2026-08-25 against the real +brain, with a pre-seeded `persona.md` so first-run onboarding is skipped, lines +written to stdin after the second beat aired plus a 3 s settle: + +| run | line 1 | line 2 | result | +|---|---|---|---| +| A | "turn the music off please" | "actually, speak Japanese from now on" | only line 2 echoed and acted | +| B | "hey, can you turn the music off? just talk tonight" | — | nothing echoed; no effect | +| C | "turn the music off" | "turn the music off" | only line 2 echoed; `settings.json` got `musicEnabled: false` | + +Run C is the clean isolation — identical text, only the second one lands — so +the defect is **positional**, not content- or intent-dependent. + +Suspected, **not verified**: a pre-broadcast `lineReader` (`src/guide.ts:143`) +left legitimately pending from the boot stretch consumes the first queued line +and discards it. `LineQueue.peek()` (`src/host.ts:108`) memoizes one shared +waiting promise, so a stale reader's callback can win the race and `takeLine()` +before the Director's own boundary race sees it. The `settled` guard +(`src/guide.ts:149-156`) exists for this class of loss, but covers only a read +already resolved through EOF or the quit latch — not a reader still pending +when an unrelated line arrives. + +Not yet investigated: whether the TUI front-end is affected too (all three runs +were `--plain`), and whether a real TTY behaves differently from the piped +stdin used in the repro. + +Spec: `specs/spec01/01-core-loop.md` §3.3 and +`specs/spec03/03-03-guide-harness.md` §3. The contract violated: a typed line +either reaches the Director or is consumed by a reader that is actually asking +something — never dropped. + +Done when a regression test pins it (a line pushed while a pre-broadcast reader +is pending still reaches the Director's steer path, fakes only), and a real +plain-mode run shows the **first** typed line echoing and taking effect. + +## 1. Sound like a DJ + +Two halves of one behaviour. + +**The intro is a label, not a lead-in.** Today the whole introduction is one +optional line on `submit_pick` (`src/music-tools.ts`), and it is spoken *after* +the engine has confirmed the stream is playing (`src/director.ts:826-851`), so +the listener hears a title and the song is already under it. What is wanted is +a beat that arrives at the track — why this one, what it follows — and then the +music. Possibly a back-announce when it ends. This likely means promoting the +announce from a field on the pick to a real talk beat, which touches spec 04 +and spec 03-02 §3.5. + +**Nothing is ever said over a song.** Inside a music segment the host is silent +from the announce to the fade unless the listener speaks first. The engine half +already exists and is in daily use — `Engine.play(voice)` ducks live music for +the clip and pre-schedules the unduck. What is missing is the director asking: +`Director.runVoice` races only the song's end, the listener's next line, and a +due switch. Adding that race arm is the feature — with the staleness rule +issue #163 records, since a buffered beat can be minutes old by the time it +airs. + +## 2. Say real things + +The self-initiated talk task has **no way to learn anything**. It runs through +`runTask` (`src/brain.ts:356-366`), whose allowlist is built from exactly the +tools the caller hands it — and `nextTalks` hands it one, the terminal +`emit_talk_beats` (`src/brain.ts:377`). `agenticOptions` sets `tools: []` and +allows only murmur's own MCP names, so there are no built-in tools underneath +either. The model holds a persona and a transcript and nothing else, so it +invents its topics, and on a cold start it lands on the same few cozy images +every time (the standing complaint in #44). A topic capability therefore has to +arrive as a murmur tool on that task, or already folded into its prompt — +loosening `isolatedOptions` (`src/brain.ts:97`) changes nothing here; that is +the tool-less plain-text path. + +The line: give the host **real material** — news, new releases, what is +happening where the listener is — weighted toward their locale, with +international as the fallback rather than the default. + +Two constraints that shape the design rather than being discovered late: + +- **Off the live loop.** Picks already run 80–190 s in a bad session (line 3); + hanging a search off the talk path would be fatal to the look-ahead. Fetch + into a **topic pool** in `MURMUR_HOME` on a schedule and have talk read from + the pool. This also keeps the spec-07 token economy honest. +- **Region is not stored, and language is not region.** The spoken language is + settled at first run and then owned by the persona, with `settings.language` + able to override it afterwards (`src/app.ts:200-203`, spec 12 §3.9); + `detectLanguage` reads only `LC_ALL` / `LC_MESSAGES` / `LANG` + (`src/locale.ts:44`) and is the boot default, not a live signal. So the pool + has to weight on the **effective** spoken language, read where the host reads + it — and get *where the listener is* from a separate signal. The system + timezone is the cheap one; it should not need a new onboarding question. + +This is also the durable fix for #44: a cold boot stops being identical when +the host actually has something in front of it. + +Cost to accept: this is a **fourth network call**, and the "three network +calls" wording in `specs/DESIGN.md` (already stale, per #104) has to move +again. + +## 3. Pick well, play reliably + +`search_music` is a single keyword query against the provider +(`src/music-tools.ts:41`). There is no notion of a source, so whatever ranks +first is what gets judged, and nothing vouches for it. The observed cost is in +#164: three dead stream probes in half an hour, one pick abandoned to talk, and +every pick in that session running 1.5–3.5× the recorded spec-04 baseline — +because a dead probe costs a whole extra pick round. + +The line: **candidates from places worth trusting** — a channel's uploads, a +curated list — as additional tools beside the open-ended search, so the model +can go somewhere specific rather than fishing. That may also be the fix for the +dead probes, not just a quality improvement. + +#149 (does the pick actually stop repeating) belongs here; it is the by-ear +half of the same question. + +## 4. Others can run it, and it does not rot + +The distribution half and the durability half of the same goal. + +- **#89** — a second brain backend so a machine without a Claude Code login can + still run the radio. The largest single item on this roadmap: a full second + implementation of the `Brain` / `Harness` / `GuideCapable` seam, including + permission routing and streaming input. +- **#98** — the first eval track. Lines 1 and 2 are entirely stochastic + behaviour; without an LLM-in-the-loop test, every prompt edit after them is + unguarded. +- **#80**, **#153**, **#102** — the first-run path a new listener actually + walks: onboarding in a real terminal, quitting mid-onboarding, and the setup + guide's consent rounds. + +--- + +## Not on this roadmap + +- **Local TTS** — the recorded want in spec 02 §3.6 stays recorded. The hosted + voice is the voice this round. +- **The by-ear acceptance passes** ([#79](https://github.com/wine-fall/murmur/issues/79), + [#81](https://github.com/wine-fall/murmur/issues/81), + [#99](https://github.com/wine-fall/murmur/issues/99), + [#138](https://github.com/wine-fall/murmur/issues/138)) — these are a gate, + not a direction. They are meant to be walked in one long real session, not + scheduled as separate work items. +- **Doc debt** ([#104](https://github.com/wine-fall/murmur/issues/104)) and + **watch items** ([#83](https://github.com/wine-fall/murmur/issues/83)) — one + edit each, taken when they are in the way. diff --git a/specs/STATUS.md b/specs/STATUS.md index 37c0102..93f9ab7 100644 --- a/specs/STATUS.md +++ b/specs/STATUS.md @@ -3,13 +3,15 @@ _The single source of truth for "what are we building right now." Read it at the start of any build task. Update it when the focus moves; date-stamp it._ +_Direction and ordering live in [`ROADMAP.md`](../ROADMAP.md); this card is +only the current focus._ + _This file is a **card, not a ledger**: an entry that is done and no longer guides the work gets **deleted**, not archived. History lives in git and PR bodies; measured facts live in the spec they verify._ -_Last updated: 2026-08-25 (index synced against GitHub; the per-spec build -history that used to sit under **Where we are** now lives where it belongs — -each spec's own status header, and git)_ +_Last updated: 2026-08-31 (direction split out into ROADMAP.md; the #145 +input-drop bug moved there as roadmap line 0 and its issue closed)_ ## Where we are @@ -35,7 +37,6 @@ hand: CI fails if this section points at an issue that is already closed. - **#80** (by-ear) First-run onboarding in a real terminal — spec 06 criterion 12. - **#81** (by-ear) A real day of pacing — spec 07 §5.16. - **#83** (watch) Enter during an uncommitted IME composition may submit the line. -- **#145** (bug, eng) The first line a listener types in a session is silently dropped. - **#98** (eng) Steer tool-choice eval (Ollama) owed — the smoke is on-demand only; spec 11 §5. - **#102** (enhancement, eng) The voice guide's live policy check burns ~6 consent rounds before degrading. - **#104** (eng) DESIGN.md still claims fully-local / two hops / Claude-brain — stale vs what shipped.