Skip to content

Epic: live updates on the runs list, and a run detail page #147

Description

@artyomsv

The complaint

"Reviews table and details view provide live updates, but Runs page does not so I was forced to
refresh it with F5"

Two separate gaps sit behind that sentence:

  1. /runs never updates itself. Runs.tsx calls getRuns() once in a useEffect and never
    again. Reviews do not work this way — /api/ws/reviews pushes changes into
    spire-ui/src/useLiveReviews.ts.
  2. There is no run detail view at all. No route, no page. And no later milestone plans one
    M3 names Work items and Approvals, M4–M6 name no screen.
    techdebt/spire-ui/4-3-three-factory-surfaces-still-have-no-screen.md asks for controls on the
    list, not a page.

So the live event stream built in M1/api/ws/runs/transcript,
RunTranscriptSocket, RunTranscriptBroadcaster, all tested — has never had a single consumer,
and nothing on the roadmap would ever give it one.

Reference

Modelled on Warren's runs pages, read live on 2026-09-10:

  • https://app.warren.run/#/runs
  • https://app.warren.run/#/runs/run_5qcqvpvzyat8

What Warren shows

List: filters (State, Agent, Project, Trigger, free-text run id) and columns State · Run · Agent ·
Project · Trigger · Started · Duration · Cost · Delivery (PR / 2 commits / 0 commits / ).

Detail: header (run id, status, delivery chip, agent · project · provider); a 5-phase strip
(Admitted · Workspace ready · Agent running · Reap · Git delivery); tabs Event stream (576 EVENTS)
and terminal; event filters All · Agent · System · Errors · Tail: on; and a right rail of
Runtime, Spend, Run definition and Prompt.

Where we deliberately differ

Warren Us Why
5 phases 3 phases We record 3 timestamps. A phase with no timestamp says "not recorded" — never a guessed duration.
Shows the full prompt Shows the 500-char task summary V43__factory_run.sql leaves the prompt out on purpose; DATA-MODEL §5 keeps work-item text out of a queryable read model. The card is titled Task.
Read-only public instance Read-only Cancel, steer and resolve stay curl for now. Tracked separately.

Status — re-reviewed 2026-09-11 against PR #149 (2b6d159) — GO

Every sub-task is implemented. Verified by re-running the suites locally, not by reading the PR
description: npx vitest run 613 passed across 75 files; :spire-orchestrator:test for the factory
package BUILD SUCCESSFUL, with EveryRunWriteIsBroadcastTest 1, RunsBroadcasterTest 3,
RunAgentStartedTest 5, RunDetailReadTest 9, RunsSocketTest 11 and RunChargesTest 13, no
failures.

All four review findings were fixed in 2b6d159 and every thread is resolved: the newest-200 filter
window is now disclosed and enforced, exists(runId) replaced the spend-aggregating existence
checks, an unserialisable row warns instead of going silent at DEBUG, and the nit is documented.

Re-verified after the fixes: npx vitest run 617 passed across 75 files; the orchestrator factory
and ws suites BUILD SUCCESSFUL at 349 tests, 0 failures. Two mutations were run and each killed
exactly its intended test — dropping .slice(0, MAX_LIVE_RUNS) fails 2 of 36, and restoring the
combined catch fails only anUnserialisableRowWarnsWithoutFailingTheProjection. CI is green on every
required check including service tests + packaging, which closes the one thing the PR description
could not verify locally.

Verdict: GO. The only thing left is a human approving review — branch protection reports
REVIEW_REQUIRED, and the reviewing account is the author, so it cannot approve itself.

Boxes are ticked for delivery, not for merge. The PR's Closes keywords close these issues when it
lands on master; nothing here is closed early.

Sub-tasks

Order. #133 -> #134 -> #135 can run in parallel with each other only after #133 lands (it adds
the column the other two read). #136 needs #134. #137 needs #135 and #136. #138 needs #137.

Decisions already taken

  • WebSocket, not polling. useAttention.ts:82 records why the attention panel stopped using a
    timer: an operator fixed a cause and then waited for the next tick to see it.
  • Filters move client-side. The socket sends the newest 200 rows unfiltered; the screen filters
    locally. A run that changes into the active filter then appears on its own, which a server-side
    filter cannot do.
  • The route is a splat (/runs/*), not /runs/:runId. A run id contains / and :. A
    single-segment parameter 404s every run that exists — RunTranscriptSocket's javadoc records this
    exact bug shipping once already.
  • The detail page refetches on a push rather than receiving a second push shape. One extra GET
    per change on one open page beats two payloads that can drift.

Traps this epic must not step in

Each of these has already cost this project something:

  • A backend status the UI union does not list falls into the success branch. refused once
    rendered as five green segments. Reuse runStatusLabel / runStatusPill / isRunUnfinished.
  • ResultSet.wasNull() reports on the last column read, not the column you name. This shipped on
    PR Close the M2 loop: a run opens a pull request, and /fix lands on it #132 and made the pull-request opener return silently with no log and no row.
  • Adding a component to a record silently drops it at every rebuild site.
  • A saga or projection test fake with an un-overridden method opens a real database from a plain
    unit test. Seven instances so far; one was completely silent.
  • A class the stylesheet does not define renders as browser default. Four screens shipped
    unstyled behind a green suite. styles.contract.test.ts now fails the build for it.
  • A UI assertion like "some dash exists" is satisfied by whichever column happens to be empty.
    Three times in Runs.test.tsx alone.
  • Mutation-verify every guard: break the production line, confirm exactly one test fails.

Done when

  • /runs updates with no refresh: a run appears when queued, moves through its statuses, and
    gains its pull-request link, all while the page sits open.
  • #/runs/<id> opens for a run id containing / and :.
  • The run page shows phases, spend with a token split, the run definition, the task summary and
    a live event stream with working filters and a tail toggle.
  • Unknown cost renders , never $0.000.
  • An unrecognised status renders as unknown, never as green and never as busy.
  • ./gradlew testFast testServices green; cd spire-ui && npm test green; npx tsc --noEmit
    silent.

Not in this epic

  • Cancel, steer and resolve controls (techdebt/spire-ui/4-3-three-factory-surfaces-still-have-no-screen.md).
  • The harness credential pool screen.
  • A terminal tab. Warren has one; we have no equivalent feed.
  • Storing the dispatched prompt.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicA tracking issue spanning several milestonesfactoryThe software factory subsystem (docs/factory)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions