You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
"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:
/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.
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.
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.
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.
The complaint
Two separate gaps sit behind that sentence:
/runsnever updates itself.Runs.tsxcallsgetRuns()once in auseEffectand neveragain. Reviews do not work this way —
/api/ws/reviewspushes changes intospire-ui/src/useLiveReviews.ts.M3 names Work items and Approvals, M4–M6 name no screen.
techdebt/spire-ui/4-3-three-factory-surfaces-still-have-no-screen.mdasks for controls on thelist, 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/#/runshttps://app.warren.run/#/runs/run_5qcqvpvzyat8What 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
V43__factory_run.sqlleaves the prompt out on purpose; DATA-MODEL §5 keeps work-item text out of a queryable read model. The card is titled Task.curlfor now. Tracked separately.Status — re-reviewed 2026-09-11 against PR #149 (
2b6d159) — GOEvery sub-task is implemented. Verified by re-running the suites locally, not by reading the PR
description:
npx vitest run613 passed across 75 files;:spire-orchestrator:testfor the factorypackage BUILD SUCCESSFUL, with
EveryRunWriteIsBroadcastTest1,RunsBroadcasterTest3,RunAgentStartedTest5,RunDetailReadTest9,RunsSocketTest11 andRunChargesTest13, nofailures.
All four review findings were fixed in
2b6d159and every thread is resolved: the newest-200 filterwindow is now disclosed and enforced,
exists(runId)replaced the spend-aggregating existencechecks, an unserialisable row warns instead of going silent at DEBUG, and the nit is documented.
Re-verified after the fixes:
npx vitest run617 passed across 75 files; the orchestrator factoryand 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 thecombined catch fails only
anUnserialisableRowWarnsWithoutFailingTheProjection. CI is green on everyrequired check including
service tests + packaging, which closes the one thing the PR descriptioncould 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
Closeskeywords close these issues when itlands 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
useAttention.ts:82records why the attention panel stopped using atimer: an operator fixed a cause and then waited for the next tick to see it.
locally. A run that changes into the active filter then appears on its own, which a server-side
filter cannot do.
/runs/*), not/runs/:runId. A run id contains/and:. Asingle-segment parameter 404s every run that exists —
RunTranscriptSocket's javadoc records thisexact bug shipping once already.
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:
refusedoncerendered as five green segments. Reuse
runStatusLabel/runStatusPill/isRunUnfinished.ResultSet.wasNull()reports on the last column read, not the column you name. This shipped onPR 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.
unit test. Seven instances so far; one was completely silent.
unstyled behind a green suite.
styles.contract.test.tsnow fails the build for it.Three times in
Runs.test.tsxalone.Done when
/runsupdates with no refresh: a run appears when queued, moves through its statuses, andgains its pull-request link, all while the page sits open.
#/runs/<id>opens for a run id containing/and:.a live event stream with working filters and a tail toggle.
—, never$0.000../gradlew testFast testServicesgreen;cd spire-ui && npm testgreen;npx tsc --noEmitsilent.
Not in this epic
techdebt/spire-ui/4-3-three-factory-surfaces-still-have-no-screen.md).