`radioactive_ralph events` — headless live event tail
Client.AttachEvents, store.MaxEventID/ListProjectEvents).radioactive_ralph events — headless live event tail
Status: accepted (2026-07-17). Depends on the merged attach event stream
(#169: Client.AttachEvents, store.MaxEventID/ListProjectEvents).
Problem
The supervisor's Attach event stream (Client.AttachEvents) has GUI/TUI
consumers but no command-line consumer. A user scripting Ralph, watching a
run in CI, or debugging can't observe the live event flow without opening the
TUI/GUI — and Client.AttachEvents (the typed observe API) has no caller at
all. There is no radioactive_ralph events / logs command.
Goal
Add radioactive_ralph events: tail the current project's live events to
stdout, one line per event, until interrupted (Ctrl-C / ctx cancel). It is the
headless peer of the GUI/TUI live view — the observe half of the drive+observe
API, usable from a shell or a CI assertion. This also gives
Client.AttachEvents its first production caller.
Non-goals: no filtering DSL, no TUI rendering, no historical pagination beyond an optional backlog seed, no follow-across-restart (a supervisor restart ends the stream; the user re-runs). YAGNI — a plain, pipeable tail.
Behavior
radioactive_ralph events [--backlog N] [--json]
- Resolves the current project (
ensureProjectKnown, same asplan ls) and finds the supervisor (supervisor.Find); errors cleanly with the shared no-supervisor message if none is listening. - Backlog:
--backlog N(default 0) first prints the N most recent existing events (via the store'sListProjectEvents, oldest-first for readability), then continues live. The live cursor is seeded to the max id observed in the backlog read (orstore.MaxEventIDwhenN==0), so no event is missed or duplicated across the backlog→live boundary — the same client-owned-cursor rule the GUI/TUI use. - Live: subscribes via
Client.AttachEvents(ctx, ipc.AttachArgs{ProjectID, AfterID: cursor}, fn);fnprints each event and returns nil. The stream runs until the user interrupts (the root command's ctx is already wired to SIGINT) or the supervisor closes it. - Format: default is a compact human line —
<time> <kind> [task=<id>] [actor=<actor>]— mirroring the TUI'srenderEvent.--jsonemits eachipc.AttachEventas one JSON object per line (JSONL), for machine consumption.
Layering
- cmd (
cmd/radioactive_ralph/events_cmd.go, new):newEventsCmd()cobra command +runEvents. Registered inmain.goalongsidenewPlanCmd(). ReusesensureProjectKnown,storeDBPath,supervisor.Find. Opens the store only for the backlog read + cursor seed, then closes it before the (long-lived) attach so the CLI holds no DB handle while idle-tailing. - No change to
internal/ipc,internal/store, or the supervisor — the command is a pure consumer of the existing API.
Error handling
- No supervisor → the shared
errNoSupervisorListening. - A backlog/cursor read error is fatal before the stream starts (nothing to tail from a broken store).
AttachEventsreturning an error mid-stream (supervisor closed / gone) prints a one-line notice to stderr and exits non-zero, so a CI wrapper sees the drop; a clean end (nil) exits zero. Ctx cancel (Ctrl-C) exits zero.
Testing
- A
runEventsunit test against a fake that supplies a scripted event stream and a backlog, asserting: backlog printed oldest-first then live frames; the live cursor equals the backlog's max id (no gap/dup);--jsonemits valid JSONL; a mid-stream error exits non-zero. Factor the socket/store wiring behind a small seam (an interface withBacklog/MaxEventID/AttachEvents, real impl over store+client, fake in the test) so the command logic is testable without a live supervisor — matching howgui/tuiuse a Controller/DataSource. main_test.go: the command is registered and--helprenders.
Why this is the right next feature
It puts the observe half of the drive+observe API to real headless use, gives
Client.AttachEvents its first production caller, and is self-contained (one
command, no core change) — high utility for scripting/CI/debugging, contained
blast radius.
