Commit graph

293 commits

Author SHA1 Message Date
Bryan Helmkamp
010828ae7a
Add run commit diff picker 2026-05-09 14:49:15 -04:00
Bryan Helmkamp
447b94da6d
feat(run-files): add sandbox diff scopes
Add committed, uncommitted, and all scope handling for run files with source reporting for sandbox and final patch responses.

Wire the run files page to persist scope in the URL and cache each scope independently.
2026-05-09 13:00:14 -04:00
Bryan Helmkamp
48545f4a7d
Merge remote-tracking branch 'origin/main' 2026-05-09 11:43:59 -04:00
Bryan Helmkamp
8ad14faa17
refactor(integrations): make chat integrations Slack-only 2026-05-09 11:43:16 -04:00
Bryan Helmkamp
697dc1294f
feat(runs): support explicit run titles
Persist resolved run titles on creation, expose title update events, and add the run title PATCH API. Regenerate API clients and refresh web/server invalidation so title changes are reflected across run detail and board views.
2026-05-09 11:18:12 -04:00
Bryan Helmkamp
1ff30ea03c
Merge remote-tracking branch 'origin/main' 2026-05-09 11:05:15 -04:00
Bryan Helmkamp
2a8884883a
refactor(workflow): remove local worktree mode
Make local sandbox execution direct by removing the public worktree mode and in-place controls from CLI, config, run state, API surfaces, docs, and UI. Keep worktree support only for internal parallel-node isolation.
2026-05-09 11:04:23 -04:00
Bryan Helmkamp
5b0b1efdc2
fix(workflow): ignore deprecated project directory
Project workflows now resolve from the discovered .fabro directory instead of honoring project.directory. Keep the legacy field parse-only while removing it from resolved settings and API/client shapes.
2026-05-09 10:55:56 -04:00
Bryan Helmkamp
5fc9157017
refactor(workflow): remove retro stage (#230)
## Summary

Removes Fabro's automatic retro generation stage so workflow runs go
directly from execution to finalization and optional PR creation. This
drops the retro-specific crate, events, projection fields, config/API
knobs, and user-facing docs in favor of the existing durable run
observability surfaces.

## What Changed

- Deleted the `fabro-retro` crate and the workflow `retro` pipeline
phase, with finalization now consuming `Executed` state directly.
- Removed retro configuration and API surface area, including
`--no-retro`, `[run.execution].retros`, manifest `no_retro`,
`features.retros`, and run projection `retro*` fields.
- Retired typed `retro.*` events while keeping historical event logs
readable by deserializing retired retro event names as `Unknown`.
- Stopped appending retro sections to generated PR bodies and updated
docs, marketing copy, screenshots, and navigation to point users toward
observability/event-stream inspection.

## Testing

Not run during PR creation; this branch already contained the
implementation commit.

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, reasoning unspecified) via
[Codex](https://openai.com/codex)
2026-05-09 10:18:20 -04:00
Bryan Helmkamp
f07bb4aaba
feat(cli): support sparse input overrides (#222)
## Summary
- Add repeatable `-I` / `--input KEY=VALUE` CLI overrides for workflow
run inputs on `fabro run`, `fabro create`, and `fabro preflight`. CLI
inputs are sparse per-key overrides that merge over the resolved config
inputs (preserving unrelated inherited values), unlike TOML
`[run.inputs]` which still replaces wholesale.
- Manifest bundling and graph-level goal resolution render workflow
source with the effective inputs before structural scanning, so
input-driven `@prompt`, `import`, and `stack.child_workflow` paths get
bundled correctly.
- Persist raw `KEY=VALUE` strings on `ManifestArgs.input` so server-side
replay applies the same sparse overrides on top of merged config.
- Review-driven cleanups: shared `TemplateContext::for_input_scan`
helper for the recurring "render inputs but defer goal" idiom (replaces
4 sites), `#[derive(Default)]` on `ManifestBuildInput` to drop
boilerplate, inline trivial `apply_input_overrides` wrapper, drop a
redundant clone, and tighten the parser/test helpers.

## Test plan
- [ ] `cargo nextest run -p fabro-cli -p fabro-config -p fabro-server -p
fabro-template -p fabro-workflow`
- [ ] `cargo +nightly-2026-04-14 fmt --check --all`
- [ ] `cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-config -p
fabro-server -p fabro-template -p fabro-workflow --all-targets -- -D
warnings`
- [ ] Smoke: `fabro run <workflow> -I key=value --input other=42`
overrides those keys while preserving unrelated inherited inputs
- [ ] Smoke: `-I` accepts strings, integers, floats, booleans, empty
values; rejects arrays, inline tables, datetimes; rejects missing `=`
and empty key
- [ ] Smoke: input-driven `@prompts/{{ inputs.foo }}` and
`stack.child_workflow="{{ inputs.bar }}/workflow.fabro"` paths bundle
correctly when overridden via `-I`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:00:35 -04:00
Bryan Helmkamp
d3e33ce32c
feat(runs): own sandbox lifecycle
Create run-owned sandbox lifecycle operations so terminal runs stop by default, resumes attach and start persisted sandboxes, and run deletion deletes or hands off provider resources according to preserve settings.
2026-05-08 18:38:08 -07:00
Bryan Helmkamp
1a43cf5abc
feat(api): expose stage handlers on run stages
Populate RunStage.handler from workflow graph metadata and use it for the run stages renderer instead of inferring from activity events.
2026-05-08 13:36:02 -07:00
Bryan Helmkamp
0b29dbe6d8
feat(runs): link stored pull requests 2026-05-08 13:17:08 -07:00
Bryan Helmkamp
b0dfd6b3b4
fix(api): type interview answer submissions
Replace the loose interview answer request payload with a discriminated OpenAPI union so generated clients enforce the wire contract. Surface structured HTTP error details in the web client and update browser, CLI, and server answer submission paths to use the typed variants.
2026-05-08 09:18:35 -07:00
Bryan Helmkamp
9b9ebdf50b
feat(web): migrate to generated API client
Expand the OpenAPI contract for frontend auth and workflow routes, regenerate the TypeScript Axios client, and route web API calls through generated client classes while preserving SSE and install exceptions.
2026-05-08 07:44:33 -07:00
Bryan Helmkamp
befb2e00ec
feat(runs): merge command output streams
Route command stderr into stdout at execution time and expose a single output log across events, projections, API clients, and the web UI. Keep replay compatibility for older command.completed events that still contain split stdout/stderr fields.
2026-05-07 22:07:13 -07:00
Bryan Helmkamp
23cb211cce
feat(runs): surface diff summary counts
Compute cheap diff stats on checkpoint and terminal events, roll them into run summaries, and use them for the Files Changed tab badge without fetching full file diffs.
2026-05-07 17:34:32 -07:00
Bryan Helmkamp
ac0492a617
feat: track last_event_at on runs and show it in the run header
Add a `last_event_at` timestamp to RunProjection (set in apply_event so
every event ticks the field) and surface it through RunSummary and the
RunListItem board response. Backed by an OpenAPI extension so both the
Rust and TypeScript clients pick up the new optional field.

In the web UI, the run-detail header gains a "Last activity Xm ago"
badge next to the elapsed-time chip, driven by a 30-second ticker so the
relative time stays current between event refreshes.

The fabro-server tests.rs hunk is incidental rustfmt drift surfaced by
running `cargo fmt --all` over the workspace; including it keeps CI's
fmt-check green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:15:04 -07:00
Bryan Helmkamp
c8336a250e
feat: show archived runs on the runs page
Extend GET /api/v1/boards/runs with include_archived=true (matching the
existing flag on listRuns), add an Archived BoardColumn that the server
appends only when the flag is set, and surface a "Show archived" toggle
on /runs that flips between request shapes. Default behavior is unchanged
— archived runs stay hidden.

Server: list_board_runs now takes ListRunsParams; board_column maps
RunStatus::Archived to BoardColumn::Archived; board_columns(include_archived)
appends the column conditionally. Two new handler tests cover the default
and flag-on paths.

Web: useBoardsRuns(includeArchived) keys requests so SWR refetches on
toggle; columnStatuses + columnStatusDisplay + columnStyles get an
"archived" entry; buildSkeletonColumns filters by the flag so the loading
state matches the eventual response. Two new buildBoardColumns tests cover
both column shapes.

Touched generated TS client files include unrelated whitespace drift from
openapi-generator-cli; including them keeps the working tree consistent
with what `bun run generate` produces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 08:36:04 -07:00
Bryan Helmkamp
d2e6f09780
refactor(system): simplify repair-runs flow and rm --force
Mark SystemRepairRunsResponse and SystemRepairRunIssue fields required so
generated Rust/TS types stop forcing Some(...) wrapping on the producer
and defensive .unwrap_or("-") on consumers. Collapse the two-arm dispatch
in fabro rm --force into a single resolve_target step + shared
delete/account block, eliminating ~20 lines of duplicated error handling.
Loosen the brittle "no events" assertion to a substring check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:15:18 -04:00
Bryan Helmkamp
6e159fa9d3
fix(system): expose unreadable run repair flow 2026-05-06 07:15:18 -04:00
fabro-sh-0530[bot]
79f89165f6
Wire end-to-end steering for running agents (#209)
## Summary
This makes the advertised mid-run steering path real: users can send
append or interrupt steering messages through the API, CLI, and web UI,
and the worker delivers them to live API-mode agent sessions or buffers
them for the next session. The change adds the control protocol, session
interrupt machinery, workflow hub, server route/OpenAPI/client updates,
and UI feedback needed for the whole path.

### Plan Summary
- Add `SteerKind`/`run.steer` wire protocol and `POST /runs/{id}/steer`
- Deliver steers through subprocess JSONL or the in-process
`SteeringHub`
- Support append and interrupt behavior in agent sessions, with bounded
buffering and events
- Expose steering in the CLI/web UI and surface SSE toasts

## Flow

```mermaid
flowchart TB
  UI["CLI / Web UI"] --> API["POST /runs/{id}/steer"]
  API -->|"subprocess transport"| Control["Worker control JSONL"]
  API -->|"in-process transport"| Hub["SteeringHub"]
  Control --> Hub
  Hub -->|"active API sessions"| Session["SessionControlHandle"]
  Hub -->|"no active session"| Pending["Pending buffer"]
  Pending -->|"first future API session"| Session
  Session --> Agent["Session round loop"]
  Agent --> Events["RunEvent stream"]
  Events --> UI
```

## What changed and why

- Agent sessions now expose a lightweight `SessionControlHandle`, drain
steering at the top of each round, and use a replaceable round
cancellation token for interrupts. LLM waits are cancelled promptly,
while tool execution observes cancellation cooperatively so every
committed `tool_use` still gets a matching `tool_result`.
- `SteeringHub` owns active API session registration, broadcast
delivery, pending buffering, FIFO queue caps, and steering
lifecycle/drop events. A completion coordinator closes the
final-response race without introducing a workflow dependency into the
agent crate.
- The server route replaces the 501 stub, validates run state and
best-effort CLI-only steerability, and forwards through either
subprocess control JSONL or the in-process hub. OpenAPI and generated
clients now include the request type.
- The CLI and web UI can send append or interrupt steers. Run detail and
board views open the new composer, and shared SSE subscriptions now
support per-subscriber event callbacks so invalidation and steering
toasts can coexist on one EventSource.

## Review notes

- Steering actors stay on top-level `RunEvent.actor`; event props only
carry steering kind/drop metadata.
- Buffered steers replay as append messages to the first API session
that registers after an empty-active period. Per-stage targeting remains
out of scope.
- CLI-mode agent stages are still not steerable; the server returns a
best-effort 409 when all active agent stages are CLI-mode, while the
worker hub remains the authoritative safety net.
- No persistence or schema migration is required; active and pending
steering state is in memory.
- New tests focus on protocol round-trips, hub buffering/bounds, session
steering-loop behavior, SSE fanout, and basic server rejection paths.

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:34:16 -04:00
fabro-sh-0530[bot]
e40dc7d9ad
Move GitHub token permissions to [run.integrations.github.permissions] (#215)
## Summary

Token scopes describe what *a run* is authorized to do, not server
identity. Today they live under
`[server.integrations.github.permissions]`, which can't be overridden by
`workflow.toml` / `project.toml` (server keys are stripped from
per-workflow layers) — so projects and workflows can't tighten or relax
permissions despite the docs already advertising a per-run config. This
PR moves them under `[run.integrations.github.permissions]`, where the
standard layer-merge (workflow > project > user > defaults) Just Works.
Greenfield, no migration shim.

## What changed

- **New layer/resolved types** in `fabro-config` and `fabro-types`:
`RunIntegrationsLayer`, `RunIntegrationsGithubLayer`, and resolved
counterparts. `permissions` becomes a flat `HashMap<String,
InterpString>` post-resolve; empty = no token requested.
- **Server schema**: `permissions` removed from `GithubIntegrationLayer`
/ `GithubIntegrationSettings`. `deny_unknown_fields` rejects the stale
path.
- **Bundled `workflow.toml` parsing** (`run_manifest.rs`): now goes
through `SettingsLayer` via the new `parse_run_layer_from_settings_toml`
helper, so stale `[server.integrations.github.permissions]` errors
instead of being silently dropped by the old `toml::Table` lift-out.
- **Consumers updated**: server preflight, run launch path, and the CLI
worker (`runner.rs`) all read run-level permissions. CLI worker
previously hardcoded `HashMap::new()` — runs launched via the local CLI
path were getting no `GITHUB_TOKEN` regardless of TOML.
- **Shared helpers** on `RunIntegrationsGithubSettings`:
`is_token_requested()` and `resolve_permissions(lookup)` so server and
CLI don't drift.
- **OpenAPI + TS client** regenerated; new `RunIntegrationsSettings` /
`RunIntegrationsGithubSettings` schemas added, `permissions` removed
from `GithubIntegrationSettings`.
- **Repo workflows + docs** rewritten to the new path. Docs gain a
security-model note (boundary = installation grants; no Fabro-side cap).

## Key design decision: hand-rolled `Combine` for
`RunIntegrationsGithubLayer`

`ReplaceMap`'s "empty inherits from below" semantics (`maps.rs:76-80`)
are wrong here — we want `permissions = {}` in a higher layer to act as
an explicit clear. So the layer field is `Option<HashMap<...>>` with
hand-rolled `Combine`:

| Higher layer | Lower layer | Result |
|---|---|---|
| `None` | anything | lower (inherit) |
| `Some(map)` | anything | `Some(map)` (full replace, including
`Some({})` = clear) |

Not derived: the blanket `Option<T: Combine>` impl would recurse into
the inner `HashMap` and reintroduce empty-fallback. Documented inline in
`layers/run.rs`.

`InterpString` is preserved through resolve and only flattened to
`String` at the start-services boundary, matching the existing pattern.

### Plan Summary

- New `[run.integrations.github.permissions]` layer + resolved types;
remove from server side.
- Hand-rolled `Combine` so empty-wins-as-clear; no change to
`ReplaceMap` semantics for other consumers.
- Strict `SettingsLayer` parse for bundled `workflow.toml` so stale
schema errors loudly.
- Both server and CLI worker paths read run-level permissions via shared
helpers.
- OpenAPI + TS client regenerated; parity test added.
- Repo workflow TOMLs and `integrations/github.mdx` rewritten.


### Fabro Details

<details>
<summary>Ran 0 stages in 61m 23s for $53.41</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **61m 23s** | **$53.41** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    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."]
    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]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:33:31 -04:00
fabro-sh-0530[bot]
786a6f67e1
Read billing and stages from RunProjection with live runtimes (#213)
### Summary
Billing and stage lists now use the event-sourced `RunProjection` as
their source of truth, so running and retrying stages appear immediately
and runtimes keep advancing in the UI. This removes the checkpoint
completed-node bypass that hid in-flight work and froze totals until the
next server response.

### Plan Summary
- Store stage `started_at`, terminal `duration_ms`, server-internal
`usage`, and lifecycle `state` on `StageProjection`.
- Populate those fields from stage lifecycle events, including retry
transitions and per-attempt reset on new starts.
- Render `/runs/{id}/stages` and `/runs/{id}/billing` from
`RunProjection.iter_stages()`.
- Expose the new API/client fields and tick in-flight billing runtimes
on the web UI.

```mermaid
flowchart TB
  Events["Stage lifecycle events"] --> Projection["RunProjection StageProjection"]
  Projection --> StagesAPI["GET /runs/{id}/stages"]
  Projection --> BillingAPI["GET /runs/{id}/billing"]
  StagesAPI --> StageUI["Stage sidebar/stages view"]
  BillingAPI --> BillingUI["Billing tab live totals"]
```

### Key decisions
Retry and revisit handling stays one row per node id: latest visit data
wins, while first-seen event sequence keeps ordering stable with
finalize output. `state` is stored rather than derived so `Retrying` is
representable, and old serialized projections still work through the
`effective_state()` fallback. Billing `usage` remains server-internal
and is skipped on the wire; public schemas only expose the fields needed
by `/stages`, `/billing`, and the frontend live timer.

Added focused reducer, server retry/revisit, API round-trip, billing UI,
and event invalidation coverage.

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-05 09:32:33 -04:00
fabro-sh-0530[bot]
6e36d8350e
Render stage activity from scoped events endpoint (#212)
## Summary
Stage detail now loads activity from a canonical stage-scoped events
endpoint instead of falling back to the first 1000 run-wide events. This
fixes empty panes for late stages in long runs and removes the
presentation-shaped `StageTurn` API from the wire.

### Plan Summary
- Add `GET /runs/{id}/stages/{stageId}/events` with cursor pagination
and server-side `node_id` filtering.
- Replace frontend stage-turn/fallback loading with paginated
stage-events loading and local event-to-activity projection.
- Broaden SSE/SWR invalidation so every activity event consumed by the
reducer refreshes the per-stage cache.
- Remove `StageTurn` schemas/client models and update demo fixtures plus
pagination/handler/reducer tests.

## What changed and why
The store now scans the run event prefix and filters by `node_id` before
applying the `limit + 1` cutoff. That preserves sparse late-stage
matches that would otherwise be dropped if we reused the run-wide
limited scan and filtered afterward. The real-mode handler returns an
empty page for an unknown stage id in an existing run, while preserving
404 for missing runs.

On the frontend, `run-stages` fetches all pages for the selected stage
and feeds them through `eventsToActivity`, keeping `TurnType` as a local
presentation model. Invalidation now targets `runs.stageEvents(runId,
stageId)` for lifecycle and reducer-consumed activity events
(`stage.prompt`, agent messages/tools, and command events), so active
panes refresh from the existing run event subscription.

The OpenAPI document and generated TS client now expose
`listStageEvents` and drop stale `StageTurn` models. Demo mode serves a
`detect-drift` stage-events fixture using the same cursor semantics as
the real endpoint.

## API notes
`/runs/{id}/stages/{stageId}/turns` is removed; clients should use
`/runs/{id}/stages/{stageId}/events?since_seq=&limit=` and project
events locally. The `stageId` path segment for this endpoint is the
workflow node id, not the visit-qualified `node_id@visit` form used by
command logs/artifacts.

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-05 09:16:52 -04:00
fabro-sh-0530[bot]
333b603f5b
Encode stage visits in run stage URLs (#206)
### Summary
Stages that re-enter the same workflow node now get distinct
`node@visit` identities end to end, so looped stages like `verify@1` and
`verify@2` no longer collapse to the same sidebar link, event stream,
graph selection, or turns view.

### What changed
- `RunStage.id` now uses the full `StageId` string (`node_id@visit`),
with required `node_id` and `visit` fields in the OpenAPI schema and
generated clients. This intentionally replaces the old `dot_id` field.
- The server builds `/runs/{id}/stages` from
`RunProjection::iter_stages()` instead of checkpoint `completed_nodes`,
preserving visit information and including in-flight stages from
projection data.
- Stage status is derived from the latest lifecycle event for each exact
`stage_id`, so retrying stages do not appear failed while a retry is
underway.
- The frontend maps and displays visits with `(N)` suffixes, filters
fallback turns by `stage_id`, invalidates suffixed stage-turn query keys
from SSE, and aggregates graph nodes by `node_id` with latest-visit
click targets.

### Plan Summary
- Preserve per-visit stage identity across API, server projection,
generated clients, and UI routing.
- Keep graph nodes keyed by workflow node while routing clicks to the
latest visit.
- Add coverage for multi-visit stages, retrying status derivation,
suffixed SSE invalidation, sidebar labels, and stage event filtering.

### Reviewer notes
This is a breaking API shape change for `RunStage`: consumers should use
`node_id` for graph/node identity and `id` for per-visit stage identity.
The old `dot_id` field is removed rather than kept as a compatibility
alias.

### Fabro Details

<details>
<summary>Ran 9 stages in 54m 55s for $41.40</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 8s | – | 0 |
| preflight_lint | 2m 14s | – | 0 |
| implement | 31m 38s | $17.65 | 0 |
| simplify_opus | 10m 2s | $2.40 | 0 |
| simplify_gpt | 6m 9s | $21.35 | 0 |
| verify | 2m 3s | – | 0 |
| fmt | 2s | – | 0 |
| **Total** | **54m 55s** | **$41.40** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    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."]
    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]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:27:11 -04:00
Bryan Helmkamp
b5b08e78d3
refactor(api): reuse board column contract across clients
Make BoardColumnDefinition.id reference the existing BoardColumn schema and carry that typed contract through generated TypeScript, server responses, demo data, and the runs board UI.
2026-05-04 15:52:24 -04:00
Bryan Helmkamp
f39e512990
feat(web): split Queued column out of Initializing on the run board
Submitted and Queued lifecycle statuses now live in a dedicated Queued
column rendered to the left of Initializing; Starting stays in
Initializing. The column is omitted from the board when it has no items
so day-to-day boards stay compact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:13:01 -04:00
Bryan Helmkamp
95eb13750a
fix(billing): render non-LLM run stages
Include completed stages without LLM usage in run billing responses so command-only runs still show runtime rows. Keep token and model aggregates scoped to billed LLM usage, and render placeholder values in the web billing table.
2026-05-03 22:49:33 -04:00
Bryan Helmkamp
649b57c185
feat(api): expose web_url on run responses and run.created event
So that CLI and other API consumers can surface a clickable link to the
run's web UI page instead of guessing route shapes or probing settings.
The server populates `web_url` from `server.web.enabled` and
`server.web.url`, returns it on `RunStatusResponse` (create plus all
lifecycle transitions), and persists it on the `run.created` event so
attach replays the same link without re-deriving it.

CLI: prints `Web UI: <url>` as a run-header info line, driven off the
replayed event so fresh runs and `attach` share one code path. Absent
when the UI is disabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:18:19 -04:00
Bryan Helmkamp
43f03087e6
Merge remote-tracking branch 'origin/main' 2026-05-02 10:06:25 -04:00
Bryan Helmkamp
29c45498b0
Fix run principal attribution gaps 2026-05-02 09:14:02 -04:00
Bryan Helmkamp
f628b91c04
Merge origin/main
Reconcile parallel refactors that converged on stage vocabulary from
opposite directions. Origin's vocabulary wins everywhere it conflicts:
StageProjection (struct), StageState (status enum, kept), StageCompletion
(record). Local's artifact-retries feature, OpenAPI typing of JSON-blob
fields, parallel awaits in build_conclusion_from_store, and shared
RETRY_SEGMENT_PREFIX constant carry forward on top.

Origin's stage_at_visit reducer helpers (8 arms) supersede the local
stage_entry_with_current_visit (5 arms); same intent, broader coverage.
Origin's iter_stages_mut and first_event_seq helper unification are
preserved unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:05:41 -04:00
Bryan Helmkamp
8d7b9a804a
Unify run event principals 2026-05-01 21:56:47 -04:00
Bryan Helmkamp
cc81f538f4
refactor(api): type StageState JSON-blob fields so the TS client stops emitting any
provider_used, script_invocation, and script_timing become object | null;
parallel_results becomes Array<object> | null. The Rust StageState type is
unaffected because fabro-api/build.rs replaces it with fabro_types::StageState.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:16:56 -04:00
Bryan Helmkamp
95dae5afac
feat(run): separate stage state and artifact retries 2026-05-01 20:10:47 -04:00
Bryan Helmkamp
cea1fa739d
refactor(run-projection): use stage vocabulary 2026-05-01 19:56:22 -04:00
Bryan Helmkamp
e50df2b58b
feat(command): distinguish cancelled commands from timeouts
Represent command termination explicitly across sandbox results, events,
run projections, API types, and the run stage UI. This removes the fake
-1 exit code path for timeout/cancel and lets consumers tell cancelled
commands apart from timed-out commands.
2026-04-30 22:45:37 -04:00
Bryan Helmkamp
8ac400df1c
feat(command): stream command logs from CAS-backed storage
Persist command stdout/stderr through scratch logs and finalized CAS refs, expose byte-offset tailing through the API, and render separate streaming panels in the web run view.

Resolve command output blob refs for execution-time consumers such as edge routing and retros, and make Docker streaming timeout/cancel drain output before returning.
2026-04-30 22:45:37 -04:00
Bryan Helmkamp
015992208a
feat(models): expose configured status
Add configured to the model API contract and server responses so clients can see whether provider credential material exists before testing. Use that signal in bulk model tests to skip unconfigured providers before printing progress and treat post-list skips as race failures.
2026-04-30 09:15:02 -04:00
Bryan Helmkamp
d65c1fa635
refactor(types): remove stage status compatibility 2026-04-30 06:48:47 -04:00
Bryan Helmkamp
ba529c23f9
refactor(web): regenerate client for precise stage states 2026-04-30 06:33:12 -04:00
Bryan Helmkamp
6a8329b376
refactor(api): align run projection types 2026-04-29 20:32:50 -04:00
Bryan Helmkamp
7feef1c6b2
Merge remote-tracking branch 'origin/main' 2026-04-29 13:01:43 -04:00
Bryan Helmkamp
6156f65799
fix(run-files): normalize degraded file diffs
Return degraded run files with the same FileDiff[] shape as live responses, using nullable contents and per-file unified patches so the web sidebar and deep links work consistently.
2026-04-29 13:00:55 -04:00
Bryan Helmkamp
890fbb8fa4
feat(server): correlate API errors with request ids
Generate a fresh UUIDv4 per request, attach it to response headers,
JSON error bodies, and HTTP response logs so client-visible failures can be
matched to server logs without trusting inbound request id headers.
2026-04-29 12:20:56 -04:00
Bryan Helmkamp
9ad47990f4
fix(web): include server URL in auth quick start
Expose the configured server.web.url in system info so the empty runs quick start can show a runnable fabro auth login command instead of a placeholder.
2026-04-29 07:45:17 -04:00
Bryan Helmkamp
4ca8962a02
fix(cli): keep validate off runtime preflight
Add a validation-only API response and route while keeping fabro validate local so it does not start or contact the server for structural workflow checks.
2026-04-28 14:50:04 -07:00
Bryan Helmkamp
642e312cca
refactor: unify duplicate API types via with_replacement
Eliminate four parallel-type duplications between fabro-api generated
DTOs and fabro-types canonical types. The wire shape is owned by
OpenAPI; canonical types are reused via fabro-api/build.rs
with_replacement so the adapter functions and silent unwrap_or_default
defaults disappear.

- SecretType moves to fabro-types (was fabro-vault); deletes
  secret_type_from_api adapter.
- DiffLineStats renamed to DiffStats, moved to fabro-types, switched
  u64 -> i64 to match the OpenAPI integer; deletes line_stats_to_api.
- ManifestPreRunPushOutcome rewritten as a oneOf+discriminator
  PreRunPushOutcome over five variant schemas, deleting both
  pre_run_push_outcome_from_manifest and build_manifest_push_outcome.
- ManifestGit and PreRunGitContext unify as GitContext: dirty:
  DirtyStatus replaces clean: bool (preserving the Unknown state
  previously truncated on the wire), sha becomes Option<String>, and
  origin_url/branch fold into the unified context. RunSpec and
  RunCreatedProps flatten three fields (repo_origin_url, base_branch,
  pre_run_git) into a single git: Option<GitContext>.

Each replacement gets a fabro-api parity test (TypeId equality plus
JSON roundtrip) modeled on run_summary_round_trip.rs. TS client
regenerated.

Greenfield app, no production deployments — wire contract changed
directly without backwards-compat shims.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 12:29:37 -07:00
Bryan Helmkamp
20e161bc07
refactor: rename checkpoints_disabled -> in_place
The persisted bool described user intent (\"the user opted into the
in-place execution mode\"), not a literal consequence -- SlateDB and
event-sourced checkpoints flow regardless of the flag, only git
checkpoints are skipped. Renaming aligns the name with intent and
decouples it from any future implementation that allows git
checkpoints in-place.

The fork validator still consults this bool to bail out with a clear
error before searching for git checkpoints that won't exist.
2026-04-28 09:33:59 -07:00