Skip to content

Epic DAG live view

kew serve renders the epic driver's own record as a live, replayable dependency graph — the flight-recorder model applied to kew epic run/advance instead of a single dispatch. Two pages:

  • /epics — a list of every epic the driver has journaled at least one tick for: ref, tri-state signal, done/failed counts, drain spend, and how long ago the last tick landed. This is navigation, not a dashboard — one epic per page, no fleet-wide composite view.
  • /epics/<ref> — the graph for one epic, in live mode while a driver is running it, or replay mode over a completed drain's full journal history.

Both pages are read-only, like the Runs view: no dispatch, merge, retry, or stop affordance lives here. A HELD node's pane deep-links to /inbox for approval context; a FAILED node's pane shows the kew epic retry <node> command rather than a button that would run it.

What feeds the page: the tick journal

Everything rendered here — signal, node statuses, counts, frontier, drain spend — is read from the epic_ticks/epic_driver_state tables the driver itself journals every tick (see kew epic: Tick journal). The page never re-derives status from GitHub on its own; it shows the driver's own record, so there is no way for "what the driver did" and "what the page shows" to disagree. An epic the driver has never ticked shows an honest empty state rather than a fabricated graph.

The honesty model: two layers, updated differently

The page draws two visually distinct layers on top of the same graph, and they are not on the same clock:

  • Status colors (a node's fill, the frontier ring, HELD/FAILED treatment) change only on journal frames — i.e. only when the driver itself observed and recorded a transition. Every status-colored pixel traces to a journaled, audited record.
  • The live-telemetry layer (an activity dot on IN_PROGRESS nodes, the in-flight cost readout) updates on the SSE poll cadence, independent of whether the driver has journaled anything new. It is visually distinct from status color and labeled as live telemetry, not as a driver observation.

Tick cadence moves the graph; run cadence moves the money. Between transitions you see proof of life — the telemetry layer, the "tick N · change M · Xs ago" header — never fake motion invented to make the page look busier than the driver actually is.

Live vs. replay

  • Live subscribes to GET /api/epics/<ref>/stream (SSE): the server fans out snapshot / economics / heartbeat frames from a shared poller reading dispatch.db, at most one poll per second per driven epic regardless of how many browser tabs are open. A reconnect (including the server's periodic stream cap) always re-sends the latest frames rather than replaying history — the live view never backfills missed intermediate actions; only replay does that.
  • Replay fetches the full journal via GET /api/epics/<ref>/ticks and steps through it locally. Pacing is frame-indexed, not wall-clock: changed-only journal frames are wildly irregular (a multi-hour HELD plateau, then a merge cascade), so linear time-compression would render most of a drain as dead air. The default preset dwells roughly 30s / frame_count per frame (clamped to a sane per-frame range). The wall clock is mandatory UI in replay — each frame shows its tick_ts and elapsed time since the drain started, and the scrubber's tick marks are spaced by real elapsed time, not by frame index, so a clock jump between two adjacent frames is visible rather than hidden behind evenly spaced marks.

Drain-spend semantics

The header's spend figure and each node's cost badge are drain-scoped, not lifetime: they sum runs rows with finished_at on or after the epic's first journal row, so a node's cost badge doesn't inflate with unrelated manual dispatches from before the drain started. Replay frames apply the same window, upper-bounded at each frame's own tick_ts — so the first replay frame's spend reads near zero, as it honestly should, and the last frame matches the epic's true total drain spend.

Lifetime issue spend (all runs ever recorded against that issue number, drain or not) is shown only in the node detail pane, explicitly labeled as lifetime — never conflated with the drain-window headline number.

Staleness, complete, and stalled banners

The header banner is signal-aware and mode-aware so it can't lie in either direction:

Situation Banner
Driver alive, ticking normally Advancing "tick N · change M · Xs ago" counter, no judgment
Driver process gone mid-drain "driver last seen …" staleness banner once the tick-age threshold is exceeded
Epic complete "COMPLETE · finished <ts>" — never a stale banner, even if the page hasn't been reloaded in hours
Epic stalled A stall banner plus the stuck nodes' reasons — distinct from "driver gone," since a stalled driver exited by design
One-shot/cron driver (interval_s is null) "last driven Xs ago (one-shot driver)" — no alive/gone claim is made, because none is possible
Replay mode The live staleness header is replaced entirely by the frame's own wall clock

Auth under --expose

The DAG page has no login UI of its own. It reuses the same session cookie the Inbox mints from the /inbox#token=… flow:

  • On loopback without --expose, reads work ungated, matching the rest of the read surface.
  • Under --expose, every /api/epics* route requires a valid session (the router-level read_guard pattern — no endpoint on this router can be added without it).
  • Any 401 redirects the browser to /inbox, which handles both a fresh #token= fragment and an existing session, then returns.
  • No epics route ever accepts a token as a query or fragment parameter. The token only ever travels once, in the /inbox fragment, exactly like every other authenticated surface in kew.

See Security: Approval inbox and kew serve: Authentication for the full model this page inherits.

Recording a demo

The page is its own capture pipeline — no export code, just a screen recorder over real infrastructure. Two scripts (run from the kew/ package dir) reproduce the canonical scripted drain end-to-end:

# 1. Generate a completed demo drain (9 journaled ticks, HELD + FAILED beats)
uv run python scripts/make_demo_epic.py --out /tmp/demo-src

# 2. Point a scratch project's [logging].dir at an empty logs dir, start
#    `kew serve` there, and open /epics in a browser.

# 3a. Replay capture: copy /tmp/demo-src/dispatch.db into the logs dir,
#     open /epics/demo-epic, switch to replay, press play, record.

# 3b. Live capture: leave the logs dir empty and perform the drain in
#     real time instead — statuses advance on screen as rows land:
uv run python scripts/drip_demo_epic.py /tmp/demo-src/dispatch.db \
    <scratch-project>/logs/dispatch.db --interval 6

The drip script shifts every timestamp so the drain reads as happening now (fresh heartbeat, live-accumulating drain spend) and recomputes each snapshot hash after the rewrite, so the journal it produces stays internally truthful. Record with the OS screen recorder (macOS: Cmd-Shift-5); the 30-second pitch is the replay at default pacing.

Known limitation: epic refs containing /

epic_ref is routed as a plain path segment (/epics/<ref>), validated against a conservative charset before use. GitHub label names can contain /, and such an epic ref is unroutable in v1 — the driver still drives it fine; only the web page 404s on it. There is no slug-encoding scheme yet; this is a known, accepted gap rather than an oversight.

  • kew epic — the driver commands and the tick journal this page reads.
  • Web UI overview — the cockpit shell this page sits alongside.
  • Security: Approval inbox — the auth model this page inherits under --expose.
  • Design spec: docs/superpowers/specs/2026-07-01-epic-dag-live-view-design.md