Skip to content

feat(settings): the listener's knobs — settings.json + /settings pane [spec 12] - #110

Merged
wine-fall merged 6 commits into
mainfrom
zachg-0807--settings-layer
Aug 7, 2026
Merged

feat(settings): the listener's knobs — settings.json + /settings pane [spec 12]#110
wine-fall merged 6 commits into
mainfrom
zachg-0807--settings-layer

Conversation

@wine-fall

Copy link
Copy Markdown
Owner

Implements specs/spec12/12-settings.md (new); amends specs/spec10/10-tui.md §3.3 and specs/DESIGN.md §10 (row 12).

What this is

The settings layer: the listener's knobs, persisted and live.

  • $MURMUR_HOME/settings.json — hand-editable and panel-written; per-key salvage on read (a broken gapSeconds never costs the mute state), atomic temp+rename writes, merged below env and flags per knob at boot. A file-set voice: "stub" carries the same explicit-mute provenance a typed --voice stub does.
  • SettingsStore — the engine-owned single writer: validate → apply live → persist the user-touched keys → notify. Runtime rule: a live change is the newest user intent and wins over a boot flag; layering re-applies at the next boot.
  • Wire, additive (no protocol bump)settings (snapshot after hello on every attach and after every accepted/rejected set; read-only facts: home, endpoint-configured, music-available — never a key) and settingsSet (partial patch; voice can be 'stub' or cleared null, 'hosted' is inexpressible by schema). Snapshots stay out of the replay backlog.
  • Hot application, all seven knobs — gap/memory-span read live per use; mute flips at the synthesis site (provider never torn down); anchors gate at the fire site (scheduler always constructed); the mix gear re-reads per boundary (LiveCadence); music off gates the discovery spend too, and toggling on re-enters through the existing boundary cold path; the pet is a client render toggle.
  • TUI /settings pane — engine-parsed command (plain host gets a pointer line instead); seven intent-labeled items (no every_n/random/brain ever shown; off-preset values render as custom), ↑↓/←→/space, Esc returns focus, broadcast never pauses. tuiPet moves into the layer; MURMUR_TUI_PET survives as the client-local final override.

Evidence

  • 651 unit tests green, tsc --noEmit clean, bare npx oxlint clean.
  • End-to-end smoke over a real unix socket (runApp + scripted fake client): attach → hello+snapshot → /settings → open-flagged snapshot → settingsSet → broadcast echo → settings.json on disk holds exactly the touched keys ({"gapSeconds":4,"voice":"stub"}).

Peer review (codex gpt-5.5, xhigh): 3 findings, 1 applied (music-off still paid prefetch discovery calls — now gated at the spend, regression-tested), 2 dismissed (untracked-files artifact of pre-commit review; schema-invalid patches are malformed messages by design, spec wording tightened).

Owed to the by-ear pass

Pane feel (focus handoff, gear labels, mid-song Esc) — folded into the existing TUI sensory pass rather than a new issue.

AI coding brief

Original request: Design-first task: a settings layer (~/.murmur/settings.json) + TUI settings pane, with exactly seven writable knobs pre-decided by the user (anchors, music, mix gear, gap, mute, pet, memory span + read-only home/endpoint facts) and an explicit non-goal list; open points (schema/precedence, writer, wire shape, hot-vs-restart, focus, spec home) to be settled in-session, then shipped via the standard spec→build→PR flow. Why: the radio's listeners need runtime knobs without learning env vars or field names.

Manual interventions: The user delegated the open-point discussion ("grill it yourself, then decide") after reviewing an agenda of recommendations; the self-grilling overturned two of the assistant's own initial calls (music toggle upgraded from restart-required to hot; whole-file settings parse downgraded to per-key salvage) and both survived into the build. No mid-build redirections.

Retro: Front-loading the frozen-decision list in the task brief (7 knobs, non-goals, hard constraints) made the design phase converge in one round; the one rework loop (away-stream regression from an over-eager prime-and-yield branch) would have been avoided by reading the existing cold-path semantics before "improving" them — trace first, then optimize.

🤖 Generated with Claude Code

wine-fall and others added 4 commits August 7, 2026 13:18
Seven writable intents and not one more: settings.json under env under
flags per knob, an engine-owned single-writer store, two additive wire
messages, and a /settings pane whose focus grab is the listener's own.
Amends spec 10 §3.3 (the pet knob moves into the settings layer) and
notes the sanctioned focus exception; master §10 gains row 12.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
settings.json reads with per-key salvage (a broken gap must not cost the
mute), writes atomically, and merges below env and flags per knob — a
file-set voice carries the same explicit provenance a typed --voice does.
The SettingsStore is the single mutation path: validate, apply live,
persist the touched keys, notify. The wire schemas land beside it: a
patch can mute or clear the voice but can never write 'hosted'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Director reads a live settings thunk instead of captured scalars:
gap and memory span at each use, anchors at the fire site (the scheduler
stays constructed), music at the scheduling site — and off gates the
discovery spend, not just the airtime (peer-review find). LiveCadence
re-reads the mix per boundary; MutableVoice flips mute per utterance
without tearing the provider down. /settings joins /quit as an engine
command: a pane-capable host shows the pane, the plain host gets a
pointer. The IpcHost grows the settings bridge — snapshot after hello on
every attach, store-rejected patches answered with truth, snapshots kept
out of the replay backlog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven items, intent labels only — the gear names a mix, never a mode;
values the presets cannot express render as an honest custom position.
Arrows move and adjust, Esc hands focus back, the broadcast never
pauses; every change rides settingsSet immediately and the pane renders
only what the engine echoes back. The pet follows the live tuiPet
setting, with MURMUR_TUI_PET kept as the client-local final override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wine-fall and others added 2 commits August 7, 2026 13:21
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…§3.4]

The first build muted by routing synthesis to the silent stub — so the
clip on air and the look-ahead's pre-synthesized beats kept speaking
after the toggle: a mute button that does not mute (user find, during
the #111 pass). Re-decided: `muted` is one gain move on the engine's
master bus — instant, mid-word, bus-wide, and the program never notices;
unmute resumes the sentence in flight. The voice/null clearing dance,
the derived-voice thunk, the endpoint greying, and MutableVoice all
dissolve; the `voice` provider knob returns to the dev surface it was.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wine-fall
wine-fall merged commit cbb5554 into main Aug 7, 2026
3 checks passed
@wine-fall
wine-fall deleted the zachg-0807--settings-layer branch August 7, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant