diff --git a/.fabro/workflows/implement-plan/workflow.fabro b/.fabro/workflows/implement-plan/workflow.fabro
index e1cdc984a..bfa79d6c2 100644
--- a/.fabro/workflows/implement-plan/workflow.fabro
+++ b/.fabro/workflows/implement-plan/workflow.fabro
@@ -17,9 +17,8 @@ digraph ImplementPlan {
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
- verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
- fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
- fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
+ verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
+ fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
@@ -30,8 +29,7 @@ digraph ImplementPlan {
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
- verify -> fmt [condition="outcome=succeeded"]
+ verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
- fmt -> exit
}
diff --git a/Cargo.lock b/Cargo.lock
index 8ee3219ec..7ec0867dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1580,7 +1580,7 @@ dependencies = [
[[package]]
name = "fabro-acp"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"agent-client-protocol",
"agent-client-protocol-tokio",
@@ -1599,7 +1599,7 @@ dependencies = [
[[package]]
name = "fabro-agent"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
@@ -1629,6 +1629,7 @@ dependencies = [
"serde_json",
"sha2",
"shell-escape",
+ "strum",
"tempfile",
"thiserror 2.0.18",
"tokio",
@@ -1639,7 +1640,7 @@ dependencies = [
[[package]]
name = "fabro-api"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"chrono",
"fabro-config",
@@ -1660,7 +1661,7 @@ dependencies = [
[[package]]
name = "fabro-auth"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
@@ -1684,11 +1685,11 @@ dependencies = [
[[package]]
name = "fabro-build-support"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
[[package]]
name = "fabro-checkpoint"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"chrono",
"fabro-config",
@@ -1704,7 +1705,7 @@ dependencies = [
[[package]]
name = "fabro-cli"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"assert_cmd",
@@ -1805,7 +1806,7 @@ dependencies = [
[[package]]
name = "fabro-client"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"bytes",
@@ -1834,7 +1835,7 @@ dependencies = [
[[package]]
name = "fabro-config"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"chrono",
@@ -1862,7 +1863,7 @@ dependencies = [
[[package]]
name = "fabro-core"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"async-trait",
"fabro-types",
@@ -1877,7 +1878,7 @@ dependencies = [
[[package]]
name = "fabro-dev"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"assert_cmd",
@@ -1896,7 +1897,7 @@ dependencies = [
[[package]]
name = "fabro-devcontainer"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"fabro-http",
"fabro-static",
@@ -1913,7 +1914,7 @@ dependencies = [
[[package]]
name = "fabro-dump"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"bytes",
@@ -1927,7 +1928,7 @@ dependencies = [
[[package]]
name = "fabro-github"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"base64",
@@ -1949,7 +1950,7 @@ dependencies = [
[[package]]
name = "fabro-graphviz"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"fabro-types",
@@ -1963,7 +1964,7 @@ dependencies = [
[[package]]
name = "fabro-hooks"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"async-trait",
"fabro-agent",
@@ -1987,7 +1988,7 @@ dependencies = [
[[package]]
name = "fabro-http"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"fabro-static",
"http",
@@ -1997,7 +1998,7 @@ dependencies = [
[[package]]
name = "fabro-install"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"base64",
@@ -2013,7 +2014,7 @@ dependencies = [
[[package]]
name = "fabro-interview"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"async-trait",
"dialoguer",
@@ -2028,7 +2029,7 @@ dependencies = [
[[package]]
name = "fabro-llm"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2062,7 +2063,7 @@ dependencies = [
[[package]]
name = "fabro-macros"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"clap",
"fabro-options-metadata",
@@ -2073,7 +2074,7 @@ dependencies = [
[[package]]
name = "fabro-manifest"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"fabro-api",
@@ -2091,7 +2092,7 @@ dependencies = [
[[package]]
name = "fabro-mcp"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"fabro-config",
@@ -2107,7 +2108,7 @@ dependencies = [
[[package]]
name = "fabro-mcp-server"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"chrono",
@@ -2134,7 +2135,7 @@ dependencies = [
[[package]]
name = "fabro-model"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"fabro-static",
"http",
@@ -2150,7 +2151,7 @@ dependencies = [
[[package]]
name = "fabro-oauth"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"axum",
@@ -2172,7 +2173,7 @@ dependencies = [
[[package]]
name = "fabro-options-metadata"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"serde",
"serde_json",
@@ -2180,7 +2181,7 @@ dependencies = [
[[package]]
name = "fabro-proc"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"cc",
"libc",
@@ -2189,7 +2190,7 @@ dependencies = [
[[package]]
name = "fabro-redact"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"aho-corasick",
"ref-cast",
@@ -2205,7 +2206,7 @@ dependencies = [
[[package]]
name = "fabro-sandbox"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2248,7 +2249,7 @@ dependencies = [
[[package]]
name = "fabro-server"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2333,7 +2334,7 @@ dependencies = [
[[package]]
name = "fabro-slack"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"fabro-http",
"fabro-interview",
@@ -2354,18 +2355,18 @@ dependencies = [
[[package]]
name = "fabro-spa"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"rust-embed",
]
[[package]]
name = "fabro-static"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
[[package]]
name = "fabro-store"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"async-trait",
"bytes",
@@ -2392,7 +2393,7 @@ dependencies = [
[[package]]
name = "fabro-telemetry"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"base64",
@@ -2418,7 +2419,7 @@ dependencies = [
[[package]]
name = "fabro-template"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"fabro-types",
@@ -2432,7 +2433,7 @@ dependencies = [
[[package]]
name = "fabro-test"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"assert_cmd",
"axum",
@@ -2455,7 +2456,7 @@ dependencies = [
[[package]]
name = "fabro-tool"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2476,7 +2477,7 @@ dependencies = [
[[package]]
name = "fabro-tracker"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2490,7 +2491,7 @@ dependencies = [
[[package]]
name = "fabro-types"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"chrono",
"clap",
@@ -2511,7 +2512,7 @@ dependencies = [
[[package]]
name = "fabro-util"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"console 0.15.11",
@@ -2531,7 +2532,7 @@ dependencies = [
[[package]]
name = "fabro-validate"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"fabro-acp",
"fabro-graphviz",
@@ -2544,7 +2545,7 @@ dependencies = [
[[package]]
name = "fabro-vault"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"chrono",
"fabro-types",
@@ -2556,7 +2557,7 @@ dependencies = [
[[package]]
name = "fabro-workflow"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"assert_cmd",
@@ -7336,7 +7337,7 @@ dependencies = [
[[package]]
name = "twin-github"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"axum",
"base64",
@@ -7355,7 +7356,7 @@ dependencies = [
[[package]]
name = "twin-openai"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
dependencies = [
"anyhow",
"async-stream",
diff --git a/Cargo.toml b/Cargo.toml
index 39008e385..2748f01a5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
-version = "0.241.0-nightly.1"
+version = "0.242.0-nightly.1"
license = "MIT"
[workspace.dependencies]
diff --git a/apps/fabro-web/app/components/run-waterfall.tsx b/apps/fabro-web/app/components/run-waterfall.tsx
new file mode 100644
index 000000000..b9b06fb08
--- /dev/null
+++ b/apps/fabro-web/app/components/run-waterfall.tsx
@@ -0,0 +1,330 @@
+import { useEffect, useMemo, useState, type ReactNode } from "react";
+import { Link } from "react-router";
+import { StageState, type RunStage } from "@qltysh/fabro-api-client";
+
+import { HoverCard, PopoverHeader, PopoverRow, PopoverRows } from "./ui";
+import { isVisibleStage } from "../data/runs";
+import { formatAbsoluteTs, formatDurationMs } from "../lib/format";
+import {
+ formatStageLabel,
+ stageStatusLabel,
+ stageStatusTone,
+} from "../lib/stage-sidebar";
+import { deriveRunPhases, type RunPhase } from "../lib/run-phases";
+import type { EventEnvelope } from "@qltysh/fabro-api-client";
+
+interface WaterfallProps {
+ runId: string;
+ events: EventEnvelope[];
+ stages: RunStage[];
+ createdAtIso: string;
+ completedAtIso: string | null;
+}
+
+interface Row {
+ key: string;
+ kind: "phase" | "stage";
+ label: string;
+ startMs: number;
+ endMs: number | null;
+ durationMs: number | null;
+ barClass: string;
+ href: string | null;
+ popover: ReactNode;
+}
+
+const MIN_BAR_WIDTH_PCT = 0.4;
+
+function useTickingNow(intervalMs: number): number {
+ const [now, setNow] = useState(() => Date.now());
+ useEffect(() => {
+ const id = setInterval(() => setNow(Date.now()), intervalMs);
+ return () => clearInterval(id);
+ }, [intervalMs]);
+ return now;
+}
+
+function stageBarClass(status: StageState): string {
+ switch (status) {
+ case StageState.RUNNING:
+ case StageState.RETRYING:
+ return "bg-teal-500 animate-pulse";
+ case StageState.SUCCEEDED:
+ return "bg-mint";
+ case StageState.PARTIALLY_SUCCEEDED:
+ return "bg-amber";
+ case StageState.FAILED:
+ return "bg-coral";
+ case StageState.PENDING:
+ return "bg-overlay-strong";
+ case StageState.SKIPPED:
+ case StageState.CANCELLED:
+ return "bg-fg-muted/40";
+ }
+}
+
+function isStageInFlight(status: StageState): boolean {
+ return status === StageState.RUNNING || status === StageState.RETRYING;
+}
+
+function chooseTickIntervalMs(rangeMs: number): number {
+ if (rangeMs < 30_000) return 5_000;
+ if (rangeMs < 120_000) return 15_000;
+ if (rangeMs < 600_000) return 60_000;
+ if (rangeMs < 3_600_000) return 5 * 60_000;
+ if (rangeMs < 6 * 3_600_000) return 30 * 60_000;
+ return 60 * 60_000;
+}
+
+function phasePopover(phase: RunPhase, durationMs: number | null, inFlight: boolean): ReactNode {
+ return (
+ <>
+