Commit graph

766 commits

Author SHA1 Message Date
Bryan Helmkamp
d59f97d6a0
refactor(web): extract runs list view into reusable module
Move the inline runs list view (pagination, sorting, column picker,
selection, bulk actions) from runs.tsx into a self-contained module at
components/runs-list/ so it can be reused by the Children sub-tab and
future run-list surfaces. No behavior change to /runs.

- RunsListView now takes an emptyState slot (Runs page passes RunsLandingEmpty)
- useRunsPage accepts an optional parentId for non-page run lists
- runListCacheMatchers also matches ["runs","children",...] so bulk
  archive/delete invalidate children caches automatically

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:40:01 -04:00
Bryan Helmkamp
7cc38e2f8d
feat(web): promote Approve to dedicated buttons
Pending approval is the only action a user can take to unblock a run,
so the Approve action shouldn't be buried in the Actions dropdown.

- Run detail header: render a primary teal "Approve" button beside the
  Actions menu when approval is pending; remove the duplicate menu item.
- Board view (/runs): surface `pendingApproval` on RunItem and render
  an inline Approve button on cards in the Pending column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:57:19 -04:00
Bryan Helmkamp
069c6baa58
Allow retrying cancelled runs
Cancelled runs are now eligible for retry alongside other failed and
dead runs. A user who cancels a run and then changes their mind no
longer has to manually re-create it from scratch.

- ensure_retryable drops the FailureReason::Cancelled rejection arm
- canRetry simplifies to failed || dead (still gated by !archived)
- OpenAPI Retry Run description no longer lists cancelled as ineligible

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:28:16 -04:00
Bryan Helmkamp
97dd28b356
Add tool descriptions as tooltips in sidebar Tools list
- Loosen vertical spacing between tool rows (space-y-1 → space-y-1.5).
- Add `title={tool.description}` so hovering a tool surfaces the
  model-facing description without re-adding inline description text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:00:50 -04:00
Bryan Helmkamp
aa1613e95a
Simplify agent sidebar: drop permission badge, slim Tools list
- Remove the "Full Access" / permission badge section; Tools now conveys
  the same surface area more directly.
- Reorder so Tools sits at the bottom (after MCPs).
- Strip each tool row to just a used/not-used indicator and the tool
  name — no descriptions, source labels, or category badges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:47:39 -04:00
Bryan Helmkamp
e68282466e
Add sandbox providers settings page
New /settings/sandboxes route surfaces the local, docker, and daytona
runtime sandbox providers using existing useServerSettings(). Mirrors
the /settings/models pattern: enabled providers shown first, disabled
hidden behind a progressive-disclosure toggle. Disabled Daytona row
links to add the DAYTONA_API_KEY secret.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:47:15 -04:00
Bryan Helmkamp
7efa38c384
Add icons to Ask Fabro preset prompts
Gives each of the four empty-state suggestions a Heroicon to aid
scannability: warning triangle for "Surface errors", bolt for
"Analyze performance", map for "Review key decisions", and lightbulb
for "Suggest improvements".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:30:14 -04:00
fabro-sh-0530[bot]
7d655d7c95
feat: expose effective agent tool list via StageProjection.agent_tools (#388)
## Summary

Adds `StageProjection.agent_tools` — a replay-authoritative list of
every tool the model can actually call — so UI and API consumers no
longer have to infer tool availability from `permission_level`. The
field is populated by a new `agent.tools.available` durable event
emitted once per stage session after provider-profile setup, MCP
integration, and access-policy filtering are complete.

### Plan Summary

- **Types (`fabro-types`)** — new `AgentToolSummary`, `AgentToolSource`,
`AgentToolCategory`, `AgentToolsAvailableProps`, and
`EventBody::AgentToolsAvailable`; `agent_tools: Vec<AgentToolSummary>`
added to `StageProjection` with skip-serializing-if-empty semantics.
- **Tool registry (`fabro-agent`)** — `ToolSource::Mcp` gains
`original_name` (no more re-parsing the qualified name downstream);
`ToolDefinitionWithSource::to_agent_tool_summary()` maps to the public
DTO; `Session::effective_tools()` / `agent_tool_summaries()` expose the
filtered list; `tool_category` split into `tool_category` (CLI gate,
defaults `Shell`) and `known_tool_category` (projection, returns `None`
→ `Other` for unknown tools).
- **Projection reducer (`fabro-store`)** — `AgentToolsAvailable`
replaces the stage's `agent_tools`; `AgentToolStarted` flips `invoked =
true` on the matching entry; legacy runs without the event get an empty
list.
- **OpenAPI + generated clients** — `AgentToolSummary`,
`AgentToolSource`, `AgentToolCategory`, `AgentToolsAvailableProps`
schemas added; `StageProjection.agent_tools` field added; `build.rs`
replacements wire them to the `fabro-types` structs.
- **Web sidebar** — new collapsible "Tools" section renders name,
description, source/category badge, and used/available state from
`stage.agent_tools`; `permission_level` is kept as secondary fallback
metadata for legacy stages.

## Key design decisions

- **`agent_tools`, not `tools`** — avoids ambiguity with MCP nested
tools and completion API tool definitions.
- **Dedicated `agent.tools.available` event** — cleaner than overloading
`agent.session.activated`; replacement semantics on replay mean
re-emission works if tool registration ever becomes mutable.
- **`original_name` carried in `ToolSource::Mcp`** — stored by the MCP
integration at registration time so the projection never needs to
re-parse qualified names like `mcp__filesystem__read_file`.
- **`invoked` is projected state, not event state** — the availability
event always emits `false`; replay of `agent.tool.started` flips
matching entries.
- **Parameter schemas omitted** — `AgentToolSummary` carries only
`name`, `description`, `source`, `category`, and `invoked` to keep
payloads small and avoid exposing implementation detail.
- **`AgentToolCategory::Other` for unknown tools** — unlike the CLI
permission gate (which defaults to `Shell` to require approval), the
projection uses `Other` to surface unrecognized MCP/skill tools
accurately.


### Fabro Details

<details>
<summary>Ran 8 stages in 56m 37s for $58.75</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 0s | – | 0 |
| preflight_lint | 2m 15s | – | 0 |
| implement | 22m 54s | $42.64 | 0 |
| simplify_opus | 16m 41s | $10.62 | 0 |
| simplify_gpt | 3m 43s | $5.49 | 0 |
| verify | 8m 33s | – | 0 |
| **Total** | **56m 37s** | **$58.75** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
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.", 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="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 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! 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"]
    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 -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 16:41:55 -04:00
fabro-sh-0530[bot]
98c26d5370
Fold context-window data into agent.message, remove snapshot event (#390)
## Summary

Removes the standalone `agent.context_window.snapshot` event and instead
attaches the context-window projection directly to `agent.message`. This
eliminates the async provider token-count API calls that the old
approach required, and simplifies the event log to a single event type
carrying all post-response agent data.

## What Changed and Why

**Before:** After each LLM turn, the agent emitted a separate
`agent.context_window.snapshot` event — first a local estimate, then
potentially a second one after an async `count_input_tokens` call
resolved (or after response usage arrived). This required fingerprint
deduplication state, a `close_token` to cancel in-flight counts, and
frontend handling for the extra event type.

**After:** The `AgentEvent::AssistantMessage` variant carries an
`Option<StageContextWindowProjection>`. The projection is computed
locally at request-build time and then refined using response token
usage when available (`ResponseUsageScaledBreakdown`), or kept as a
`LocalEstimate` when response usage is absent. No provider API calls are
made.

### Plan Summary

- **Task 1:** Added `context_window:
Option<StageContextWindowProjection>` to `AgentMessageProps` (Rust types
+ OpenAPI), removed `AgentContextWindowSnapshotProps` and
`EventBody::AgentContextWindowSnapshot`.
- **Task 2:** Removed the spawned `count_input_tokens` task,
`close_token`, fingerprint sets, and both snapshot-emit methods from
`Session`. Added `context_window_from_response_usage` to
`context_window.rs`; `BuiltRequest` now holds the local projection
instead of the tool list.
- **Task 3:** Workflow conversion copies `context_window` from
`AgentEvent::AssistantMessage` into `AgentMessageProps`; store reducer
reads it from `AgentMessage` instead of the removed snapshot variant and
stamps `event_seq`.
- **Task 4:** GET endpoint tests updated to seed data via
`agent.message` with embedded context-window; endpoint behavior
unchanged.
- **Task 5:** Frontend constant and tests for
`agent.context_window.snapshot` removed; `agent.message` already
invalidates `stageContextWindow` through existing stage-activity
handling. TypeScript client regenerated with the new `AgentMessageProps`
model.

### Key Design Decisions

- **No provider token-count API calls** during normal execution —
context-window accuracy relies on local estimates scaled by response
usage, which is always available for successful turns.
- **Failed-before-response turns** emit no context-window data
(`context_window: None`), matching the old behavior where a snapshot
would have been emitted but response-usage scaling would never arrive.
- `BuiltRequest` drops the `tools` field (only needed for the
now-removed snapshot emission path); the local projection is computed at
build time and stored directly.


### Fabro Details

<details>
<summary>Ran 8 stages in 60m 3s for $55.78</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 1s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 30m 39s | $44.82 | 0 |
| simplify_opus | 10m 48s | $4.03 | 0 |
| simplify_gpt | 5m 1s | $6.93 | 0 |
| verify | 8m 47s | – | 0 |
| **Total** | **60m 3s** | **$55.78** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
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.", 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="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 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! 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"]
    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 -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 15:48:21 -04:00
Bryan Helmkamp
9d1f170908
Spin the in-progress TODO icon in the stage sidebar
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:25:49 -04:00
Bryan Helmkamp
aa65b79e57
Fix list-view sort by making preferences the unit of mutation
Clicking a column header on /runs?view=list silently did nothing.
handleSortClick called updateParam three times in a row; each call
cloned a fresh URLSearchParams from the same closure-captured
searchParams and invoked setSearchParams independently. React Router's
setSearchParams calls don't merge in a single tick, so only the last
one's params landed in the URL -- the sort change was overwritten by
the trailing page-reset. setPageSize had the same shape and was also
quietly losing the size change.

Replace the per-key URL mutator with a reducer-shaped
updatePreferences((prev) => next) that operates on the typed
RunsWorkspacePreferences model. URL and localStorage are derived from
the same next object via the existing converters, and each handler
makes exactly one call -- so concurrent-update races are
structurally impossible. Use setSearchParams((prev) => ...) so the
updater reads the latest committed URL params instead of a closure.

Add page to RunsWorkspacePreferences so the model describes the full
URL view state; strip it before persisting to localStorage since page
is ephemeral. Rename persistRunsWorkspaceSearchParams to
persistRunsWorkspacePreferences to match what it now consumes. Guard
the hydration useEffect with a useRef so it runs only on mount.

Add a regression test that clicks a SortHeader and asserts the URL
gains sort=status while preserving view=list&archived=1, and that a
second click toggles direction=asc.
2026-05-24 13:24:23 -04:00
Bryan Helmkamp
e1ea4ebb9d
Fix verification fixture drift 2026-05-24 12:14:59 -04:00
Bryan Helmkamp
2e85a1ec48
Add New Automation form and refresh Secrets form layout
- Add /automations/new with Basics/Source/Goal/Triggers panels and a
  kebab-case Slug auto-derived from Name until the user edits it
- Wire the "Create Automation" button on /automations to the new page
- Move individual automation URL from /automations/<slug> to
  /automation/<slug>; /automations and /automations/new are unchanged
- Refresh /settings/secrets/new to use the Panel + Row layout pattern:
  breadcrumb header, one field per row, plain footer, no inside-Panel
  stacked form

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:04:50 -04:00
Bryan Helmkamp
f59a39db0e
Restrict event export actions to Debug sub-tab
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:50:41 -04:00
Bryan Helmkamp
6544e2c589
Suppress redundant Starting pill on runs list view
The lifecycle status pill next to the run title now hides when the run
is in the initializing column, mirroring the board view behavior. This
removes the duplicate "Initializing" / "STARTING" indicators that
appeared together on the row during startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:46:04 -04:00
Bryan Helmkamp
8123caad80
Remove non-functional demo-mode Connect dropdown
The Connect menu (Preview / SSH) on the run detail header was wired up
only in demo mode and the items were never connected to real actions.
Drop the dead UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:24:31 -04:00
Bryan Helmkamp
6b88af9ccb
Improve contrast of muted stage status pills
The Pending, Skipped, and Cancelled status pills used `text-fg-muted`
(#4B5768) on `bg-overlay-strong`, producing ~1.7:1 contrast against the
panel — well below WCAG AA. Switch to `text-fg-3` (#A8B5C5) for ~6:1
while keeping the subdued look that distinguishes these states from
active/result tones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:22:58 -04:00
Bryan Helmkamp
96356a9a93
Show stage details in hover popover on run overview graph nodes
Reuses the StagePopover from the stages sidebar so graph nodes reveal
the same handler, timing, model, and status-specific detail on hover.
The graph is server-rendered SVG injected via innerHTML, so listeners
are attached imperatively alongside the existing click handlers; the
popover is portal-positioned via the shared hoverCardStyle helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:05:37 -04:00
Bryan Helmkamp
e21bc6e59d
Show stage details in hover popover on run stages sidebar
Hovering a stage row reveals handler, timing, model, and status-specific
detail — failure reason for failed/retrying, notes for skipped/partial,
tokens and files-touched for succeeded. Lazy-fetches per-stage events
on first hover via the existing useRunStageEvents hook; HoverCard gains
an openDelay so a cursor sweep doesn't trigger fetches for every row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:57:37 -04:00
Bryan Helmkamp
abc5991177
Add Copy/Download actions for loaded stage events
Two icon buttons on the stage events toolbar, to the right of the
model usage label: copy all loaded events as pretty-printed JSON, or
download them as JSONL. Both read from the existing SWR cache, so no
new API surface — for in-flight stages they're a snapshot of what the
client has fetched so far.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:22:35 -04:00
Bryan Helmkamp
bab40caf88
Show model details in hover popover on stage events toolbar
Replace the native browser title tooltip on the model chip with a
structured HoverCard listing provider, model, reasoning, and speed.
Shorten the chip label to `model[effort]` (e.g. `gpt-5.5[xhigh]`).
2026-05-24 10:19:15 -04:00
Bryan Helmkamp
fe1afdedcd
Collapse events toolbar search into icon by default
Search expands on focus with a width transition and collapses on blur
when empty. Ghost icon style when collapsed; full input styling slides
in on expand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:44:53 -04:00
Bryan Helmkamp
c19cedaede
Link unconfigured providers to prefilled secret form
On /settings/models, unconfigured providers now offer "Add secret →"
alongside "Get API key →", deep-linking to /settings/secrets/new with
the expected vault secret name prefilled. Driven by a new
`expected_secret_name` field on the Provider API, derived from the
first vault credential in the catalog so the suggestion stays in sync
with the catalog instead of being hardcoded on the frontend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:34:24 -04:00
Bryan Helmkamp
95ca1b9619
Use ClockIcon for Automations nav entry
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:32:35 -04:00
Bryan Helmkamp
b79ba12bbf
Add Copy run ID action to run detail Actions menu
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:25:22 -04:00
Bryan Helmkamp
38ae3b7c81
Add run size badge to run header
Surfaces the existing summary.size (XS/S/M/L/XL) as a compact bold
badge to the right of the last-event chip, with a tooltip showing
the underlying billed cost. Hidden when no billing data is available
so we don't show a misleading "XS" for runs with zero cost.
2026-05-24 09:21:44 -04:00
Bryan Helmkamp
78037c22cc
Add batch Delete action to runs list bulk toolbar
Wires the existing POST /api/v1/runs/delete endpoint into the runs list
selection toolbar. Surfaces a confirmation dialog before calling the
fail-soft batch delete, since deletion is irreversible. Only archived
runs are eligible, matching the single-run delete semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:10:48 -04:00
fabro-sh-0530[bot]
04e169ef79
Add POST /api/v1/runs/delete batch delete endpoint (#382)
## Summary

Adds a fail-soft batch delete endpoint (`POST /api/v1/runs/delete`) that
mirrors the existing archive/unarchive batch pattern, processes 1–250
run IDs independently, and returns per-item outcomes with an aggregate
summary. Existing `DELETE /api/v1/runs/{id}` behavior is unchanged.

### Plan Summary

- **OpenAPI-first**: new
`BatchDeleteRunsRequest/Response/Result/Summary` schemas added to the
spec; Rust (`fabro-api`) and TypeScript (`fabro-api-client`) clients
regenerated.
- **Delete internals refactored**: `DeleteRunOutcome` gains `Deleted`
and `AlreadyAbsent` variants (replacing the old `NoContent`);
`delete_run_internal` and its helpers now return `Result<_, ApiError>`
instead of `Result<_, Response>`, enabling both the single-delete
handler and the new batch handler to reuse the same logic.
- **Batch handler**: `batch_delete_runs` in `lifecycle.rs` validates the
request (reusing the generalized `validate_batch_run_ids`), loops over
IDs, and assembles `BatchDeleteRunsResult` items mapping
`ApiError::status()` to outcome strings (`conflict`, `error`).
- **Web helper**: `deleteRuns` added to `run-actions.ts` alongside
`archiveRuns`/`unarchiveRuns`, with the same `as unknown as` cast needed
for the openapi-generator `Set<string>` quirk.
- **Tests**: six new server integration tests cover ordered results,
mixed outcomes without rollback, force deletion, sandbox preservation
handoff, pre-mutation validation rejection, and auth gating.

### Key design decisions

**`POST /runs/delete` not `DELETE /runs`** — JSON request bodies on
`DELETE` are poorly supported by proxies and HTTP clients; the existing
batch lifecycle endpoints already use JSON-body `POST` actions.

**`already_absent` counts as success** — consistent with single-delete
semantics where `204` means "deleted or already absent"; callers doing
cleanup don't need to special-case missing IDs.

**`force` is batch-wide** — callers needing mixed force behavior issue
separate requests; this keeps the request schema simple.

**`SandboxDeleteOutcome` internal enum** — introduced alongside
`DeleteRunOutcome` to cleanly separate the sandbox-layer result
(absent/cleaned/preserved) from the top-level outcome that callers see,
avoiding a leaky intermediate type.


### Fabro Details

<details>
<summary>Ran 8 stages in 41m 52s for $13.37</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 3s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 15m 12s | $8.10 | 0 |
| simplify_opus | 8m 54s | $3.30 | 0 |
| simplify_gpt | 3m 54s | $1.97 | 0 |
| verify | 9m 0s | – | 0 |
| **Total** | **41m 52s** | **$13.37** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
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.", 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="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 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! 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"]
    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 -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-24 09:00:16 -04:00
Bryan Helmkamp
e8639e552e
Quiet stage insights sidebar polish (#381)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary

Three small UX tweaks to the agent stage insights sidebar so it reads as
informational rather than alarming.

- **Permission badge stays neutral.** Removed `text-coral` (red) from
Full and `text-amber` (orange) from Read/write — every level now sits in
the foreground palette (`fg-2` / `fg-3`). Icon shape (lock / pencil /
bolt) carries the level distinction and the badge label spells it out.
- **Collapsed footer always uses the muted lock icon.** The footer is a
static affordance, not a danger signal, so a Full-access stage no longer
splashes a colored icon in the corner of the page.
- **Hide the Todos section when there are zero todos.** No header row,
no `0/0` count, no "No todos." line — saves vertical space on stages
where the agent never used TodoWrite.

## Test plan

- [x] `bun run typecheck` (apps/fabro-web)
- [x] `bun test app/components/stage-insights-sidebar.test.tsx` (8/8
pass)
- [ ] Visually confirm in a browser: Full-access agent stage shows a
neutral bolt + "Full access" label (no red); collapsed sidebar footer
shows a single muted lock regardless of level; stage with zero todos has
no Todos section.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:50:14 -04:00
Bryan Helmkamp
846d1e91af
Polish stage insights sidebar
- Track which MCP servers the agent invoked. New `invoked: bool` on
  `McpServerProjection` (OpenAPI + Rust type + generated TS client),
  set by the projector when an `AgentToolStarted` event has an
  `mcp__<server>__*` tool_name. UI shows `used/total` in the section
  header, replaces the tool count with `used` on invoked rows, and dims
  rows that weren't invoked. Sticky across status re-reads.

- Quiet noisy context-window warnings. When the snapshot's total is
  provider-authoritative (ProviderApiScaledBreakdown or
  ResponseUsageScaledBreakdown), drop local-estimator warning codes
  from the snapshot — they imply the user-facing total is wrong when
  it isn't. Also dedupe by code so a 35-turn conversation with opaque
  reasoning blocks no longer surfaces 35 copies of the same warning.

- Reword the legitimately-local warnings. "opaque provider context
  estimated from JSON" → "Some content couldn't be precisely
  tokenized; total is approximate." Same treatment for the media,
  provider-options, and whole-request local-estimate messages.

- Rename the sidebar header from "INSIGHTS" to "AGENT" to better
  describe what it shows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:34:24 -04:00
Bryan Helmkamp
deb8f27a27
Add stage insights sidebar to agent stage pages
Renders a second left sidebar on /runs/<id>/stages/<agent-stage> with
todos, color-coded context-window usage and breakdown, skills, MCP
servers, and permission level. Data comes from the existing
StageProjection and the context-window endpoint added in #378; no API
changes.

Also set permission_level to Full on workflow agent SessionOptions —
workflow agents run with no tool_access_policy and expose the full
tool registry, so Full is the honest report and avoids "Unknown"
rendering in the new sidebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:32:43 -04:00
fabro-sh-0530[bot]
8ceb246b5a
feat: Add batch archive/unarchive API endpoints and update web bulk act… (#380)
## Summary

The web UI previously issued one archive/unarchive HTTP request per
selected run. This PR adds `POST /api/v1/runs/archive` and `POST
/api/v1/runs/unarchive` endpoints that process up to 250 runs in a
single fail-soft, non-transactional request, then wires the web
bulk-action toolbar and board column menu to use them.

### Plan Summary

- **OpenAPI contract** — four new schemas (`BatchRunLifecycleRequest`,
`BatchRunLifecycleResponse`, `BatchRunLifecycleResult`,
`BatchRunLifecycleSummary`) and two new paths; Rust and TypeScript
clients regenerated.
- **Server handlers** — `batch_archive_runs` / `batch_unarchive_runs`
behind `RequiredUser`; full request validation (empty, >250, duplicates,
unparseable IDs) before any mutation; per-item outcome mapping
(`archived`, `already_archived`, `unarchived`, `not_archived`,
`conflict`, `not_found`, `error`).
- **Frontend helpers** — `archiveRuns` / `unarchiveRuns` wrappers in
`run-actions.ts`; single-run helpers unchanged.
- **UI integration** — `BulkActionToolbar` and `ColumnActionsMenu`
replaced `Promise.allSettled` fan-out with one batch call; new
`summarizeBatchLifecycleAction` helper drives toast copy for
all-success, partial, and all-failure cases.

## Key Design Decisions

**Fail-soft `200` for valid batches.** A batch where some items fail is
still a successfully *processed* request; the per-item `ok` flag and
`summary` counts communicate individual outcomes without requiring the
caller to handle HTTP errors for partial failures. Request-level
problems (bad IDs, empty list) still return `400`.

**`RequiredUser` only.** Batch endpoints accept any-run mutations from a
request body, so a run-scoped worker token must not be accepted. This is
enforced at the handler level, separate from existing single-run
lifecycle routes.

**Request validation before any mutation.** Empty list, >250 IDs,
duplicate IDs, and unparseable IDs all return `400` before touching any
run — avoiding partial mutation surprises from invalid input.

**Idempotent outcomes are successes.** `already_archived` (archive of an
already-archived run) and `not_archived` (unarchive of a terminal
non-archived run) both set `ok=true`. This matches the existing
single-run semantics and avoids spurious failures in retry scenarios.

**`ask_fabro_readiness` hoisted out of the per-item loop.** Readiness
resolution involves LLM credential work; it's identical for every run in
the batch, so it's resolved once before the loop and shared via
`&AskFabroReadiness`.

**`uniqueItems: true` / `Set<string>` workaround.** The OpenAPI
generator maps `uniqueItems` arrays to `Set<T>` in TypeScript, but the
HTTP wire format is still a JSON array. The frontend helper casts
through `unknown` to send an array so Axios serializes correctly.


### Fabro Details

<details>
<summary>Ran 8 stages in 47m 48s for $23.53</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 20m 17s | $15.09 | 0 |
| simplify_opus | 10m 27s | $6.18 | 0 |
| simplify_gpt | 3m 58s | $2.25 | 0 |
| verify | 8m 14s | – | 0 |
| **Total** | **47m 48s** | **$23.53** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
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.", 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="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 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! 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"]
    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 -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-23 23:32:35 -04:00
Bryan Helmkamp
6eb57685d4
Fix Quick Start flash when navigating to /runs with archived prefs
The Runs nav link goes to /runs (no query string), so the route briefly
rendered with default params (view=columns, archived=false) before a
post-commit useEffect restored the URL from stored preferences. On
repeat clicks the useAllRuns SWR cache for {includeArchived:false}
returned zero rows immediately, flashing the Quick Start landing for
users whose only runs are archived.

Resolve workspace search params synchronously during render via
resolveRunsWorkspaceSearchParams(), so the first frame already reflects
stored prefs. The effect now just writes the URL back to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:35:15 -04:00
fabro-sh-0530[bot]
39fa73d5e2
Add context-window snapshot API for agent stages (#378)
## Summary

Adds a best-effort `GET
/api/v1/runs/{id}/stages/{stageId}/context-window` endpoint that exposes
model-visible input-token usage, broken down by category (system prompt,
tools, MCP tools, skills, memory, conversation, other). The endpoint
degrades gracefully: it returns a stored projection snapshot when the
stage is inactive, and `available: false` when no snapshot has ever been
observed rather than surfacing count gaps as HTTP errors.

### Plan Summary

- **Unit 1** – OpenAPI schemas (`StageContextWindow`,
`StageContextWindowProjection`, breakdown/enum types) and generated Rust
+ TypeScript clients, with `fabro-api` build-time type replacements
pointing at the hand-written `fabro-types` structs.
- **Unit 2** – `ToolSource` enum on `RegisteredTool` (Native / Mcp /
Skill) + `ToolDefinitionWithSource`; new `context_window.rs` builder in
`fabro-agent` that assembles a content-free category breakdown at
request-assembly time; `fabro-llm::token_count` narrow public helpers
(`estimate_message_tokens`, `estimate_tool_definition_tokens`,
`estimate_request_control_tokens`).
- **Unit 3** – `AgentEvent::ContextWindowSnapshot` carries a
`StageContextWindowProjection`; the session emits a local snapshot
immediately, then a provider-scaled replacement (or
response-usage-scaled replacement) asynchronously; fingerprinting
prevents double-counting the same request.
- **Unit 4** – Server endpoint (stubbed routing; full handler targets a
follow-up) returning the latest projected snapshot.
- **Unit 5** – `queryKeys.runs.stageContextWindow`,
`useRunStageContextWindow` hook, and SSE invalidation for
`agent.context_window.snapshot` and all stage-lifecycle events.

### Key design decisions

**Agent-side counting, not server-side.** The exact `fabro_llm::Request`
only exists inside the active agent session. Rather than moving raw
prompt/message content into server-managed state, the session counts the
request it already has and emits content-free projection events. The
HTTP endpoint just reads the latest durable snapshot.

**Hybrid category ownership.** `fabro-agent` owns the category taxonomy
(it sees memory documents, skills, MCP registration, and session
history); `fabro-llm` exposes narrow estimation helpers. Neither crate
leaks the other's concerns.

**Provider count is async and non-blocking.** A spawned task calls
`Client::count_input_tokens(..., PreferProvider)` with a clone of the
request. It is cancelled via `close_token` when the session closes.
Failures produce a warning on the snapshot, not a stage error.

**`available: false` instead of 4xx for known-but-unobserved stages.**
The sidebar needs stable empty states; HTTP errors only mean the run or
stage doesn't exist.

```mermaid
flowchart TB
    A[Session::build_request] --> B[build_local_snapshot\nLocalEstimate]
    B --> C[emit ContextWindowSnapshot]
    C --> D{provider count\nspawned task}
    D -- success --> E[scaled_snapshot\nProviderApiScaledBreakdown]
    D -- failure --> F[warning appended to local snapshot]
    E --> G[emit ContextWindowSnapshot]
    G --> H[run_state reducer\nupdates StageProjection.context_window]
    F --> H
    H --> I[GET context-window endpoint\nreturns projection]
```

**`ToolSource` on every `RegisteredTool`.** All 20+ `make_*_tool` call
sites are updated to set `ToolSource::Native`; MCP tools get
`ToolSource::Mcp { server_name }` at registration time;
`make_use_skill_tool` gets `ToolSource::Skill`. A parallel
`definitions_with_source_for_policy` method preserves existing
`definitions_for_policy` behaviour unchanged.


### Fabro Details

<details>
<summary>Ran 8 stages in 90m 57s for $70.01</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 1m 59s | – | 0 |
| preflight_lint | 2m 11s | – | 0 |
| implement | 45m 14s | $48.47 | 0 |
| simplify_opus | 25m 50s | $18.29 | 0 |
| simplify_gpt | 6m 12s | $3.25 | 0 |
| verify | 8m 59s | – | 0 |
| **Total** | **90m 57s** | **$70.01** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
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.", 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="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"]
    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 -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 19:58:18 -04:00
Bryan Helmkamp
bad8c0baf5
Sidebar polish: visit syntax, multi-select archive, settings logos, workspace prefs (#376)
## Summary
- Render repeated stage visits as `verify@2` in the sidebar (and
waterfall/header/artifacts) to match Fabro's stage-reference syntax
instead of the parenthesized `verify (2)` form.
- Persist runs workspace preferences across sessions.
- Add multi-select with bulk archive/unarchive on the runs list view.
- Show provider logos on settings/models and integration logos on
settings/integrations, with a slightly wider logo-to-text gap.

## Test plan
- [ ] `cd apps/fabro-web && bun test` passes.
- [ ] Sidebar shows `verify`, `verify@2`, `verify@3` for a looped node
on a run with multiple visits.
- [ ] Runs list: select multiple runs and bulk archive/unarchive.
- [ ] Workspace preference on the runs page persists after reload.
- [ ] Settings → Models and Settings → Integrations render
provider/integration logos with the new spacing.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:14:06 -04:00
Bryan Helmkamp
044ec78f4d
Persist runs workspace preferences
Restore saved Runs view/filter/sort settings on plain /runs visits while keeping explicit URL params authoritative.
2026-05-23 16:45:20 -04:00
Bryan Helmkamp
6420987edb
Widen logo-to-text gap on settings/models and settings/integrations
Bumps the flex gap from 12px to 16px so the logo chip and the name/help block
breathe a little more.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:31:01 -04:00
Bryan Helmkamp
d3129d87d9
Show integration logos on settings/integrations page
Vendor brand icons for GitHub, Slack, Microsoft Teams, Discord, Linear, and
Jira under apps/fabro-web/public/images/integrations/ and render each one in
the same light chip used on the providers page. Refactors the existing rows
into IntegrationRow so name + help sit alongside the logo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:30:34 -04:00
Bryan Helmkamp
42402f0487
Show provider logos on settings/models page
Vendor the 9 SVGs from models.dev under apps/fabro-web/public/images/providers/
and render each one in a light chip alongside the provider name and model count.
LiteLLM has no logo on models.dev; the onError handler falls back to an initial
in the same chip style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:23:25 -04:00
Bryan Helmkamp
7c10aa3f1e
Add multi-select with bulk archive/unarchive to runs list view
Adds a leading checkbox column with a tri-state select-all header and
a fixed bottom action toolbar that surfaces selection count plus
Archive and Unarchive buttons. Selection clears when pagination,
sort, or filters change. Bulk actions fan out to the existing single-
run endpoints via Promise.allSettled and report per-run outcomes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:16:31 -04:00
Bryan Helmkamp
eda9e8855e
Make runs list Repo, Title, Workflow, and Changes columns server-side sortable
Extend the RunsSort enum and sort_runs() with case-insensitive ordering
for repo, title, and workflow names, and total line changes (additions
+ deletions) for changes. Swap the corresponding `<th>` cells in the
runs list view to `<SortHeader>` so every column can toggle asc/desc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:07:01 -04:00
Bryan Helmkamp
0f583f8e8b
Hide runs list pager when total run count fits on one default page
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:59:03 -04:00
fabro-sh-0530[bot]
dbe3e3966d
Migrate sandbox config to named environments; add InterviewOption metad… (#372)
## Summary

Two related changes land together: the sandbox configuration surface is
replaced with a named-environment model, and `InterviewOption` gains
`description` and `preview` fields needed for the mid-stage agent
interview tools described in the plan.

## What changed

### Named environments (was `[run.sandbox]`)

`[run.sandbox]` and its provider-specific sub-tables
(`[run.sandbox.daytona]`, `[run.sandbox.docker]`) are replaced by a
two-level model:

- **`[environments.<slug>]`** — reusable catalog entries with a unified
shape: `provider`, `image`, `resources`, `network`, `lifecycle`,
`labels`, `volumes`, `env`.
- **`[run.environment] id = "<slug>"`** — selects which environment a
run uses.
- **`[run.environment.<field>]`** — sparse run-level overrides applied
on top of the selected environment.

The OpenAPI schema drops `RunSandboxSettings`, `DaytonaSettings`,
`DaytonaSnapshotSettings`, `DaytonaNetworkLayer`, and `DockerSettings`
in favour of `EnvironmentSettings`, `RunEnvironmentSettings`, and the
new sub-schemas (`EnvironmentImageSettings`,
`EnvironmentResourcesSettings`, `EnvironmentNetworkSettings`,
`EnvironmentLifecycleSettings`, `EnvironmentVolumeSettings`). The
`--sandbox` CLI flag becomes `--environment`.

All docs, example configs, `.fabro/project.toml`, and the
automation-detail / run-settings UI panels are updated to the new shape.
The run-settings page renames "Sandbox" → "Environment" and reads from
the new field paths.

### `InterviewOption` metadata fields

`description` and `preview` are added to the canonical `InterviewOption`
type (OpenAPI, helpers.ts, interview-dock, human-qa renderer). Both are
treated as untrusted model-authored text — stored and displayed as plain
strings, never rendered as HTML. The `interview-dock` test asserts that
raw HTML in `preview` is not rendered. Option `description` is shown as
secondary text under the label in choice and multi-select buttons.

### `StageModelUsage` projection

`provider_used` on `RunStageInfo` and stage projections is promoted from
a freeform object to a typed `StageModelUsage` schema (with `mode`,
`provider`, `model`, `reasoning_effort`, `speed`). The
`extractStageModel` event-scraping helper is replaced by
`formatStageModelUsageLabel` and `stageModelUsageTitle`, which work
directly from the projection field. The `Stage` interface gains
`providerUsed` and the `EventsToolbar` consumes it.

### Other schema additions

`ReasoningEffort` enum, `small_default` on model info,
`SubAgentProjection`/`SkillsProjection`/`McpServerProjection` inline in
stage projections, and `TodoListProjection` moved from the run-state
top-level `todos_by_list` map into per-stage `todos`.

### Plan summary

- Replace `[run.sandbox]` config with `[environments.<slug>]` +
`[run.environment]` selection across config, OpenAPI, UI, and docs.
- Extend `InterviewOption` with `description` and `preview`; render
`description` in choice/multi-select buttons.
- Promote `provider_used` to a typed `StageModelUsage` schema; drop
event-scraping in favour of the projection field.
- Add `ReasoningEffort`, `small_default`, subagent/skills/MCP
stage-projection schemas to OpenAPI.


### Fabro Details

<details>
<summary>Ran 9 stages in 93m 2s for $48.56</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 39m 28s | $35.95 | 0 |
| simplify_opus | 22m 6s | $8.66 | 0 |
| simplify_gpt | 7m 18s | $1.66 | 0 |
| verify | 6m 33s | – | 0 |
| fixup | 12m 34s | $2.29 | 0 |
| **Total** | **93m 2s** | **$48.56** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
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.", 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="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"]
    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 -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Fabro <fabro@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 15:47:33 -04:00
fabro-sh-0530[bot]
f73f2a53f3
Replace queued with pending/runnable and add approval flow (web + API s… (#371)
## Summary

Replaces the single `queued` pre-execution state with explicit `pending`
and `runnable` states, and wires approve/deny actions for
parent-generated child runs that require human approval before they can
execute. This diff covers the web UI and OpenAPI spec layers of that
change.

## What changed

**Run status model**
- `queued` is removed from all TypeScript types, display maps, column
routing, and tests.
- `pending` (awaiting approval) and `runnable` (eligible for the
scheduler) replace it as distinct board columns and `RunStatus` variants
with their own labels and colors (`runnable` gets cyan; `pending` stays
muted).

**Approval actions**
- New `approveRun` / `denyRun` API calls in `run-actions.ts` invoke the
new `POST /runs/{id}/approve` and `POST /runs/{id}/deny` endpoints.
- `canApprove` predicate requires both `status.kind === "pending"` and
`lifecycle.approval?.state === "pending"` — a run whose status is
pending but has no approval record does not expose the action.
- `useApproveRun` / `useDenyRun` mutations in `mutations.ts` follow the
same pattern as `useCancelRun`.
- `ActionsMenu` in `run-detail.tsx` gains Approve (lifecycle group) and
Deny (destructive group) menu items.

**Board and event plumbing**
- `columnForStatus` now routes `pending → pending column` and `runnable
→ runnable column`; `submitted` stays in the pending column.
- `BOARD_STATUS_EVENTS` and `RUN_SUMMARY_EVENTS` replace `run.queued`
with `run.start_requested`, `run.pending`, `run.approved`, `run.denied`,
and `run.runnable`.
- The `pending` column is hidden when empty (same behaviour the old
`queued` column had).

**Waterfall phases (`run-phases.ts`)**
- `queued` phase is removed; `pending` and `runnable` phases are added
in order.
- The submitted phase closes at `run.start_requested` rather than
`run.queued`.
- Each phase derives its timestamps from its own event rather than a
single `firstTs` lookup, making multi-phase pre-execution timelines
accurate.

**OpenAPI spec**
- `POST /api/v1/runs/{id}/approve` and `POST /api/v1/runs/{id}/deny`
endpoints added with 200/404/409 responses.
- `startRun` description updated to describe the pending/runnable
branching behaviour.
- `cancelRun` description updated to reference `pending`/`runnable`
instead of `queued`.

### Plan Summary

- **Task 3** (OpenAPI schema additions for approve/deny endpoints) —
complete in this diff.
- **Task 6** (Web UI surfaces: board columns, run-detail actions,
waterfall phases, event subscriptions) — complete in this diff.
- **Task 7** (doc cleanup: references to `queued` replaced in plans,
brainstorms, and QA docs) — complete in this diff.


### Fabro Details

<details>
<summary>Ran 9 stages in 127m 37s for $104.98</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 29s | – | 0 |
| implement | 92m 10s | $91.53 | 0 |
| simplify_opus | 18m 35s | $10.65 | 0 |
| simplify_gpt | 7m 36s | $2.81 | 0 |
| verify | 3m 42s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **127m 37s** | **$104.98** | **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.", 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]

    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: fabro <fabro@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 15:34:33 -04:00
Bryan Helmkamp
fd63f4b523
feat(api): expose run size buckets (#369)
## Summary

Adds a stable `Run.size` API field so clients can bucket workflow runs
by current best-effort billed usage without introducing a separate
cost-estimation system. The field uses the `RunSize` enum and serializes
as uppercase `XS`, `S`, `M`, `L`, or `XL`.

## Changes

- Derives run size from terminal billed totals when available, otherwise
from the existing projected stage usage while a run is still active.
- Exposes `size` on `Run` in the OpenAPI contract and regenerated
TypeScript client.
- Preserves existing `Run.billing` behavior so live/provisional usage
only affects `size`, not the nullable billing summary.

## Verification

- `cargo nextest run -p fabro-types run_size`
- `cargo nextest run -p fabro-store
summary_size_tracks_current_projected_usage_before_terminal_conclusion`
- `cargo nextest run -p fabro-api
run_summary_json_matches_openapi_shape`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd apps/fabro-web && bun run typecheck`
- `git diff --check`
- `cargo build --workspace`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-23 14:41:41 -04:00
Bryan Helmkamp
1cd5d89316
Runs list view: table layout, server-side sort/pagination, redesigned toolbar (#367)
## Summary

Overhauls the `/runs` list view and consolidates the two runs endpoints
that backed it.

**API**
- Removes `GET /api/v1/boards/runs`, `PaginatedBoardRunList`, and
`BoardColumnDefinition`. The board view is now a pure frontend
rendering.
- `GET /api/v1/runs` gains `status` (repeatable `BoardColumn`), `sort`
(`created_at | updated_at | status | elapsed`, default `created_at`),
and `direction` (`asc | desc`, default `desc`).
- `BoardColumn` enum gains `removing`; default behavior hides
Removing-status runs, opt in with `?status=removing`.
- `PaginationMeta` gains an optional `total: int64`; `list_runs` fills
it in (free — it already filters all runs in memory before paging).

**List view UI**
- Renders as a real `<table>` with column headings instead of horizontal
cards.
- Sortable Status, Elapsed, Created, and Updated headers — click to
toggle direction, click another to switch sort key (resets to desc). URL
params drive `sort`/`direction`/`page`/`size`.
- New pager footer with rows-per-page selector (10/25/50/100), `Page X
of Y`, and first/prev/next/last icon buttons.
- Toolbar redesigned into left (search + filter buttons for
Time/Repo/Workflow + archived toggle) and right (column picker + view
toggle) sections. Filter buttons use Headless UI `Menu` popovers; the
column picker uses Headless UI `Listbox` with `multiple` for
multi-select. Hidden columns persist via `?hide=...`.

**Tests**
- 589 server tests pass, including new coverage for status filter
(single + repeated), Removing opt-in, sort × direction with `id desc`
tiebreak, and status-bucket sorting.
- Frontend tests updated for the matcher-based cache invalidation and
the new `buildBoardColumns` signature; 435 pass (3 pre-existing
`RunDetail full-height` failures unrelated to this change).

## Test plan

- [ ] `cargo build --workspace`
- [ ] `cargo nextest run -p fabro-server`
- [ ] `cd lib/packages/fabro-api-client && bun run generate` — no diff
(already regenerated and committed)
- [ ] `cd apps/fabro-web && bun run typecheck && bun test`
- [ ] Manual: visit `/runs` — board view still renders all columns in
canonical order, Removing runs hidden, archived toggle works.
- [ ] Manual: visit `/runs?view=list` — table renders with sortable
headers; clicking a header updates URL; pager advances; changing
rows-per-page resets to page 1; column picker hides/shows columns and
round-trips via `?hide=`.
- [ ] Manual: `curl '/api/v1/boards/runs'` → 404; `curl
'/api/v1/runs?status=removing'` returns only removing runs; `curl
'/api/v1/runs?sort=status&direction=asc'` returns runs grouped by status
bucket.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:25:58 -04:00
fabro-sh-0530[bot]
7d8506cb7c
feat: add manual retry for failed and dead runs (#362)
## Summary

Adds a **Retry** action that creates a fresh run from a failed or dead
run's captured durable definition. The new run gets a new ID, records
`retried_from: <source_run_id>`, and is immediately queued. The source
run is left entirely unchanged.

### Plan Summary

- `retried_from: Option<RunId>` added to `RunCreatedProps`,
`RunProjection`, and `Run` (API surface); defaults to `null` for
backward compat with legacy events.
- `POST /api/v1/runs/{id}/retry` → `201` with the new run; eligible
source states are `failed` (except `reason=cancelled`) and `dead`.
- New `retry_run` workflow operation (modeled after `fork`) creates a
new run store, appends `run.created` with `retried_from` set, then
`run.submitted`. No checkpoints, sandbox state, PR links, questions, or
conclusions are copied.
- `start_run` handler refactored into a reusable `queue_run_start(state,
id, resume)` helper so retry can queue the new run through the same
path.
- Web: `canRetry` predicate, `useRetryRun` mutation, **Retry** menu item
in `ActionsMenu`, "Retried from" link in `RunSummaryPanelView`, and
navigation to the new run on success (deduplicated via
`lastProcessed.retry`).
- OpenAPI spec updated; generated Rust and TypeScript client types
regenerated.

## Key design decisions

**No runtime state is copied.** Only the durable definition fields
(`graph`, `settings`, `labels`, `git`, `manifest_blob`,
`definition_blob`, `fork_source_ref`, `parent_id`, `title`) are
forwarded to the new `RunCreated` event. Checkpoints, sandbox, billing,
PR links, and pending controls are left in the source run.

**`queue_run_start` extraction.** `start_run` was restructured to
extract a `queue_run_start(state, id, resume) -> Result<(), ApiError>`
helper, so the retry handler can reuse the exact same queueing path
without duplicating logic.

**`lastProcessed.retry` deduplication.** The UI effect that fires on
`retryMutation.data` checks `state.lastProcessed.retry === result`
before navigating, so React StrictMode double-invocation or re-renders
won't push duplicate navigations.

**Demo mode guard.** `canRetry` is gated by `!demoMode` at the call site
so the button is hidden in demo mode and never navigates to a missing
demo run.


### Fabro Details

<details>
<summary>Ran 9 stages in 65m 2s for $51.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 53s | – | 0 |
| preflight_lint | 2m 8s | – | 0 |
| implement | 36m 51s | $36.06 | 0 |
| simplify_opus | 16m 22s | $14.46 | 0 |
| simplify_gpt | 3m 10s | $1.18 | 0 |
| verify | 3m 39s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **65m 2s** | **$51.70** | **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.", 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]

    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>
2026-05-23 14:07:24 -04:00
fabro-sh-0530[bot]
0f36304d2c
Add small_default model role and async generated run titles (#368)
## Summary

Introduces a `small_default` catalog role for identifying each
provider's small/cheap utility model, and uses that model to
asynchronously generate human-readable run titles when the caller
doesn't supply one explicitly.

### Plan Summary

- **Catalog**: Add `small_default: Option<bool>` to
`ModelCatalogSettings` and `small_default: bool` to the `Model` type.
Mark built-in small defaults: `claude-haiku-4-5` (Anthropic),
`gpt-5.4-mini` (OpenAI), `gemini-3.1-flash-lite-preview` (Gemini).
Validate that each provider has at most one small default; zero is
allowed with fallback to the provider's regular default.
- **Helpers**: Add `small_default_for_provider` and
`small_default_for_configured_ids` on `Catalog`, mirroring the existing
`default_for_provider` / `default_for_configured_ids` /
`probe_for_provider` pattern.
- **Title generation**: New `run_title_generation` module in
`fabro-server` builds a prompt from workflow identity, goal, and raw run
inputs, calls `generate_object` with `max_tokens(64)` and a 10 s
timeout, normalizes output (trim, reject blank/control, truncate to 100
chars), and falls back to the deterministic title on any failure.
- **Server integration**: In the create-run handler, if no explicit
`RunManifest.title` was supplied and at least one LLM provider is ready,
spawn a detached task that generates a title and appends
`run.title.updated` — but only if the title hasn't been changed by a
concurrent user PATCH.

## What changed and why

**`small_default` vs `default`** — the existing `default` role drives
normal model selection for workflow execution and must not be disturbed.
`small_default` is a separate, additive role for lightweight metadata
work. The two roles are intentionally independent so teams can promote a
newer large model to `default` without accidentally routing title
generation there.

**Best-effort, async title enrichment** — run creation is kept
synchronous and reliable. The title task is fire-and-forget: LLM errors,
timeouts, and validation failures all silently leave the deterministic
title in place. The stale-title guard (`current.title !=
deterministic_title`) prevents the async task from clobbering a
concurrent user edit via `PATCH /runs/{id}`.

**No redaction** — per the design goal, raw input values are forwarded
to the model. This is noted explicitly in the prompt and in the module
docs.

**Prompt size bounding** — each of the three prompt sections (workflow
identity, run inputs, workflow summary) is independently capped at 4 000
characters with a `...[truncated]` marker so pathological inputs can't
produce enormous requests.

## Public interface changes

- `Model` gains `small_default: bool` in the Rust type, OpenAPI schema,
and generated TypeScript client.
- `MAX_RUN_TITLE_CHARS` is now `pub` in `fabro-types` so the
title-generation module can reuse the same limit.
- Config docs (`models.mdx`, `litellm.mdx`) document `small_default =
true` alongside `default` and `probe`.


### Fabro Details

<details>
<summary>Ran 9 stages in 61m 23s for $32.17</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 52s | – | 0 |
| preflight_lint | 2m 5s | – | 0 |
| implement | 29m 16s | $23.26 | 0 |
| simplify_opus | 17m 15s | $6.28 | 0 |
| simplify_gpt | 7m 1s | $2.63 | 0 |
| verify | 3m 7s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **61m 23s** | **$32.17** | **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.", 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]

    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>
2026-05-23 13:48:39 -04:00
fabro-sh-0530[bot]
f81c5b96b1
Move agent state (todos, subagents, skills, MCP) onto StageProjection (#364)
## Summary

The `GET /runs/{id}` response now carries per-stage agent state — todos,
subagents, skills, and MCP server status — directly on each
`StageProjection`, unblocking the agent-stage sidebar without any new
endpoints.

## What changed and why

**`RunProjection.todos_by_list` removed.** The run-level map was the
only home for todos, but every list was already 1:1 with a stage (keyed
by `openai_plan:<session>` or `anthropic_tasks:<root_session>`). Moving
todos onto `StageProjection.todos: Option<TodoListProjection>`
eliminates the awkward cross-stage indirection with no loss of
expressiveness.

**Four new fields on `StageProjection`** (`todos`, `subagents`,
`skills`, `mcp_servers`) plus five new projection-side types
(`SubAgentProjection`, `SubAgentStatus`, `SkillsProjection`,
`ActivatedSkill`, `McpServerProjection`, `McpServerStatus`). All
colocated in `run_projection.rs`; no new modules. Four existing
event-payload types are reused directly (`TodoListProjection`,
`AgentSkillSummary`, `AgentSkillActivationSource`,
`AgentMcpToolSummary`) via `with_replacement(...)` in
`fabro-api/build.rs` so progenitor doesn't generate parallel `ApiFoo`
aliases.

**Reducer rerouting** (`run_state.rs`): `TodoCreated/Updated/Deleted`
now resolve the owning stage via the existing
`stage_at_stored_or_current_visit` helper and mutate `stage.todos`
directly. Eight new event arms handle
`AgentSubSpawned/Completed/Failed/Closed`,
`AgentSkillsDiscovered/Activated`, and `AgentMcpReady/Failed` using the
same `stage_at_stored_or_visit` pattern already used for other agent
events.

**Design decisions called out in the plan:**
- `SkillActivated` appends every activation (event-sourced replay
semantics); the UI can collapse if needed.
- `upsert_mcp_server` replaces by `server_name`, so a server that
recovers after a failure shows its final state.
- `SubAgentStatus` and `McpServerStatus` are projection-side enums,
intentionally distinct from the runtime per-process status types in
`fabro-agent`.

**OpenAPI + TS client** updated in lockstep: new schemas added,
`todos_by_list` removed from `RunProjection`, `bun run generate`
regenerated 20+ new model files.

### Plan Summary

- Extend `StageProjection` with `todos`, `subagents`, `skills`,
`mcp_servers` + supporting types in `run_projection.rs`
- Remove `RunProjection.todos_by_list` (no cross-stage use case)
- Reroute todo reducer handlers; add 8 new event arms for
subagent/skill/MCP events
- Wire reused types through `fabro-api/build.rs` `with_replacement`;
regenerate TS client
- Update all existing todo reducer tests to read from `stage.todos`; add
new test modules for subagent, skill, and MCP event families
- Update stale comment in `run-events.ts`


### Fabro Details

<details>
<summary>Ran 9 stages in 38m 52s for $13.29</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 16m 8s | $7.80 | 0 |
| simplify_opus | 10m 34s | $3.91 | 0 |
| simplify_gpt | 3m 23s | $1.58 | 0 |
| verify | 3m 51s | – | 0 |
| fmt | 2s | – | 0 |
| **Total** | **38m 52s** | **$13.29** | **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.", 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]

    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>
2026-05-23 13:26:51 -04:00
fabro-sh-0530[bot]
7e6052bec3
Surface reasoning_effort + speed in stage badge end-to-end (#363)
## Summary

The run page stage badge showed only the model name. This PR plumbs
`reasoning_effort` and `speed` from the LLM call site all the way
through the event stream, store projection, API, and UI so the badge now
renders `gpt-5.5 · high`.

### Plan Summary

- **Event props** — `AgentSessionActivatedProps` and `StagePromptProps`
gain `reasoning_effort: Option<ReasoningEffort>` and `speed:
Option<Speed>` with `serde(default, skip_serializing_if)` for
back-compat.
- **Typed projection** — `provider_used: Option<serde_json::Value>` is
replaced by `Option<StageModelUsage>`, a proper struct in `fabro-types`
with factory methods (`from_prompt_props`,
`from_agent_session_activated`). The freeform JSON bag is gone.
- **Emission sites** — `ActivationLeaseOptions` carries the new fields;
`emit_stage_prompt()` (new shared helper) resolves
`EffectiveRequestControls` via the backend and stamps them on
`Event::Prompt`. `AgentHandler` and `PromptHandler` both call this
helper instead of building the event inline.
- **ACP path** — `AgentAcpStarted` no longer writes `provider_used`; the
canonical source is the later `AgentSessionActivated` event, which is
already emitted for ACP steering sessions. Runs without a hub
legitimately leave `provider_used` unset.
- **OpenAPI** — new `StageModelUsage` and `ReasoningEffort` schemas
replace the `object | null` bag; `build.rs` maps both to the canonical
Rust types; a new `stage_model_usage_round_trip` integration test
enforces the parity requirement.
- **UI** — `extractStageModel` (event-scanning heuristic) is deleted;
replaced by `formatStageModelUsageLabel` and `stageModelUsageTitle` that
read directly off `selectedStage.providerUsed`. `parseFanInOutcome` now
sources the reducer model from `stage.prompt` instead of
`prompt.completed`.

### Key design decisions

**No type sprawl**: `fabro_model::ReasoningEffort` and `Speed` are
reused verbatim via `with_replacement` in `build.rs` — no parallel
enums.

**ACP behavior change**: previously `AgentAcpStarted` wrote a bespoke
`provider_used` blob and a later `AgentSessionActivated` would be
ignored for ACP sessions. Now `AgentSessionActivated` is the single
write path for all modes; ACP runs that never activate a steering hub
correctly leave `provider_used = null`. The integration test (`acp.rs`)
is updated to assert the new shape, and the unit test is renamed
`agent_acp_started_alone_leaves_stage_provider_used_unset` to document
intent.

**`emit_stage_prompt` helper**: both `AgentHandler` and the existing
prompt path share one function to avoid the two call sites drifting
apart again.


### Fabro Details

<details>
<summary>Ran 9 stages in 98m 44s for $65.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 30s | – | 0 |
| implement | 42m 19s | $26.85 | 0 |
| simplify_opus | 38m 3s | $35.17 | 0 |
| simplify_gpt | 9m 20s | $3.68 | 0 |
| verify | 3m 38s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **98m 44s** | **$65.70** | **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.", 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]

    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: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
2026-05-23 13:14:55 -04:00