Commit graph

144 commits

Author SHA1 Message Date
Bryan Helmkamp
2902b8c773
fix(web): harden build version detection 2026-07-25 15:15:25 -04:00
Bryan Helmkamp
695a981f42
feat(web): tell open tabs when a new build ships
A tab left open across a deploy keeps running the previous build's
JavaScript indefinitely. index.html is fetched only on a full page load,
all later navigation is client-side, and hashed bundles are served
`immutable`, so nothing reveals that the code is stale. This produced a
false-positive bug report where two correctly-deployed fixes appeared to
be missing.

Publishes a build id and offers a reload when the running document falls
behind. The toast never reloads on its own; the only automatic reload is
recovery from a chunk that no longer exists.

Build id derivation
-------------------
The obvious approach — hash the emitted asset filenames, which already
embed content hashes — does not work: Bun's minified identifier naming is
not deterministic. Building an unchanged tree twice produces byte-different
output roughly one run in three (same length, ~100k differing bytes, all of
it mangled names). Output hashes therefore move with no source change,
which would fire the toast on redeploys of identical code and train people
to ignore it.

The id is instead derived from the bundle's source inputs, so it changes if
and only if something we control changed. Verified stable across eight
consecutive builds while the entry hash flipped between both variants.

This non-determinism also means two builds of the same commit embed
different bytes into the server binary, which is worth addressing
separately for reproducible builds.

Detection
---------
SWR with `refreshInterval` + `revalidateOnFocus`, per the repo's React
effects policy. SWR does not poll while the document is hidden, so
background tabs stay quiet without extra gating. Unknown state on either
side — missing meta tag, failed fetch, 503 during a dev rebuild — never
produces a prompt.

Stylesheet hashing
------------------
Tailwind's output was stable-named and therefore served `no-cache`, letting
a tab revalidate into new CSS while running old JS. Tailwind purges unused
classes per build, so classes the old bundle still emits could silently
lose their styles. It is now content-hashed and moves with the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:45:26 -04:00
Bryan Helmkamp
67ed7af026
Remove the model request status line above the stage toolbar
The "Model request · waiting on <model>" readout sat directly above the
Chat/Thread/Debug toolbar and appeared and disappeared as requests opened
and closed, shifting the toolbar underneath it.

Drops the StageInferenceIndicator component and everything that existed
only to feed it: the inference/runSettled prop threading through
RunStages, and StageActivity's watchdogTimedOut field. The watchdog.timeout
event now falls through to the same ignore path it always would have, since
it was never in STAGE_ACTIVITY_EVENT_TYPES.

The run-events invalidations for watchdog.timeout and agent.llm.* stay:
they still refresh stage events for the Debug tab and run state for the
insights sidebar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 08:38:46 -04:00
Bryan Helmkamp
d4f619bc2a
fix: clean up inference observability 2026-07-24 22:50:00 -04:00
Bryan Helmkamp
6659ae768a
feat(events): make inference in-flight state observable
During a long LLM turn the durable event stream was silent: between
`agent.tool.completed` and the next `agent.message` nothing was emitted,
so "the model is generating" and "the worker is wedged" were
indistinguishable from the run store, SSE, or the UI.

The signal already existed. `AssistantTextStart` fired at exactly the
right point — after `build_request()`, after compaction, immediately
before the stream opens — then was classified as streaming noise and
thrown away. This promotes it rather than inventing a new one.

Two events, each asserting only what is provable when it is emitted:

- `agent.llm.started` carries the *requested* provider/model. No usage,
  no cost, no context window: none of it exists yet, and failover can
  re-target, so `agent.message` stays authoritative for what answered.
- `agent.llm.first_output` is edge-triggered on the first output of an
  attempt and names what arrived. `ToolCall` is required, not optional:
  a turn that opens with a tool call produces no text or reasoning
  delta, so a latch keyed on those two would stay silent for exactly
  the tool-heavy rounds where liveness matters most.

`agent.llm.retry` now also fires on the one previously invisible
mid-turn path — a stream that ends without a finish event, which
replays the turn and discards its output with nothing to show for it.
Its `attempt` field was already fed by two independent counters, so an
optional `phase` (open | consume) names which loop it counts.

`StageProjection.inference` projects the open bracket. `Some` means
"the event log contains an unclosed inference bracket", not "the model
is computing now" — a SIGKILLed worker leaves it open, which is the
truthful statement of what we know, and `watchdog.timeout` remains the
authority on actually-stuck.

The close is the subtle part. Terminal cancel and wall-clock timeout
tear the session down through `discard_session` without emitting a
message, error, or interrupt, so a session-lifecycle backstop is
required. It has to be `agent.session.ended`, not
`agent.session.deactivated`: deactivation is emitted by `lease.release()`
*before* the forwarder drains queued agent events, so a queued
`agent.llm.started` can arrive after it and re-open the bracket. But
`agent.session.ended` carries no stage identity, so the close takes
ordering from the event and identity from the projection, scanning for
brackets the ending session opened. A normal stage lookup there finds
no target and silently no-ops.

Presentation states what the log proves and nothing more: no progress
bar or ETA (no completion estimate exists), "reasoning" only when the
provider sent reasoning output, elapsed counted since the request
opened, and no live animation once the run is terminal.

Scope is session-backed agent stages. One-shot completions call
`client.complete` directly and never build a session; covering them
means moving the emit point into `fabro-llm`, filed as a follow-up.

`agent.output.start` was never persisted — it existed in a name map,
an `unreachable!` arm, and docs — so the rename carries no migration
risk. Corrects `events.md`, which documented it as a real emitted
event, and the v2 proposal, which mapped it to `message.part.started`
despite it firing before the request opens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:47:51 -04:00
Bryan Helmkamp
e739f86f6a
Merge remote-tracking branch 'origin/main' into feat/stage-chat-view
# Conflicts:
#	apps/fabro-web/app/routes/run-stages.test.ts
#	apps/fabro-web/app/routes/run-stages.tsx
2026-07-24 17:39:48 -04:00
Bryan Helmkamp
b1bf868c9d
refactor(web): simplify stage chat projection 2026-07-24 17:29:04 -04:00
Bryan Helmkamp
7b82a150ad
refactor(web): simplify thread DNA selection identity 2026-07-24 17:23:08 -04:00
Bryan Helmkamp
110058bb4b
fix(web): correct tool-group boundaries and DNA timeline attribution
Empty-text `agent.message` events were discarded, erasing the boundaries
between batches of tool calls. Eight short shell calls issued across five
model responses collapsed into one `Bash x8` group whose DNA bar spanned
the model-response gaps between them, showing a misleading six-minute
duration. Filtering could recreate the same artificial adjacency.

- Always emit an assistant turn for `agent.message`, carrying
  `tool_call_count` so a text-free response renders as
  "Requested N tool calls" instead of a blank row.
- Derive grouping and DNA timing from the complete turn stream, then
  apply kind/search filters as a pure visibility pass over display
  items. Hiding a tool can no longer inflate an adjacent Agent bar, and
  hiding an Agent can no longer merge the tool groups on either side.
- Give a tool group the wall-clock envelope of its children (earliest
  start to latest end) rather than the sum of their durations or the
  span to the last array element. Row, details header, DNA bar, and
  tooltip all read the same values.
- Advance the DNA previous-activity cursor by the maximum observed end
  so out-of-order or overlapping completions cannot move it backward.

Frontend only: no event, persistence, or API schema changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 13:56:57 -04:00
Bryan Helmkamp
eb83539a18
Merge origin/main into feat/stage-execution-identity-on-resume
Resolves conflicts with the shared-checkout parallel rewrite (#607) and the
cached-run/billing dedup (de60eb900):

- handler/parallel.rs: rebuilt on main's shared-checkout version. Branch
  ordinals are still reserved inside the branch task right before
  ParallelBranchStarted (with graph_visit/resumed_from_stage_id), and the
  reserved StageScope is shared with post-await error paths via a OnceLock
  slot instead of main's dispatch-time visit=1 scope, so completion events
  are never emitted under a guessed ordinal.
- billing.rs: keep this branch's run_stage_from_projection (RunStage grew
  graph_visit/resumed_from_stage_id and a typed id), adopt main's
  state.cached_run() and drop the removed run_stage_from_stage_id import.
- run_projection.rs: adopt main's typed parallel_results
  (Option<Vec<ParallelBranchResult>>).
- run_event/misc.rs: union of both sides' imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:07:35 -04:00
Bryan Helmkamp
78fea736e3
fix: harden stage execution identity on resume 2026-07-24 09:37:05 -04:00
Bryan Helmkamp
cd706646c6
feat: treat resumed in-flight nodes as new stage executions
A node cancelled (or lost to a crash) mid-flight and then resumed now
starts a new stage execution with the next StageId ordinal (work@2)
instead of reusing and clearing the cancelled execution's projection.
The old execution stays immutable with its own events, session, output,
timing, billing, and termination state.

Engine:
- Add a run-scoped StageExecutionTracker on RunServices with per-node
  high-water marks. Ordinals are reserved after the StageStart hook
  passes on the first attempt (retries reuse the reservation), ensured
  at the composite checkpoint pre-step for hook-skips, and reserved in
  on_terminal_reached for terminal nodes' synthetic events.
- Keep three concepts distinct: graph visit (max_visits/checkpoints,
  unchanged), stage execution ordinal (the @N in StageId), and handler
  attempt. The tracker is not checkpointed; the append-only stage event
  history is its durable source of truth.
- resume() seeds the allocator from the run projection and computes a
  node -> StageId provenance map of executions observed after the
  selected checkpoint, threaded through execute_persisted_run,
  RunSession, and InitOptions.

Events and projections:
- stage.started, parallel.branch.started, and checkpoint.completed
  carry optional graph_visit and resumed_from_stage_id; StageProjection
  stores both. Old events deserialize with None and legacy duplicate
  stage.started replays keep last-attempt behavior.
- The CheckpointCompleted reducer is envelope-first: diffs and
  skipped-stage synthesis attach to the exact execution StageId, an
  existing Retrying projection finalizes as Skipped without losing
  identity, and historical node_outcomes no longer create or collide
  with newer ordinals (node_visits remains a legacy fallback).

Handlers:
- Parallel fan-out reserves child ordinals through the shared tracker,
  derives worktree pass{N} from the parent's execution ordinal, and
  seeds branch contexts with explicit child stage scopes so branch
  lifecycle and nested handler events agree.
- Artifact capture and manager-loop child logs follow the ordinal.

API and UI:
- RunStage documents visit as the execution ordinal and adds optional
  graph_visit and resumed_from_stage_id; Rust and TypeScript clients
  regenerated.
- The web sidebar lists both executions chronologically; resumed stages
  show a "Resumed from" link in the stage detail header and hover
  popover, with the graph visit surfaced when it diverges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:00:37 -04:00
Bryan Helmkamp
af27e98e1c
refactor: simplify parallel handler and overview parsing
- Extract emit_branch_completed() to replace three near-identical
  ParallelBranchCompleted constructions; status now reads consistently
  from outcome.status
- Add context_diff_public() so parallel.rs and manager_loop.rs share the
  diff-minus-engine-internal-keys step; move context_diff tests next to
  the function in context.rs
- Replace fan_in's dead BranchShape struct with the canonical
  Vec<ParallelBranchResult> (from_value moves, so no payload cloning)
- Narrow parseParallelOverview to ParallelBranchSummary {id, status};
  its only consumer renders just those fields
- Drop helpers.test.ts's duplicate envelope() fixture in favor of the
  shared makeEventEnvelope

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:28:58 -04:00
Bryan Helmkamp
4621149b6e
Merge remote-tracking branch 'origin/main' into feat/shared-checkout-parallel 2026-07-24 06:54:32 -04:00
Bryan Helmkamp
0a39ba9e06
Shared-checkout parallel execution (recovered from run 01KY7YH7RYCJ1BDVTTP96ZA4HV)
Cumulative implement + simplify_fable diff recovered from the run's meta
branch (fabro/meta/01KY7YH7RYCJ1BDVTTP96ZA4HV, stage 006 diff.patch).
The run validated this tree clean: cargo nextest (7,007 passed), clippy,
fmt, TS client regen + typecheck, web tests (679 passed), docs check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 06:19:11 -04:00
Bryan Helmkamp
22238575ac
fix: report delete-specific run errors 2026-07-23 21:27:58 -04:00
Bryan Helmkamp
9adf24348b
refactor: simplify cancellation lifecycle code from review
- Extract the quadruplicated watchdog check-and-clear logic in
  schedule_worker_cancel_escalation into ManagedRun methods
  (escalation_still_current, clear_escalation_for)
- Derive strum::IntoStaticStr for WorkerRef instead of a hand-written
  variant-to-string match in kind()
- Use the generated AgentControlState constant instead of the raw
  "waiting_for_steer" literal in run-detail.tsx
- Replace optimisticCancellationRunId state with a boolean; the
  component is keyed by run id, so the stored id could only ever be
  this run's own

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:15:35 -04:00
Bryan Helmkamp
5cf1c7d183
Harden cancellation and interrupt lifecycles 2026-07-23 20:40:22 -04:00
André Mazoni
daa6f4cdbf
Increase LR graph zoom to 400% and remember zoom per direction (#581)
Some checks failed
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Raises the LR graph zoom ceiling from 200% to 400%. TB is unchanged at
200%.

Zoom and pan are now tracked separately per direction instead of shared.
Switching LR to TB and back restores the viewport you left in each mode,
so a round trip no longer loses your position. Previously a single
shared zoom value was clamped down whenever you switched into TB, which
meant going LR to TB and back cost you your LR zoom.

`run-overview.tsx` holds two view states, remembered per run under
`<runId>-TB` and `<runId>-LR`. `clampZoom` and `zoomAtPoint` take a
`direction` argument and apply the matching ceiling, so the
clamp-on-direction-change effect is gone. 24 tests in
`graph-viewport.test.ts`.

Requirements:
docs/brainstorms/2026-07-21-graph-zoom-lr-increase-requirements.md
Plan: docs/plans/2026-07-21-graph-zoom-lr-increase-plan.md

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Fabro <noreply@fabro.sh>
2026-07-21 16:06:25 -04:00
Bryan Helmkamp
12529cba2f
Fix squished avatars in runs list "By" column (#569)
Some checks failed
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) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
## Problem

In the runs list view, avatars in the **By** column render as squished
ovals.

## Cause

The "By" column `<td>` is `w-8` (32px) with `px-3` padding (24px total),
leaving ~8px of content width. The glyph sits inside the Tooltip's
`inline-flex`, so its wrapper is a shrinkable flex item that collapses
to that 8px. Since Tailwind Preflight sets `img { max-width: 100% }`,
the 20px avatar's width shrinks to ~8px while `size-5` keeps its height
at 20px — producing the squished oval.

## Fix

Wrap the glyph in `inline-flex shrink-0` so it keeps its 20px intrinsic
width and the auto-layout column grows to fit instead of compressing the
image. This also covers the non-user principal icon glyphs
(agent/system/slack/webhook/worker).

Typecheck passes.

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 13:25:13 -04:00
André Mazoni
c5dd5772d0
Keep run graph zoom/pan when switching tabs (#561)
Switching from a run's Overview tab to another tab and back reset the
graph zoom and position to the default. Now it holds.

## Why

The viewport (pan and zoom) lived in `RunOverview` component state.
Overview and Stages are sibling routes under `runs/:id`, so switching
tabs unmounts Overview and drops that state.

## Fix

`apps/fabro-web/app/routes/run-overview.tsx`: cache the viewport per run
outside the component so it survives the remount, and reset it when the
run id changes, since the route instance is reused when only the id
changes.

Added two tests: viewport restores on remount for the same run, and does
not carry across runs.

Does not persist across a full page reload (in-memory only).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-07-08 10:50:02 -04:00
André Mazoni
df4fee4dff
feat(web): trackpad pan + ⌘-scroll zoom on the run graph (#555)
## What

On **Runs → Overview**, the workflow graph now supports the standard
Figma/Excalidraw canvas interactions:

- **Two-finger scroll → pan**
- **⌘/Ctrl + scroll → zoom**, anchored under the cursor (mac trackpad
pinch works too — the browser delivers it as `ctrl+wheel`)

The graph already had drag-to-pan, stepped zoom (toolbar +/−), and
fit-to-window. This adds the missing wheel/trackpad input on top of that
existing transform state.



https://github.com/user-attachments/assets/15eac98b-2603-44c9-b438-7ee27034ccd7


## How

- **`app/lib/graph-viewport.ts`** (new) — pure, framework-free zoom
math: `zoomAtPoint` keeps the point under the cursor fixed while
scaling; `clampZoom` + zoom constants. Zoom becomes a continuous float
(was a discrete step index) so ⌘-scroll is smooth instead of jumping
between steps. Unit-tested (`graph-viewport.test.ts`), including the
cursor-anchor invariant.
- **`useElementEvent` in `hooks/effects.ts`** (new) — element-scoped,
non-passive listener, a sibling to the existing
`useWindowEvent`/`useDocumentEvent`. Non-passive is required so the
handler can `preventDefault()` the browser's own ⌘-zoom; a JSX `onWheel`
can't.
- **`routes/run-overview.tsx`** — coalesces zoom+pan into one `view`
state (atomic cursor-anchored updates), adds the wheel handler (plain
scroll → pan, ⌘/Ctrl → zoom), and `touch-none overscroll-contain` so a
horizontal swipe can't trigger browser back-nav.
- **`components/graph-toolbar.tsx`** — presentational continuous
interface; +/− buttons reuse `zoomAtPoint` (center-anchored). Deletes
the now-dead `graph-toolbar-constants.ts`.

## Testing

- `bun run typecheck` clean; `bun test` green (incl. 4 new viewport
tests).
- Verified live against a real 10-node run graph via Chrome DevTools:
two-finger pan tracks the scroll delta; ⌘+wheel zoom is cursor-anchored
(confirmed even with the cursor over a node); toolbar +/− step ×1.25 and
clamp/disable at 200%; fit-to-window sets a continuous scale; node
click/hover unaffected.

## Non-goals

- **Playground canvas** (`components/playground/canvas`) shares the same
hand-rolled pan/zoom pattern and also lacks wheel support — deliberately
out of scope; `graph-viewport.ts` is the seam to adopt it later.
- **No persistence** — zoom/pan stays ephemeral per visit, as it was
before.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:15:52 -04:00
fabro-sh-fabro[bot]
09d1a6036e
Fix workflow slug field to use kebab-case instead of snake_case (#556)
Multi-word workflow slugs typed into the New/Edit Automation form were
being silently converted to snake_case (e.g. `patch-cves` →
`patch_cves`), causing scheduled automations to resolve against a
non-existent directory and **silently never fire**.

## What changed

- `automation-form.tsx`: `onChange` for the Workflow slug field now
calls `kebabify()` instead of the removed `snakeify()`. The "create from
run" fallback prefill is updated the same way. Help text and placeholder
are updated to reflect dash-separated slugs.
- `snakeify()` is removed entirely (was only used in these two spots).
- `kebabify()` is unexported (it was `export function`; it's now only
used within the same file).
- `automations-new.test.tsx`: updates the pre-populate assertion from
`"fix_ci"` → `"fix-ci"`, adds a regression test that dashes are
preserved and `"Patch CVEs"` → `"patch-cves"`, and adds a unit test for
the `automationFormValuesFromRun` kebab fallback.

## Why kebab-case is correct

Workflow slugs are derived from on-disk directory names
(`.fabro/workflows/patch-cves/`), which are dash-separated by
convention. The backend validator already accepts dashes; `AutomationId`
actually forbids underscores. The snake_case behavior was a UI-only
outlier present since the form's first draft with no documented
rationale.

No backend changes are needed. Existing automations with a stored
snake_cased `workflow` selector will need a manual `PUT` to correct the
value — that is an operational fix, out of scope here.

### Fabro Details

<details>
<summary>Ran 8 stages in 31m 57s for $6.85</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 20s | – | 0 |
| preflight_lint | 2m 33s | – | 0 |
| implement | 4m 28s | $2.46 | 0 |
| simplify_fable | 8m 39s | $3.31 | 0 |
| simplify_gpt | 2m 25s | $1.07 | 0 |
| verify | 11m 2s | – | 0 |
| **Total** | **31m 57s** | **$6.85** | **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-8; }
        "
    ]
    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_fable    [label="Simplify (Fable)", prompt="@prompts/simplify.md", model="claude-fable-5", reasoning_effort="xhigh"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, timeout="1800s", 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_fable -> 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-07-07 14:08:32 -04:00
fabro-sh-fabro[bot]
c945fb404b
feat: add MCP servers settings UI at /settings/mcps (#540)
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

Adds a full CRUD management UI for server-managed MCP servers at
`/settings/mcps`, consuming the already-shipped `MCPServersApi` backend.
The implementation mirrors the existing `/settings/environments` pages
exactly in structure, naming, and component conventions.

## What changed

### Step 1 — Shared `KeyValueEditor` extracted
`KeyValueEditor`, `KeyValueEntry`, `entriesFromMap`, and
`mapFromEntries` are moved from `environment-form.tsx` into a new
`components/key-value-editor.tsx`. The component gains an optional
`renderEntryHint` prop so per-row warnings can be injected without
coupling the editor to credential logic. `Label` is promoted from
`environment-form.tsx` to `settings-panel.tsx` so both forms can use it.

### Step 2–4 — Query plumbing
- `query-keys.ts`: `mcpServers.{list, detail}` keys.
- `api-client.ts`: `mcpServersApi` instance (same pattern as
`environmentsApi`).
- `queries.ts`: `useMcpServers()` and `useMcpServer(id)` SWR hooks.

### Step 5 — Credential heuristics (`lib/credential-heuristics.ts`)
Pure functions `looksLikeCredential`, `secretNameForKey`,
`secretReference`. Key-name matching covers `authorization`, `password`,
`token`, `api[-_]?key`, `_key`/`_token`/`_secret` suffixes.
Value-entropy fallback fires for strings ≥ 20 chars, no spaces, mixed
case/digit classes. Template references (`{{ secrets.* }}`) are never
flagged.

### Step 6–7 — Form model + component (`components/mcp-server-form.tsx`)
- Flat `McpServerFormValues` discriminated on `McpTransportKind`.
- `defaultMcpServerFormValues`, `mcpServerToFormValues` (populates
`env`/`headers` from `env_keys`/`header_keys` with **empty values** —
the §5 write-only design), `createRequestFromForm`,
`replaceRequestFromForm`, `isMcpServerFormValid`, `credentialWarnings`.
- `McpServerFormFields` renders stdio / http / sandbox panels switching
on `values.transport`. Per-row credential nudge opens the secrets-new
page in a new tab and substitutes a `{{ secrets.NAME }}` reference; save
is never blocked by the heuristic.
- On edit, a row with a non-empty key and empty value blocks save with
an inline error (the intentional overwrite guard).

### Step 8–10 — Route pages
| File | Mirrors |
|---|---|
| `routes/settings-mcps.tsx` | `settings-environments.tsx` |
| `routes/settings-mcps-new.tsx` | `settings-environments-new.tsx` |
| `routes/settings-mcps-edit.tsx` | `settings-environments-edit.tsx` |

The edit page shows a write-only-values banner whenever the transport
has any `env_keys`/`header_keys`, uses `key={server.revision}` to
remount the form on external change, and translates 409 responses into
the `staleAwareMessage` pattern.

### Steps 11–12 — Router + nav
Three routes registered under `settings` children. `PuzzlePieceIcon` nav
entry added to the same section as Environments.

### Plan Summary
- Extract `KeyValueEditor` to shared component with hint-injection slot
- Credential heuristics library (pure, fully unit-tested)
- MCP form model: flat values ↔ discriminated API types, write-only-key
guard
- List / new / edit pages following environments pattern exactly
- Route registration and settings nav link


### Fabro Details

<details>
<summary>Ran 9 stages in 65m 58s for $20.54</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 47s | – | 0 |
| preflight_lint | 4m 15s | – | 0 |
| implement | 26m 29s | $12.27 | 0 |
| simplify_opus | 7m 41s | $4.95 | 0 |
| simplify_gpt | 6m 51s | $2.69 | 0 |
| verify | 15m 44s | – | 0 |
| fixup | 1m 41s | $0.63 | 0 |
| **Total** | **65m 58s** | **$20.54** | **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-8; }
        "
    ]
    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-07-01 10:32:37 -04:00
Bryan Helmkamp
bc0bda73a6
feat(web): add server-managed Environments CRUD settings UI (#462)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (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
## What

Adds a CRUD interface for **server-managed Environments** at
`/settings/environments`, driven by the `/api/v1/environments` REST API
(list / create / retrieve / replace / delete), and reshapes how built-in
environments are provisioned and protected.

The page lives in the **Workflows** settings nav section (also
introduced in this branch), positioned before Variables.

## Why

The Environments REST API shipped (#453) but had no UI — environments
could only be managed via the API/CLI. This gives operators a web UI
alongside Variables and Secrets, and along the way tightens the model:
environments are seeded at install time (not silently re-created on
every boot), and the `default` fallback is an ordinary, deletable
environment.

## Web UI

**Pages & component**
- `settings-environments.tsx` — list view: provider badge,
image/resource summary, row actions (Edit/Delete). **"New environment"
is a dropdown** of the enabled sandbox providers; the chosen provider is
fixed for the environment's lifetime.
- `settings-environments-new.tsx` / `settings-environments-edit.tsx` —
create/edit flows; create reads the provider from a query param.
- `environment-form.tsx` — shared form, reorganized:
- **General** panel (merged identity + image): id, and an **image-source
selector** (Image reference *vs* inline Dockerfile) that shows,
requires, and sends only the selected, mutually-exclusive source.
- **Resources**: CPU / memory / disk as **range sliders** (CPU 1–8,
memory 1–16 GB, disk 1–20 GB), each always writing a concrete value.
  - **Environment variables** key/value editor.
- **Advanced** progressive-disclosure section holding **Network** (a
single "Block all network access" toggle — allow-all vs block) and
**Lifecycle** (preserve / stop-on-terminal / auto-stop). Opens by
default when any advanced value is non-default.
- The in-form **provider control and the Labels editor were removed** —
labels remain API-managed and are round-tripped untouched so UI edits
never clear them.

**Data layer**: `environmentsApi` client, `queryKeys.environments`,
`useEnvironments` / `useEnvironment` SWR hooks.

**Nav & routing**: "Environments" item in the Workflows section before
Variables; routes registered in `router.tsx`.

## Backend: seed at install, deletable `default`

- **Seeding moved to install time.** The server no longer seeds
built-ins on startup; `EnvironmentStore::load_or_seed` → `load`
(load-only). A new public `seed_environments(dir)` (idempotent,
preserves operator edits) is called by both the web installer and the
CLI installer. An uninstalled instance therefore has no managed
environments, and a run selecting an absent environment fails explicitly
(`unknown environment: default`) rather than resurrecting a built-in.
- **`default` is no longer protected.** The delete guard and the
`Protected` error variant are gone; deleting `default` succeeds (204)
and removes the run fallback on purpose — forcing an explicit choice.
`local` is unchanged (reserved, in-memory).
- **`volumes` removed** from environment settings across the OpenAPI
spec, generated Rust + TS clients, config layers,
sandbox/server/workflow plumbing, docs, and tests.

## API contract details honored
- Edit sends the environment `revision` as `If-Match`; 409 conflicts
surface a "changed since you opened it" message.
- The REST API accepts inline Dockerfiles only — the form never sends a
Dockerfile path.

## Verification
- Rust: `cargo build` (touched crates) ✅, `cargo nextest -p
fabro-environment` 21/21 ✅, server env unit + `tests/it` integration 2/2
+ 15/15 ✅, `clippy` (nightly, touched crates, all targets) clean ✅, `fmt
--check` clean ✅. Full `--workspace` suite not run here — worth a CI
pass.
- Web: `bun run typecheck` ✅, `bun run build` ✅,
`environment-form.test.ts` 5/5 ✅. Web suite: 512 pass / 1 unrelated
pre-existing `RunDetail` failure.
- **Not visually verified in-browser** — the local app is login-gated
and automated loads redirect to `/login`; rendering of the form, the
New-environment dropdown, and `default` delete should be confirmed in a
logged-in session.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com>
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Release Repro <release-repro@example.com>
2026-06-13 08:44:38 -04:00
Scott Werner
d590122531
feat: chat-driven workflow builder at /playground (#450)
## Summary

Adds a new `/playground` route where users build a Fabro workflow by
chatting with Ask Fabro on the right while watching a live canvas
re-render on the left. The workflow can be downloaded as a `.fabro.zip`
or — eventually — launched as a real Fabro run; today the "Run for
real" button POSTs to `/api/v1/runs` and redirects to the resulting
`/runs/{id}` page, with a placeholder project/repo/folder picker.

The feature is built as a standalone component subtree under
`apps/fabro-web/app/components/playground/` with no `AppShell` or
`react-router` dependencies, so it can be re-embedded in other contexts
later by passing `chatEndpoint`, `authMode`, and an optional
`realRunRedirect` prop.

## What changed

**Frontend (`apps/fabro-web/`)**

- New `/playground` route + `<Playground>` component tree.
- Live SVG canvas via `@viz-js/viz` with click-to-inspect (read-only
  node detail panel), pan, zoom, fit-to-window, and a simulated walk
  through the graph driven by a Play button.
- Docked chat sidebar (assistant-ui) wired to the new
  `/api/v1/playground/chat` endpoint, with auto-retry on parse failure
  and a playground-specific tool-call summary that reads
  `Wrote workflow.fabro (N nodes, M edges)`.
- File tabs (`workflow.fabro` / `workflow.toml` / `README.md`),
  `.fabro.zip` download via `fflate`, and a "Run for real" toolbar
  button that POSTs an inline `RunManifest` to `/api/v1/runs`.
- Draft persists across page refreshes via `localStorage`.

**Backend (`lib/crates/fabro-server/`)**

- New `POST /api/v1/playground/chat` SSE endpoint. Server is stateless
  across turns: each request carries the full draft, the server runs
  the LLM with a single `write_workflow_file` tool, streams
  `StreamEvent` frames back, and lets the client own diffing/animating
  the result into the canvas.
- Request-size caps before the LLM call (50 messages, 100 nodes, 200
  edges) so a misbehaving or malicious client can't drag multi-MB
  transcripts through token billing.

**Spec / wire contract**

- OpenAPI: new `playground/chat` operation + four new schemas
  (`CreatePlaygroundChatRequest`, `PlaygroundWorkflowDraft`,
  `PlaygroundWorkflowNode`, `PlaygroundWorkflowEdge`).
- `lib/packages/fabro-api-client` not regenerated yet (the playground
  uses raw `fetch`); reviewers who want the TS client to pick up the
  new types can run `bun run generate` in that package.

## Key design decisions

1. **Single `write_workflow_file` tool, not six per-op tools.** The
   first cut exposed `add_node`/`update_node`/`connect`/etc. as
   discrete tool calls. The model would routinely add nodes without
   wiring them up, leaving the canvas in a broken half-state. Pivoted
   to a single tool that takes the full new `workflow.fabro` content;
   the browser parses the DOT, diffs it against the local draft, and
   animates the resulting reducer ops in. The model only has to "get
   the file right", and the canvas still paints node-by-node thanks
   to the client-side animator.

2. **Stateless server.** Each chat turn POSTs the full current draft;
   nothing is persisted server-side. Keeps the endpoint cheap, makes
   refresh-resumption trivial (browser owns the truth), and means the
   same endpoint can later sit behind a rate-limited anonymous variant
   without growing per-session state.

3. **Standalone component subtree.** `<Playground>` has no
   `AppShell`/router/store dependencies. All cross-cutting concerns
   flow in as props (`chatEndpoint`, `authMode`, `realRunRedirect`).
   This is the structural hook that makes future re-embedding possible
   without a refactor.

4. **Chat is the only mutation path.** Click-to-inspect on the canvas
   is read-only. Bi-directional canvas editing was explicitly cut from
   scope to keep one source of truth for "how the workflow changed."

5. **Inline `RunManifest` instead of temp-dir-then-clone.** The
   playground has no project to run against, so the `Run for real`
   modal builds a `RunManifest` that carries the full DOT and
   `workflow.toml` source inline (`workflows[key].{source, config}`).
   `cwd` is pinned to a fixed `/tmp/fabro-playground` constant — no
   LLM-controlled segment in a filesystem-looking field.

6. **React effects policy compliance.** All `useEffect` calls in
   playground component code go through the existing primitives in
   `app/hooks/effects.ts` (`useDocumentEvent`, `useInterval`) or a
   purpose-named hook (`useCanvasRender`).

## Still outstanding (planned follow-ups)

- [ ] **Actually kicking off the ad-hoc run.** "Run for real" today
      POSTs a manifest with a placeholder project/repo/folder
      fieldset. The intent is to reuse the project-picker pattern
      being introduced on the in-flight automations branch — once
      that pattern lands, the disabled inputs in
      `run-for-real-modal.tsx` become the live surface.
- [ ] **Header link to `/playground`.** No nav entry yet; users have
      to type the URL directly.
- [ ] **Live SSE-driven canvas overlay** via
      `GET /api/v1/runs/{id}/attach` — currently the modal redirects
      to the standard run-view page; the "watch it build on the
      playground canvas" experience comes when the `stage.*` events
      are wired through.
- [ ] **Regenerate `lib/packages/fabro-api-client`** so the new types
      ship to TS consumers.
- [ ] **Smoke test:** end-to-end download → unzip →
      `fabro run <name>` round-trip.
- [ ] **`scripts/build.ts` dist-symlink bug:** `pruneOldBuilds` can
      delete the directory `apps/fabro-web/dist` points at, which
      pins the dev server in 503 "build in progress" forever.
      Workaround documented; the real fix is a separate PR.

## Test plan

- [ ] `cd apps/fabro-web && bun run test app/components/playground/` —
111 tests pass
- [ ] `cd apps/fabro-web && bun run typecheck` — clean
- [ ] `cargo test -p fabro-server playground` — 6 tests pass
- [ ] Visit `/playground`; the canvas renders the welcome `start → ??? →
exit` ghost.
- [ ] Type "build me a release-notes workflow" in chat; nodes/edges
animate in; ack reads `Wrote workflow.fabro (N nodes, M edges)`.
- [ ] Click a node → inspector panel populates; click empty canvas →
deselects.
- [ ] Click `Simulate`; nodes light up `start → ... → exit` along the
resolved path.
- [ ] Click `Download .fabro`; unzip; `cd <unzipped> && fabro run
<name>` runs locally.
- [ ] Click `Run for real` → modal opens → confirm → POST succeeds →
redirected to `/runs/{id}` → run executes.
- [ ] Refresh the page; the draft persists from localStorage.
- [ ] Click `Start over` → `Yes`; canvas resets to welcome state.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 11:24:56 -04:00
fabro-sh-0530[bot]
fe1d33c041
Remove top-level automation enabled master gate (#456)
The top-level `enabled` flag on automations created a confusing
two-level activation model (automation-level + trigger-level). Since
automations are brand new with no existing data to migrate, the master
gate is removed entirely — trigger-level `enabled` is now the sole
activation control.

## What changed

**Domain model (`fabro-automation`):** `enabled` removed from
`Automation`, `AutomationDraft`, `AutomationReplace`, and
`PersistedAutomation`. `enabled_api_trigger()` no longer short-circuits
on the automation flag. The `default_true()` helper is gone. A new test
asserts that TOML with a top-level `enabled` key is rejected (no silent
compatibility path).

**Server handler:** Conflict detail updated from `"automation is
disabled or has no enabled API trigger"` → `"automation has no enabled
API trigger"`. The
`disabled_automation_run_endpoint_returns_conflict_code` test is
deleted; the trigger-disabled and missing-trigger tests remain as the
authoritative inactive-run coverage.

**OpenAPI + generated clients:** `enabled` removed from `Automation`,
`CreateAutomationRequest`, and `ReplaceAutomationRequest` schemas and
from the generated TypeScript interfaces. Trigger-level `enabled` on
`AutomationApiTrigger` and `AutomationScheduleTrigger` is untouched.

**Web UI:** `AutomationFormValues.enabled` and the "Enabled" toggle row
are gone. `isFormValid` no longer requires at least one enabled trigger.
`canRun` in the detail view is now just `apiTrigger?.enabled === true`.
The `StatusChip` component is removed. The automations list uses a new
`apiEnabled` field (derived from `hasEnabledApiTrigger`) to drive
run-button state and tooltip copy. A shared `lib/automation.ts` helper
centralises `findApiTrigger`, `findScheduleTrigger`, and
`hasEnabledApiTrigger` to avoid repeated inline `.find()` calls across
routes.


### Fabro Details

<details>
<summary>Ran 8 stages in 41m 34s for $17.84</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 25s | – | 0 |
| implement | 13m 0s | $9.25 | 0 |
| simplify_opus | 9m 43s | $6.18 | 0 |
| simplify_gpt | 3m 56s | $2.41 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **41m 34s** | **$17.84** | **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-29 22:18:14 -04:00
fabro-sh-0530[bot]
a65473f216
Add "Create automation from run" prefill flow (#454)
## Summary

Adds a frontend-only flow that lets users bootstrap a new automation
from an existing run's metadata. The run actions menu grows a
context-aware entry: ordinary runs get **Create automation from run**
(navigates to `/automations/new?from_run=<id>`); runs already tied to an
automation get **View automation** instead. The `/automations/new` route
reads the query param, loads the run + settings, and mounts a keyed form
child pre-populated with the inferred values — no `useEffect` needed.

### Plan Summary

- **`automation-form.tsx`** — new exported
`automationFormValuesFromRun(run, settings)` helper plus three narrow
private parsers (`githubRepositoryFromSettings`, `githubRepositoryName`,
`githubRepositoryFromOriginUrl`) that only produce `owner/repo` for
verifiable GitHub-style values and leave everything else blank.
- **`automations-new.tsx`** — route split into a wrapper that reads
`from_run`, calls `useRun`/`useRunSettings`, and either shows a loading
placeholder, a graceful error fallback, or a keyed
`<AutomationCreateForm>` so initial state is set once from props rather
than via effects.
- **`run-detail.tsx`** — `automationAction` computed from
`summary.automation?.id` and inserted into the `operations` group after
Preview.
- **Tests** — new `automations-new.test.tsx` covers empty form, prefill,
and error-fallback paths; `run-detail.test.ts` extended with automation
navigation assertions and refactored `makeRunSummary` to accept named
params.

### Key design decisions

| Decision | Rationale |
|---|---|
| Keyed child form (`key={`from-run:${id}`}`) | Lets React reset
`useState` from props without `useEffect`, per the effects policy |
| Wait for both queries before mounting | Prevents edits being
overwritten when settings arrive after the run |
| GitHub-only repository parsing | Narrow match avoids silently
populating wrong values for non-GitHub or unknown providers |
| No schedule inference | Prefilled automations default to manual/API
trigger enabled, schedule disabled |
| Navigation-only action | No disabled states for terminal/demo runs —
it's just a link |


### Fabro Details

<details>
<summary>Ran 8 stages in 31m 21s for $11.39</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 6s | – | 0 |
| preflight_compile | 2m 11s | – | 0 |
| preflight_lint | 2m 31s | – | 0 |
| implement | 10m 10s | $7.83 | 0 |
| simplify_opus | 4m 23s | $2.16 | 0 |
| simplify_gpt | 1m 34s | $1.40 | 0 |
| verify | 9m 42s | – | 0 |
| **Total** | **31m 21s** | **$11.39** | **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-29 22:18:04 -04:00
Bryan Helmkamp
0e224aa705
fix(web): remove slug field from automation edit page
The slug cannot be changed after creation, so showing it as a read-only
row on the edit page added noise without value. Keep the editable slug
input on the create page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:35:57 -04:00
Bryan Helmkamp
fee245d788
fix(web): make plural /automations/:id the canonical detail route
The list card linked to the singular /automation/:id, which mismatched
the rest of the new automations CRUD surface (/automations,
/automations/new, /automations/:id/edit). Switch the card link and the
slug-preview text on the create form to the plural form, and mount
/automations/:id in the router alongside the existing singular route
(kept as a back-compat alias for any older bookmarks).

Drive-by: fold two adjacent `use super::*` imports into one and reflow
a long `if let` line in the automations handler (linter cleanup; no
behavior change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:47:31 -04:00
Bryan Helmkamp
737dd75149
feat(web): theme toasts to match the app
Replace Sonner's default richColors palette with a Fabro-themed
FabroToaster: dark panel surface, accent-colored Heroicons type icons
(coral error, mint success, teal info, amber warning), and a themed
close button so persistent error toasts can be dismissed. Extract the
shared config out of the two duplicated <Toaster> mount points.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:44:48 -04:00
Bryan Helmkamp
7e33f7a01a
fix(web): center Size column in run list
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:01:45 -04:00
Bryan Helmkamp
87516c25ce
feat(automations): wire UI to API and auto-start runs from API trigger
Make the Automations area in the web UI functional end-to-end against the
real Automation API, and fix the backend so runs created by an automation's
API trigger actually start instead of sitting in Submitted forever.

Web:
- Reveal the Automations nav tab outside demo mode; drop the now-empty
  demoOnly mechanism.
- List page: render via listAutomations (was workflows mock data); wire
  ellipsis menu to Edit and Delete, with ConfirmDialog + If-Match revision.
  Move Create Automation into the toolbar, switch the trigger select to a
  shared FilterButton, hide the redundant page-header title via a new
  hideTitle handle flag.
- Play button on each card fires createAutomationRun with spinner + toast
  and navigates to the new run.
- New automation form: drop the dead Goal panel and hardcoded repository
  list, post to createAutomation with real triggers.
- Edit automation: new /automations/:id/edit route reusing a shared
  AutomationFormFields component, PUT via replaceAutomation with If-Match.
- Show page: rebuild like a run detail page — breadcrumb, title, chips
  (enabled status, repo+ref, workflow, schedule), Edit + Run actions
  (Run hits createAutomationRun), and a Runs panel using RunsListView
  with URL-driven search/sort/pagination/column-picker like the Children
  sub-tab. Drop the obsolete Definition/Diagram/Runs child routes.

Backend (fabro-server):
- create_automation_run now calls lifecycle::queue_run_start after the
  run is persisted, so the run transitions Submitted → Runnable and the
  scheduler picks it up. Logs a warn and returns the created response if
  start fails (no worse than the prior always-stuck behavior).
- queue_run_start in lifecycle.rs is promoted to pub(super) so sibling
  handlers can reuse it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:24:09 -04:00
Bryan Helmkamp
e18772888e
Model run sandbox lifecycle explicitly (#431)
## Summary

Fixes sandbox state reporting by separating a requested sandbox plan
from an initialized sandbox instance. Runs now project sandbox lifecycle
as `planned`, `initializing`, `ready`, or `failed`, and live sandbox
operations only proceed once a real instance exists.

## Changes

- Introduces `RunSandboxPlan`, `RunSandboxInstance`, and
lifecycle-backed `RunSandbox` domain types, with serde validation that
prevents `ready` sandboxes without an instance.
- Updates store projection behavior so sandbox events transition through
planned, initializing, ready, and failed states while preserving
requested provider/image/snapshot separately from runtime metadata.
- Tightens server sandbox handlers so
details/files/services/terminal/VNC helpers require an initialized
instance and return a clear 404 when the sandbox was never created.
- Updates the OpenAPI contract and regenerated clients so `Run.sandbox`
exposes lifecycle state while `SandboxDetails.sandbox` contains only
initialized instance metadata.
- Updates the web UI to render lifecycle state directly from run
summaries, hide the Sandbox tab for pure planned sandboxes, and disable
sandbox controls until the instance is ready.
- Cleans up duplicated lifecycle display/type logic and duplicate
server-side sandbox instance loading found during review.

| Lifecycle state | Meaning | Live controls |
| --- | --- | --- |
| `planned` | Sandbox was requested but no provider instance exists |
Hidden/disabled |
| `initializing` | Provider setup has started | State view only |
| `ready` | Runtime instance exists | Enabled |
| `failed` | Provider setup failed with error details | State view only
|

## Testing

- `cargo check --workspace`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test app/routes/run-detail.test.ts
app/routes/run-sandbox.test.tsx
app/components/run-summary-panel.test.tsx`
- `cargo nextest run -p fabro-types --test sandbox_model_serde`
- `cargo nextest run -p fabro-store
run_created_projects_planned_sandbox_lifecycle
sandbox_lifecycle_events_update_projected_sandbox_state
run_failed_before_sandbox_events_leaves_sandbox_planned`
- `cargo nextest run -p fabro-server
planned_sandbox_returns_404_from_details_endpoint
planned_sandbox_rejects_live_operations
failed_sandbox_rejects_live_operations
local_sandbox_returns_provider_neutral_details`
- `cargo nextest run -p fabro-api --test run_sandbox_round_trip`
- `cargo nextest run -p fabro-api --test sandbox_details_round_trip`

---

[![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-27 12:48:56 -04:00
fabro-sh-0530[bot]
b196a97ac4
Introduce approved effect hooks and migrate direct useEffect calls (#425)
## Summary

Implements the React Effects Policy by creating the approved hook
surface in `hooks/effects.ts` and migrating a broad set of direct
`useEffect` calls across the codebase to either purpose-named hooks or
non-effect patterns.

### Plan Summary

- Add `hooks/effects.ts` exporting `useMountEffect`, `useInterval`,
`useTimeout`, `useDebouncedValue`, `useWindowEvent`, `useDocumentEvent`,
`useDocumentTitle`, `useMediaQuery`, `useLocationHash`, and
`useResizeObserver`
- Extract large imperative effects into purpose-named hooks:
`useTerminalSession`, `useFloatingTooltipMeasurements`,
`useAnnotatedRunGraphSvg`, `useInstallEffects`, and others
- Move install session fetch from a component effect into a SWR query
(`install-query.ts`)
- Replace `useEffect` + `useState` state-derivation patterns with
render-time computation or ref callbacks
- Replace `AskFabroLayoutProvider`/`useAskFabroLayout` context with a
prop callback

## What changed and why

**`hooks/effects.ts`** — the new approved primitive surface. All
internal `useEffect` calls here are intentional; the hooks expose the
*external system* they manage rather than leaking `useEffect` to
component code. `useMediaQuery` and `useLocationHash` use
`useSyncExternalStore` instead of effect + state.

**`useTerminalSession`** — the largest extraction. The 130-line
xterm/WebSocket/ResizeObserver setup block moves from
`terminal-view.tsx` into its own hook, which now owns the `terminalRef`,
`fitRef`, and `socketRef` that previously cluttered the component.
`TerminalConnectionError` and `ConnectionStatus` types are exported from
the hook.

**`useFloatingTooltipMeasurements`** — extracts the `useLayoutEffect` +
ResizeObserver + window resize listener out of `FloatingTooltip`. The
`FloatingTooltipSize` type moves with it so consumers don't need to
import from the component.

**`useInstallSessionQuery` + `useInstallEffects`** — the install session
fetch moves from a component effect to SWR (`install-query.ts`). The
three remaining install effects (token URL scrubbing, GitHub error URL
scrubbing, health-poll restart) move into
`hooks/use-install-effects.ts`. The root-redirect effect is replaced
with a render-time `<Navigate>` gate. The `SessionState` discriminant
now carries `token` so stale query results can be discarded without an
effect chain.

**`SelectionCheckbox`** — `useEffect` setting `input.indeterminate` is
replaced with a ref callback, which runs synchronously after the node is
attached and avoids a stale-frame flash.

**`event-debug.tsx`** — the manual `window.addEventListener("keydown",
...)` pattern is replaced with `useWindowEvent`, removing the
`react-doctor-disable` suppression comments.

**`run-waterfall.tsx`** — the local `useTickingNow` is deleted;
`RunWaterfall` now calls the shared `useTickingNow` from `lib/time` with
the new `active` parameter signature.

**`toast.test.tsx`** — `useEffect(() => onReady?.(api), ...)` in the
test helper is replaced with a direct call during render, which is valid
because `onReady` has no side effects that React cares about.

**`AskFabroSidebar`** — `setIsResizing` from the layout context is
replaced with an `onResizeActiveChange` prop, removing the
`useAskFabroLayout` call and the hidden context coupling from the
sidebar.


### Fabro Details

<details>
<summary>Ran 3 stages in 114m 5s for $95.71</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| work | 103m 3s | $80.42 | 0 |
| audit | 10m 19s | $15.29 | 0 |
| **Total** | **114m 5s** | **$95.71** | **0** |

</details>

<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>

```dot
digraph Goal {
    graph [
        goal="Complete the user-provided goal",
        rankdir=LR,
        max_node_visits=30
    ]

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

    work [
        label="Work",
        thread_id="goal",
        fidelity="full",
        max_visits=12,
        model="gpt-55",
        reasoning_effort="xhigh",
        prompt="@prompts/continue.md"
    ]

    audit [
        label="Completion Audit",
        thread_id="goal",
        fidelity="full",
        goal_gate=true,
        retry_target="work",
        output_schema="routing",
        output_retries=2,
        max_visits=12,
        model="gpt-55",
        reasoning_effort="xhigh",
        prompt="@prompts/audit.md"
    ]

    start -> work -> audit

    audit -> exit [label="Done", condition="outcome=succeeded"]
    audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
    audit -> work [label="No clear verdict"]
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-27 10:37:29 -04:00
fabro-sh-0530[bot]
c2da22a27c
Replace DIY overlay primitives with Radix UI + Sonner (#424)
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

Replaces ~285 lines of hand-rolled Tooltip, HoverCard, and Toast code in
`fabro-web` with battle-tested primitives — gaining real keyboard
accessibility, Radix collision detection, and Sonner's toast lifecycle —
while keeping all 13+ call sites unchanged.

### Plan Summary

- **Tooltip + HoverCard → Radix wrappers**: `@radix-ui/react-tooltip`
and `@radix-ui/react-hover-card` replace the DIY `useHoverAnchor` hook.
A `TooltipProvider` is mounted in `app-shell.tsx` (200ms delay, 300ms
skip-delay for grouped sidebar hovers). `<Tooltip>` self-wraps in a
local provider when rendered outside the shell (tests, isolated mounts).
- **Toast system → Sonner**: `toast.tsx` shrinks to a ~30-line shim
preserving the `{ push, dismiss, clear }` API. `ToastProvider` becomes a
no-op pass-through in DOM contexts; in non-DOM test environments it
renders an `aria-live` fallback backed by `useSonner` so test assertions
still work. The `action` field is dropped (was test-only).
`toast.test.tsx` is rewritten against observable rendered text.
- **CSS-only tooltips → `<Tooltip>`**: Two inline `group-hover/*` blocks
in `settings-models.tsx` are swapped for the new wrapper, gaining
keyboard focus + Esc dismiss + collision avoidance.
- **SVG-anchored hovers → `FloatingTooltip`**: A new
`app/components/floating-tooltip.tsx` helper portals to `document.body`
and computes collision-avoiding `top`/`bottom` placement from a raw
`DOMRect` (no wrappable trigger). It absorbs `hover-card-style.ts`
(deleted) and is used by `run-overview.tsx` and `event-debug.tsx`.

### What changed and why

**`FloatingTooltip`** handles the two SVG/Graphviz hover sites where
there is no React trigger element to wrap — only a `DOMRect` measured
from DOM events. It uses `useLayoutEffect` + `ResizeObserver` to measure
its own rendered size before applying final position, so it never clips
at viewport edges. This is the one place a `useLayoutEffect` is
intentional and documented.

**`Tooltip` provider fallback**: Radix throws if `<Tooltip>` renders
without an ancestor `TooltipProvider`. Rather than requiring every test
to mount the shell, the component detects provider presence via context
and injects a local one when needed.

**Toast shim backward-compat**: `ToastProvider` previously accepted
`autoDismissMs` as a prop; that prop is silently dropped. The `action`
field on `ToastInput` is removed (only one test referenced it —
`run-detail.test.ts` is updated accordingly). All other consumers
compile without changes.

**CSP fix** (bundled): `img-src` gains
`https://avatars.githubusercontent.com` to allow GitHub avatar images,
with the corresponding integration-test assertion updated.


### Fabro Details

<details>
<summary>Ran 8 stages in 54m 1s for $32.86</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 21m 38s | $22.48 | 0 |
| simplify_opus | 13m 51s | $7.00 | 0 |
| simplify_gpt | 3m 54s | $3.38 | 0 |
| verify | 9m 35s | – | 0 |
| **Total** | **54m 1s** | **$32.86** | **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-26 23:03:06 -04:00
Bryan Helmkamp
71c06c1bc4
feat(web): add "Created by" avatar column to runs list view
Visible by default to the right of Status; toggleable via the column
picker. Extracts the principal avatar/label helper out of the run
summary panel so both surfaces share one renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:28:20 -04:00
Bryan Helmkamp
7c69807f1d
feat(web): add multi-select run status filter to /runs toolbar
Status filter operates on the eight non-archived BoardColumn lanes and
filters both the board (hides whole lanes) and the list (hides rows).
Show archived remains a standalone toggle alongside it; an `archived`
token in a previously-saved status string is migrated into the toggle on
read so the two controls stay independent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:28:20 -04:00
Bryan Helmkamp
09fe367004
feat(web): add "Test models" sweep button on settings/models (#410)
## Summary

`fabro model test` (CLI) probes every configured model with a cheap "Say
OK" prompt and prints a results table. Until now, the equivalent on
`/settings/models` was "open a terminal." This PR adds a single **Test
models** button in the section header that runs the same sweep against
the visible rows and renders per-row results inline. Wire format is the
existing `POST /api/v1/models/{id}/test` — no backend changes.

## Behavior

- One button beside the provider filter + search. Tests *whatever the
table currently shows* (filter + search applied at click time).
- Concurrency cap of 4 to mirror the CLI's `--jobs 4` default.
- Rows render `Queued` → `Testing…` → `Ok` (mint check) or red X +
truncated error (full message on hover via `title`).
- After each sweep, a small `N ok · M failed` chip appears next to the
button (mint when clean, coral on failures).
- Re-clicking starts a fresh sweep over the current view.

## Out of scope (deliberately)

- **No deep-test toggle** — page calls basic mode only; `fabro model
test --deep` still covers that case from the CLI.
- **No per-row Test button** — the page-level sweep replaces it.
- No cancellation, no result persistence across navigation/refresh, no
toast — the inline state *is* the feedback.

## Files

- `apps/fabro-web/app/routes/settings-models.tsx` — `RowState`/`Sweep`
types, `runSweep` worker pool, header button + summary chip, new "Test"
column, `TestStatusCell` component.
- `apps/fabro-web/app/components/state.tsx` — `Spinner` is now exported
(was previously private).

## Test plan

- Click "Test models" with several configured providers → rows flip in
waves of 4; summary lands as `N ok · 0 failed`.
- Revoke a provider's API key, click again → that provider's rows end in
red X with the upstream error in the cell (full text on hover).
- Apply a provider filter, click → only filtered rows test.
- DevTools Network panel → at most 4 in-flight `/models/<id>/test`
requests at any time.

---

[![Compound Engineering
v2.60.0](https://img.shields.io/badge/Compound_Engineering-v2.60.0-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via
[Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:42:16 -04:00
Bryan Helmkamp
62f0b3e7d1
refactor(web): improve React Doctor score (#405)
## Summary

Improves the web UI's React Doctor audit score by separating reusable
helpers from React component modules, tightening effect/state ownership,
and extracting real component boundaries in the install wizard, stage
activity view, run-files diff browser, RunDetail route, and Runs
workspace. The branch removes the previously deferred RunDetail and Runs
giant-component diagnostics without changing RunDetail UX, route
contracts, action ordering, or Runs workspace behavior.

| Metric | Main baseline | Initial PR | Current PR |
|--------|---------------|------------|------------|
| React Doctor score | 63 | 71 | 99 |
| React Doctor errors | 123 | 0 | 0 |
| React Doctor warnings | 241 | 163 | 3 |
| React Doctor diagnostics | 364 | 163 | 3 |

## Changes

- Moves exported helper logic out of component files so Fast
Refresh/component-export rules no longer dominate the audit.
- Adds a targeted React Doctor config exception for React Router route
modules, where non-component exports like route metadata are
intentional.
- Refactors low-risk state/effect patterns: keyed interview question
state, reducer-backed editable run title state, event-owned preview
opening, route-keyed insights editor initialization, refresh timer
ownership, and selection/derived list cleanup.
- Reworks `InstallApp` around an install reducer, a controller hook for
install lifecycle state, and focused wizard step components for LLM,
server, object-store, sandbox, and GitHub setup.
- Moves `RunStages` selected-stage activity into a keyed boundary for
panel/debug detail state while preserving stage activity filters across
navigation.
- Extracts the `RunFiles` loaded diff-browser view from route/query
coordination so the route owns data/URL state and the loaded view owns
rendering.
- Splits `RunDetail` into route-local header, actions, tab shell, docked
controls, model, and lifecycle-toast modules; the actions menu now uses
grouped descriptors instead of a large boolean/callback prop matrix.
- Extracts Runs workspace preference ownership into
`useRunsWorkspacePreferences` and moves toolbar rendering into
`RunsToolbar`, leaving the route focused on data, DnD state, filtering,
and view selection.
- Guards `InsightsEditor` query execution with a latest-run id and
timeout cleanup so stale or unmounted mock query runs cannot overwrite
newer results.
- Adds regression coverage for archived-run deletion from RunDetail and
stale-result handling in InsightsEditor.
- Improves semantic/accessibility coverage with labeled controls, native
meter/section semantics, decorative status dots, and clearer unavailable
copy.
- Removes dead UI code and applies local suppressions only where the
rule is a documented false positive or an intentional imperative
integration boundary.

## Remaining React Doctor warnings

Current score is 99 with 0 errors and 3 warnings. The remaining warnings
are intentionally left for separate judgment rather than mechanical
churn:

- `prefer-useReducer` (3): `AutomationsNew`, `InsightsEditor`, and
`CreateSecretForm` need reducers only if they encode real coupled
transitions, not simple field setters.

## Verification

- `cd apps/fabro-web && bun test app/routes/run-detail.test.ts` -> `22
pass`, `0 fail`
- `cd apps/fabro-web && bun test app/routes/insights-editor.test.tsx
app/routes/runs.preferences.test.tsx` -> `7 pass`, `0 fail`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test --isolate` -> `490 pass`, `0 fail`
- `cd apps/fabro-web && bunx react-doctor@latest --full --json >
/tmp/fabro-react-doctor-runs-insights.json` -> score `99`, `0` errors,
`3` warnings
- Earlier branch verification also included `cd apps/fabro-web && bun
run build`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context not reported, default reasoning) via
[Codex](https://openai.com/codex)
2026-05-25 22:41:37 -04:00
Bryan Helmkamp
1b8dcd41de
fix(web): hide runs list pager when pagination isn't relevant
Show the pager only when there's actually more than one page or the
user is past page 1, replacing the hardcoded total >= 25 threshold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:35:14 -04:00
fabro-sh-0530[bot]
e8f0aceee8
refactor: rationalize server secret scopes (vault-only for optional int… (#401)
## Summary

Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.

## What changed

**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.

**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.

**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).

**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.

**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.

**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.

**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.

### Plan Summary

- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.


### Fabro Details

<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **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-25 17:26:01 -04:00
Bryan Helmkamp
2c851d5a41
Merge remote-tracking branch 'origin/main' 2026-05-25 15:50:43 -04:00
fabro-sh-0530[bot]
b15d8b8476
feat: Add approve/deny run controls to MCP and CLI (#400)
## Summary

Exposes the existing `POST /api/v1/runs/{id}/approve` and `POST
/api/v1/runs/{id}/deny` REST endpoints through the `fabro_run_interact`
MCP tool and two new top-level CLI commands (`fabro approve`, `fabro
deny`). Workflow agents are explicitly blocked from using these actions
— approval remains a human/user operation.

## What changed

**Client & tool backend** (`fabro-client`, `fabro-tool`): Added
`approve_run` and `deny_run` to `Client` and the `FabroToolBackend`
trait, implemented in `ClientBackend`. `deny_run` passes a
`DenyRunRequest` body; absent, blank, or whitespace-only reasons are
normalised to `None`.

**`fabro_run_interact` MCP tool**: Added `Approve` and `Deny` variants
to `RunInteractAction` / `ValidatedInteractAction`, and an optional
`reason` parameter (only valid for `deny`; validated and trimmed on
input). Both actions return `{ "summary": … }` using the existing shape.
The tool description is updated to list the new actions.

**Workflow-agent guard** (`fabro-workflow`): Before dispatching
`fabro_run_interact`, the handler checks
`validated.action.requires_user()`. If the action is `approve` or
`deny`, it returns an immediate `ToolError` without ever reaching the
backend, keeping the guard explicit and independent of server auth.

**CLI** (`fabro-cli`): Extracted the archive/unarchive batch loop into a
shared `run_resolved_run_batch` helper in `commands/runs/mod.rs`, then
implemented `approval.rs` using the same helper. Both commands follow
the same batch contract as archive: attempt all runs, collect per-run
errors, exit non-zero if any fail, and emit `{ "approved"/"denied": […],
"errors": […] }` in JSON mode.

**Server auth regression** (`fabro-server`): Extended
`run_tools_worker_cannot_call_user_only_non_mcp_routes` to cover `POST
/runs/{id}/deny` alongside the existing `approve` and `timeline` checks.

**Docs** (`mcp.mdx`, `cli.mdx`): Updated the `fabro_run_interact` table
entry and added approve/deny examples, plus reference sections for the
two new CLI commands.

### Plan Summary

- Add `approve_run` / `deny_run` to `Client` and `FabroToolBackend`
- Extend `fabro_run_interact` with `approve`, `deny`, and optional
`reason`
- Block workflow-agent self-approval with an early `ToolError`
- Refactor archive batch loop into shared `run_resolved_run_batch`
helper
- Add `fabro approve` and `fabro deny` CLI commands reusing that helper
- Add integration tests for CLI commands, MCP tool, and server auth
guard


### Fabro Details

<details>
<summary>Ran 9 stages in 63m 57s for $42.33</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 28m 16s | $32.57 | 0 |
| simplify_opus | 10m 59s | $4.24 | 0 |
| simplify_gpt | 6m 13s | $3.96 | 0 |
| verify | 10m 51s | – | 0 |
| fixup | 2m 29s | $1.55 | 0 |
| **Total** | **63m 57s** | **$42.33** | **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-25 15:49:57 -04:00
Bryan Helmkamp
b4434af951
feat(web): add models table to /settings/models
Mirrors `fabro model list` output below the existing Providers panel.
Server-side provider + query filters, debounced search, sortable
columns, and a hover/focus popover that surfaces model aliases.

Genericizes SortHeader so non-runs tables can reuse it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:21:40 -04:00
Bryan Helmkamp
acf8caa351
feat(web): add sortable Size column to runs list
Surfaces the run t-shirt size (XS/S/M/L/XL) in both the main runs
list and the Children sub-tab, visible by default. L renders in
amber and XL in coral to flag risky and unhealthy runs at a glance.

Extracts a shared SizeChip component used by the run header and the
table cell, derives Ord on RunSize so the new sort key (server-side
ListRuns sort) orders by bucket, and reorders TOGGLEABLE_COLUMNS so
the column picker mirrors the visible table order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:07:17 -04:00
Bryan Helmkamp
d69198bc30
feat(web): hide Elapsed by default in favor of Size
Now that Size is a first-class column in the runs list, Elapsed is
redundant with it for at-a-glance scanning. Hide Elapsed by default
alongside Updated and Changes; users can still reveal it via the
column picker.

Existing users with stored prefs from the previous "updated,changes"
default keep their stored value, so they'll see both Elapsed and Size
until they toggle Elapsed off (or clear localStorage). New users get
the cleaner default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:03:59 -04:00
Bryan Helmkamp
855d73630b
refactor(web): remove unused RunRow + RUNS_LIST_GRID_TEMPLATE
The grid-based RunRow was the only Children-tab consumer of the
runs-list module's row primitive. When Children adopted the full
RunsListView (table layout) in 4dfcbc0e0, RunRow became unused — the
re-export in runs.tsx was preserved for a release as a precaution, but
nothing imports it. Same for RUNS_LIST_GRID_TEMPLATE, which only the
grid RunRow needed.

Note: automation-runs.tsx still defines its own local RunRow with the
same name; that one is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:39:17 -04:00
Bryan Helmkamp
febd5e2d2e
feat(web): hide Updated/Changes by default, reorder Elapsed after Updated
Updated and Changes are now hidden by default in both the main runs list
and the Children sub-tab — they're still toggleable via the column
picker. The column order shifts so Elapsed lives between Updated and
Changes (i.e. after Created/Updated), keeping the time-related columns
grouped on the right.

Defaults are applied in two places: fresh sessions (no stored prefs)
and existing v1 stored prefs that have no `hide` field. Users who
explicitly cleared all hides keep that choice; stored `hide: ""`
serializes round-trip as `?hide=` (empty value) so the URL distinguishes
"show every column" from "use defaults".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:31:47 -04:00
Bryan Helmkamp
1724cbadcd
fix(web): wire diff stats into list rows and format consistently
The list-view Changes column was always empty because mapRunListItem
never copied additions/deletions from the API's diff payload. Populate
them so rows actually render +/- counts.

The run overview's Changes cell was rendering raw numbers; switch it to
toLocaleString() so it matches the list view's formatting.

Also tighten tabCountBadges in the run-detail test to scope to the
tab-strip's rounded-full badges. The previous selector matched any
tabular-nums span, so the unconditional size chip caused a false
positive in "hides the Files Changed tab badge when diff stats are
absent" after the chip went unconditional in 7d4aa474f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:19:51 -04:00