Commit graph

3222 commits

Author SHA1 Message Date
Bryan Helmkamp
9b7c1907eb
docs: sync public docs to recent runtime changes 2026-05-06 07:39:09 -04:00
Bryan Helmkamp
6836aebc0c
docs(changelog): refresh recent product changes 2026-05-06 07:39:09 -04:00
Bryan Helmkamp
ac931a8838
docs: refresh CLI reference for system repair
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:28:40 -04:00
Bryan Helmkamp
5a9f568e46
refactor: simplify token plumbing and parallelize read_many_files
- Replace mint_github_token's hand-rolled Pat/Installation/App match with
  GitHubCredentials::resolve_bearer_token, removing a near-duplicate of
  the same logic already in run_metadata::mint_token.
- Parallelize read_many_files via futures::future::join_all so the tool
  actually reads concurrently — previously serial despite the name.
- Replace .expect() on the post-refresh GitHubTokenSource cache with a
  proper anyhow error so a refresh edge case can't panic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:15:18 -04:00
Bryan Helmkamp
f1c247bc0f
fix(github): refresh installation tokens during workflows
Split PATs from installation access tokens so static configuration cannot accidentally store expiring ghs_* credentials. Workflow command and API agent stages now resolve GITHUB_TOKEN lazily from a refreshable source, while CLI agent stages surface their launch-time refresh limitation.
2026-05-06 07:15:18 -04:00
Bryan Helmkamp
d2e6f09780
refactor(system): simplify repair-runs flow and rm --force
Mark SystemRepairRunsResponse and SystemRepairRunIssue fields required so
generated Rust/TS types stop forcing Some(...) wrapping on the producer
and defensive .unwrap_or("-") on consumers. Collapse the two-arm dispatch
in fabro rm --force into a single resolve_target step + shared
delete/account block, eliminating ~20 lines of duplicated error handling.
Loosen the brittle "no events" assertion to a substring check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:15:18 -04:00
Bryan Helmkamp
b64352dccd
chore: plan 2026-05-06 07:15:18 -04:00
Bryan Helmkamp
10f5eac1d2
chore: add gh-list workflow 2026-05-06 07:15:18 -04:00
Bryan Helmkamp
6e159fa9d3
fix(system): expose unreadable run repair flow 2026-05-06 07:15:18 -04:00
fabro-releases[bot]
408b5ab79d Bump version to 0.225.0-nightly.0 2026-05-06 10:02:14 +00:00
Bryan Helmkamp
603a64810c
fix(web): virtualize run file diffs consistently
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
Always route non-empty Files Changed views through Pierre's Virtualizer and worker pool, with full-height layout propagation and stable per-file cache keys. Copy Pierre worker assets during the web build so the static worker URL resolves in production.
2026-05-05 16:41:39 -04:00
Bryan Helmkamp
bd82366e6b
fix(docs): point docs.json to renamed stage events endpoint (#217)
## Summary
- `docs.json` referenced `GET /api/v1/runs/{id}/stages/{stageId}/turns`,
but that operation was renamed to `/events` in `fabro-api.yaml` between
the last passing and first failing Mintlify deploy.
- Mintlify could not resolve the operation under the API Reference tab
and reported `Failed to fetch OpenAPI file for anchor or tab`, failing
every docs deploy on `main` since commit `e40dc7d9a`.

## Verification
- Cross-checked every operation page reference in
`docs/public/docs.json` against operations defined in
`docs/public/api-reference/fabro-api.yaml`; all references now resolve.

## Test plan
- [ ] Mintlify Deployment check turns green on this PR
- [ ] After merge, https://docs.fabro.sh updates and the API Reference >
Run Internals group shows the renamed `List Stage Events` endpoint

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

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

## Flow

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

## What changed and why

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

## Review notes

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

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

---------

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

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

## What changed

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

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

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

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

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

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

### Plan Summary

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


### Fabro Details

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

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

</details>

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

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:33:31 -04:00
Bryan Helmkamp
f3784e2e7f
fix(store): remove unused projection cache wrapper 2026-05-05 14:05:09 -04:00
Bryan Helmkamp
5fe9ce8816
cache run projections 2026-05-05 13:55:11 -04:00
Bryan Helmkamp
4661c0fbf4
fix(llm): omit Anthropic thinking for forced tools 2026-05-05 13:02:29 -04:00
Bryan Helmkamp
dc5602580b
refactor(store): collapse list_runs onto list_runs_with_projection
Implement list_runs in terms of list_runs_with_projection and drop the
now-unused RunDatabase::build_summary wrapper. Removes the duplicated
catalog-iteration loop and sort key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:28:59 -04:00
Bryan Helmkamp
890f9fc6f3
perf(server): avoid board run metadata rereads
Thread the RunProjection already built by SlateDB list_runs through to the board runs handler, so PR, sandbox, and pending-question metadata are read without reopening each run.
2026-05-05 12:19:22 -04:00
fabro-sh-0530[bot]
7cec7825d9
Cancel in-flight agent stages with CancellationToken (#211)
## Summary
Run cancellation now reaches in-flight agent work instead of waiting for
an agent stage to finish or recording cancellation as a failed stage.
The workflow cancellation primitive is now
`tokio_util::sync::CancellationToken`, with child tokens passed through
setup, handlers, manager-loop child runs, sandbox streaming commands,
CLI agent invocations, and API agent sessions.

### Plan Summary
- Promote run cancellation to `CancellationToken` while keeping stall
timeout separate.
- Route CLI agents through cancellable sandbox streaming with optional
timeouts.
- Bridge run cancellation into API sessions and preserve
`Error::Cancelled` propagation.
- Add typed events/projections for CLI cancellation and timeout.

## Cancellation flow
```mermaid
flowchart TB
  RunToken[Run CancellationToken]
  Executor[Core executor]
  Services[RunServices]
  Manager[Manager-loop child run]
  CLI[Agent CLI backend]
  API[Agent API backend]
  Sandbox[Sandbox streaming exec]
  Session[fabro-agent Session]

  RunToken --> Executor
  RunToken --> Services
  Services -- child_token --> Manager
  Services -- child_token --> CLI
  CLI -- child_token --> Sandbox
  Services --> API
  API -- bridge guard --> Session
```

## What changed and why
- `RunOptions`, `RunServices`, core `ExecutorOptions`, CLI/server run
state, and detached-run guards now use `CancellationToken` instead of
`Arc<AtomicBool>`. Dropping services or tokens still does not mean
cancellation; only explicit `.cancel()` does.
- Manager-loop child workflows are given child tokens so parent
cancellation propagates down, while stop/max-cycle cancellation remains
scoped to the child workflow.
- Stall timeout remains intentionally separate as a stall token and
still returns `Error::StallTimeout { node_id }`, not `Error::Cancelled`.
- Agent, prompt, human, fan-in, and parallel handler paths now pass
cancellation tokens through and avoid converting `Error::Cancelled` into
normal failed outcomes.

## Agent backend behavior
CLI-mode agents no longer launch detached `setsid` jobs with temp
stdout/stderr/exit-code polling. They run through
`Sandbox::exec_command_streaming` with a child token; a missing node
timeout passes `None` to preserve the existing unbounded agent runtime,
while explicit node timeouts still apply. Cancelled CLI runs emit
`agent.cli.cancelled`, clean temp files, and return `Error::Cancelled`;
timed-out CLI runs emit `agent.cli.timed_out` and return a handler
timeout error; `agent.cli.completed` remains natural-exit only.

API-mode agents install a per-invocation `SessionCancelBridgeGuard`
after acquiring a fresh or cached session. The guard maps the run token
into the session interrupt reason and session cancel token, and aborts
stale bridge tasks before session replacement or cache reinsertion so
reused sessions are not tied to old run tokens. `Session::initialize`
now returns `Result`, and project-doc, skill, MCP, and environment
discovery paths check cancellation and pass child tokens to sandbox
commands.

## Sandbox and event model
`Sandbox::exec_command_streaming` now accepts `Option<u64>` for timeout.
Production streaming implementations use a pending future for `None`
instead of a giant sleep, while the trait fallback maps `None` to
`u64::MAX` only when delegating to non-streaming `exec_command`.

The run event model now includes typed `agent.cli.cancelled` and
`agent.cli.timed_out` payloads with stdout, stderr, and duration, plus
conversion and projection support. OpenAPI/client regeneration was
unnecessary because the API schema already models run events with a free
event string and arbitrary properties; only Rust event types changed.

## Reviewer notes
Expect signature churn around `Session::initialize`,
`CodergenBackend::run`, `RunOptions.cancel_token`,
`StartServices.cancel_token`, and `Sandbox::exec_command_streaming`. The
main behavioral checks are that user cancellation reaches in-flight
CLI/API work and that timeout/stall paths remain distinct from user
cancellation.

### Fabro Details

<details>
<summary>Ran 9 stages in 117m 40s for $150.32</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 8s | – | 0 |
| preflight_lint | 2m 13s | – | 0 |
| implement | 77m 12s | $56.78 | 0 |
| simplify_opus | 18m 5s | $5.83 | 0 |
| simplify_gpt | 15m 33s | $87.71 | 0 |
| verify | 1m 48s | – | 0 |
| fmt | 2s | – | 0 |
| **Total** | **117m 40s** | **$150.32** | **0** |

</details>

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

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-05 09:54:22 -04:00
Bryan Helmkamp
5e4035981f
refactor(cli): format auth status timestamps to seconds precision
Use to_rfc3339_opts with SecondsFormat::Secs so auth status output
shows clean second-precision timestamps instead of nanoseconds.

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

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

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

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

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

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-05 09:32:33 -04:00
fabro-sh-0530[bot]
e901cd3a81
Surface silent fallback warnings in runs and logs (#205)
### Summary
Silent fallback paths now emit stable warnings instead of degrading
without a user-visible signal. The fallback behavior is unchanged; runs
still continue, but worktree, Git, checkpoint, and LLM failover issues
now show up in the run feed and logs.

### Plan Summary
- Emit run notices for workflow Git/worktree fallback paths.
- Reuse the existing failover event for one-shot LLM provider fallback.
- Add tracing for sandbox pipe drain failures.

### What changed
- Added `worktree_skipped_no_git` and gated `sandbox_git_unavailable`
notices during initialization.
- Added `git_push_failed` and `parallel_base_checkpoint_failed` notices,
including redacted output tails where available.
- Logged GitHub token mint failures with a structured `error` field
before the existing notice.
- Plumbed `Emitter` and `StageScope` through `CodergenBackend::one_shot`
so the API backend emits the existing `agent.failover` event instead of
a duplicate tracing-only warning.
- Extracted sandbox pipe draining into a helper that warns on
stdout/stderr read failures, with unit coverage for the error path.
- Updated CLI snapshots for the new worktree warning in stderr and JSON
event output.

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

---------

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

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

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

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

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

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

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

---------

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

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

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

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

### Fabro Details

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

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

</details>

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

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:27:11 -04:00
Bryan Helmkamp
d9c8030e74
refactor(workflow): harden PR content generation
Return named PR content from the builder and keep title/body fallback logic inside the builder.

Move the PR body prompt to markdown and scale prompt truncation from model context windows.

Keep PR creation resilient when generated bodies are empty by emitting a reviewer-visible skeleton body.
2026-05-05 08:25:17 -04:00
fabro-releases[bot]
a2fbac1d60 Bump version to 0.224.0-nightly.0 2026-05-05 09:52:01 +00:00
fabro-sh-0530[bot]
7769c5cec1
Generate Fabro PR titles and bodies with structured output (#208)
Some checks are pending
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
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
### Summary
Fabro now asks the LLM for a structured PR title and reviewer-sized body
instead of deriving every title from the workflow goal. This ports the
compound-engineering PR-writing recipe into the existing pull request
pipeline while preserving Fabro's programmatically appended trailing
sections.

### What changed
- Replaced plain-text PR body generation with `generate_object` and a
strict `{ title, body }` schema.
- Added the sizing matrix, writing principles, visual-aid guidance, and
duplicate-section guardrails to the PR prompt.
- Added model-aware goal/plan/diff truncation caps, with unknown or
smaller-context models using the conservative tier.
- Kept goal-derived titles as a narrow fallback only when the LLM
returns a usable body with an empty title.
- Enforced a 72-character title cap across both LLM-generated and
fallback titles.
- Updated workflow, server, and integration tests for structured
responses, fallback behavior, title truncation, and blank-body failures.

### Plan Summary
- Move PR content generation to structured output.
- Keep existing body assembly and appended sections intact.
- Add coverage for title fallback and validation edge cases.

### Fabro Details

<details>
<summary>Ran 9 stages in 42m 43s for $44.59</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 3s | – | 0 |
| preflight_lint | 2m 13s | – | 0 |
| implement | 15m 8s | $6.37 | 0 |
| simplify_opus | 11m 16s | $3.53 | 0 |
| simplify_gpt | 9m 5s | $34.69 | 0 |
| verify | 2m 17s | – | 0 |
| fmt | 2s | – | 0 |
| **Total** | **42m 43s** | **$44.59** | **0** |

</details>

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

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-04 22:00:15 -04:00
Bryan Helmkamp
4b100d350d
chore: add plan 2026-05-04 16:09:12 -04:00
Bryan Helmkamp
b5b08e78d3
refactor(api): reuse board column contract across clients
Make BoardColumnDefinition.id reference the existing BoardColumn schema and carry that typed contract through generated TypeScript, server responses, demo data, and the runs board UI.
2026-05-04 15:52:24 -04:00
Bryan Helmkamp
63940fdddc
fix(web): recover cross-tab SSE coordination after fallback
Reset coordinator state when the last subscriber leaves, clear pending debounce timers on close, and keep coordinated EventSource construction owned by the coordinator while fallback subscriptions keep their local factories.
2026-05-04 15:52:18 -04:00
Bryan Helmkamp
e4e51511e0
refactor(web): simplify cross-tab SSE message parsing and helpers
Use unknown.ts helpers in parseMessage, factor out parseLeaderPair/Triple
and per-variant parsers to remove repeated typeof guards. Extract
leaderIsFresh() for the staleness check used in three places, and make
RecentEventCache amortized O(1) by walking expired entries from the
oldest instead of scanning the whole map per event. Drop the
closeOnTerminal parameter in run-events; the fallback path computes
close at its single call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:38:19 -04:00
Bryan Helmkamp
6529845554
fix(web): clean up cross-tab SSE lifecycle
Prune stale election candidates as generations advance, reset coordination availability on explicit close, and keep fallback subscribers tracked so coordinator shutdown can clean them up consistently.
2026-05-04 15:29:42 -04:00
Bryan Helmkamp
38726666af
fix(web): harden cross-tab SSE fallback
Stop coordinated election and leadership work when BroadcastChannel posting fails, so tabs degrade cleanly to per-subscriber fallback without stale resync or heartbeat side effects. Expand election coverage for the edge cases called out in the coordination plan.
2026-05-04 15:24:25 -04:00
Bryan Helmkamp
ade721ae65
feat(web): coordinate SSE subscriptions across tabs
Elect a single browser tab to own the global attach stream and broadcast run events to sibling tabs. Keep the existing per-tab EventSource path as the fallback when cross-tab coordination is unavailable.
2026-05-04 14:54:39 -04:00
Bryan Helmkamp
f39e512990
feat(web): split Queued column out of Initializing on the run board
Submitted and Queued lifecycle statuses now live in a dedicated Queued
column rendered to the left of Initializing; Starting stays in
Initializing. The column is omitted from the board when it has no items
so day-to-day boards stay compact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:13:01 -04:00
Bryan Helmkamp
8064aa269e
fix(web): hide runs landing zero-state until data resolves
Render kanban column shells while board/auth/system queries load, so the
"Your runs will appear here" panel no longer flashes before runs arrive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:46:58 -04:00
Bryan Helmkamp
3eef9ee928
Model Test Bounded Concurrency Implementation Plan (#204)
This change makes bulk `fabro model test` run configured model checks
concurrently instead of serially. A new `--jobs/-j` flag (defaulting to
4, minimum 1) controls the concurrency bound; the single-model path
(`--model <MODEL>`) is unaffected. Under the hood, the serial `for` loop
over configured models is replaced with a
`futures::stream::buffer_unordered(jobs)` pipeline that clones the
shared-state `Client` per request. Completed results carry their
original list index and are sorted before rendering, so final stdout
table rows and JSON output remain in listing order regardless of which
requests finish first.

Three new integration tests verify the concurrency behavior using an
inline Axum harness with a `ConcurrencyGate` barrier. The gate holds all
in-flight requests until the expected number arrive simultaneously, then
releases them, letting tests assert `max_in_flight` exactly rather than
relying on timing. The ordering test goes further by assigning
reverse-listing response delays so the last-listed model always finishes
first; if the index sort were dropped, the JSON result order would
invert and the assertion would fail. A 15-second gate timeout ensures a
regression to serial execution surfaces as a clear `max_in_flight == 1`
failure rather than a hung test.

Existing behavior is fully preserved: unconfigured models are still
skipped without a POST, a configured model returning `skip` after
listing is still a failure, `--deep` uses the same `--jobs` value, and
`--jobs 1` reproduces the previous serial behavior for users hitting
provider rate limits.

### Fabro Details

<details>
<summary>Ran 9 stages in 30m 52s for $19.61</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 8s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 8m 56s | $3.87 | 0 |
| simplify_opus | 7m 55s | $1.43 | 0 |
| simplify_gpt | 6m 58s | $14.32 | 0 |
| verify | 1m 49s | – | 0 |
| fmt | 2s | – | 0 |
| **Total** | **30m 52s** | **$19.61** | **0** |

</details>

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

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-04 13:46:44 -04:00
Bryan Helmkamp
92cfcbde71
chore: update plan 2026-05-04 12:55:19 -04:00
Bryan Helmkamp
88216313bf
perf(sandbox): skip throwaway reqwest::Client in Daytona probe
Two follow-ups that were still costing ~1s per credential probe:

- Bumped the daytona-sdk-rust pin to fa4870f, which deletes a dead
  underscore-prefixed _http_client field on Client. The field was
  unused but new_with_config built a fresh reqwest::Client for it on
  every call, paying the macOS proxy-discovery tax even with our
  injection seam in place.

- build_api_keys_configuration was using Configuration::new() and then
  overwriting cfg.client with our injected client. The Default impl
  generated by openapi-generator builds a reqwest::Client::new() for
  the client field eagerly, which we then threw away — another
  ~470ms hit per probe. Construct the Configuration as a struct
  literal so the injected client is the only one we ever build.

Drops the three credential-probe tests from ~700ms to ~10ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:48:44 -04:00
Bryan Helmkamp
17f1d1dfeb
perf(sandbox): inject http client into Daytona credential probe
Routes the two reqwest clients in the Daytona credential probe through
fabro_http (system-proxy) in production and fabro_test::test_http_client
(no_proxy) in tests, by threading an http_client parameter through
check_daytona_api_key_with and build_api_keys_configuration. Bumps the
daytona-sdk-rust pin to 314ffd9, which exposes DaytonaConfig::http_client
and ships on reqwest 0.13.

Drops the three credential-probe unit tests from >1s SLOW to ~0.5s by
skipping macOS proxy discovery on the localhost httpmock requests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:39:46 -04:00
Bryan Helmkamp
f1742d1ab2
test(cli): align attach JSON snapshot with new OpenAI default
The default OpenAI model moved from gpt-5.5 back to gpt-5.4 in 38b51c4c2,
but this attach test snapshot still asserted gpt-5.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:54:45 -04:00
Bryan Helmkamp
38b51c4c29
refactor(server): simplify model availability probes
Use a lightweight basic probe target for preflight instead of fabricating catalog models, run configured model probes with bounded concurrency, and keep expensive model choices opt-in for defaults and live tests.
2026-05-04 11:47:37 -04:00
Bryan Helmkamp
2ef34a228e
docs: sync public docs to recent runtime changes 2026-05-04 11:43:03 -04:00
Bryan Helmkamp
31cbdb5c31
docs(changelog): refresh recent product changes 2026-05-04 11:38:55 -04:00
Bryan Helmkamp
061ccc673b
refactor(server): probe LLM providers concurrently in doctor
Use join_all to fan out provider probes instead of awaiting them
sequentially, and reuse fabro_util::error::collect_chain for the chain
rendering. Carry Provider through ProviderFailure instead of stringifying
it at construction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:32:31 -04:00
Bryan Helmkamp
f4f5416db8
docs: clarify chain-rendering boundary in error strategy
`thiserror`-derived `Display` does not walk `#[source]`, so `format!("{err}")`
and `format!("{err:#}")` on a typed error silently produce only the
top-level message — the same format string changes meaning when migrating
from `anyhow::Result` to a typed `Result`. Point at
`fabro_util::error::collect_chain` as the canonical helper and broaden
the test guidance to cover typed errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:28:28 -04:00
Bryan Helmkamp
8dcb8ebe7e
fix(server): preserve LLM error source chain in doctor output
`fabro_llm::Error`'s Display only renders the top-level message field for
`Network`/`Stream`/`Configuration`/`RequestTimeout` variants — the
`#[source]` chain is dropped. Walk the chain at the rendering boundary
so connectivity failures (DNS, connection refused, TLS) surface their
underlying cause in `fabro doctor` output.

Per docs/internal/error-handling-strategy.md, CLI surfaces should render
the full cause chain. Adds a regression test that walks `err.source()`
on a typed Network error with an inner io::Error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:23:58 -04:00
Bryan Helmkamp
558e585985
feat(server): treat LLM provider probe failures as errors
`fabro doctor` now classifies LLM provider connectivity and auth probe
failures as `CheckStatus::Error` (so the command exits non-zero) and
surfaces the actual probe error text — truncated to one short line per
provider — instead of the generic "Connectivity issues with: <provider>".

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