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.
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.
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>
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>
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>
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>
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>
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.
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>
`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>
`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>
Propagate OpenAI Responses SSE error and response.failed events as structured provider errors, and treat response.incomplete as a normal length finish with partial output preserved.
Also preserve those stream errors through Codex-mode complete_via_stream and add agent coverage proving quota failures do not replay the turn.
GPT-5.5 (released 2026-04-23) replaces 5.4 as the default OpenAI model.
Live integration tests confirm both new IDs respond on the OpenAI API;
they require default temperature like other reasoning models.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use BilledTokenCounts::default() for the non-LLM branch, hoist the
by-model stage count and hasLlmStages predicate out of JSX, and drop
the in-test for-loop in favor of iterator-based assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Include completed stages without LLM usage in run billing responses so command-only runs still show runtime rows. Keep token and model aggregates scoped to billed LLM usage, and render placeholder values in the web billing table.
Keep sandbox exec failures structured until event emission so git push, checkpoint, notice, and retro failures can expose redacted output tails without expanding their terse error strings.
Also add log rendering that appends sanitized tail content for exec-backed errors while preserving the existing safe Display behavior.
Emit snapshot slow-path events only when Docker or Daytona actually performs image or snapshot work, replace retired completion markers with snapshot.ready, and render the lifecycle in attach/log output.
ULID-derived sandbox names like fabro-01KQR3V9D4VPFFWMNTVH09J48G tripped
the entropy detector and rendered as REDACTED in CLI run output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delegate to the existing AppState::canonical_origin helper instead of
re-resolving server.web.url and re-checking emptiness inline. The helper
already validates the URL via validate_public_url, so a misconfigured
non-http(s) origin no longer leaks through into run_web_url's output.
Also pass web_url into create_run_input directly rather than constructing
with None and immediately patching the field at the call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So that CLI and other API consumers can surface a clickable link to the
run's web UI page instead of guessing route shapes or probing settings.
The server populates `web_url` from `server.web.enabled` and
`server.web.url`, returns it on `RunStatusResponse` (create plus all
lifecycle transitions), and persists it on the `run.created` event so
attach replays the same link without re-deriving it.
CLI: prints `Web UI: <url>` as a run-header info line, driven off the
replayed event so fresh runs and `attach` share one code path. Absent
when the UI is disabled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Probe the Daytona API at install, `fabro secret set DAYTONA_API_KEY`,
and `fabro doctor` time to confirm the configured key carries the
snapshot/sandbox scopes Fabro needs. Operators now see a precise scope
error against the control plane instead of a generic sandbox-create
failure at first run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use HTTP health checks with short deadlines for managed server readiness and add finite control-plane request timeouts for CLI/server clients. Keep stream bodies uncapped so SSE attach flows can remain long-lived.
Leftover from when rules.rs was a single 3500-line file. Numbering
was stale (Rule 23 and Rule 24 each appeared twice after the split)
and duplicated info already in the filename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keep fabro_workflow::event as the public facade while moving event conversion, names, redaction, sink, emitter, stored-field helpers, and StageScope into focused modules. Co-locate the existing event tests with the moved code and update the events strategy docs for the new module layout.
Tighten auth state to remove impossible identity branches and stringly error codes.
Route worker JWTs by header metadata, avoid unnecessary auth context cloning, and reuse shared helpers across tests and Slack payload handling.
Carry typed timeout actor metadata through failures instead of deriving it from display text.
Replace hand-built RequestAuthContext literals in github_webhook with the
existing ::invalid()/::authenticated() constructors, collapse the duplicate
demo/real principal layers into a single cloneable layer, and forward the
_with_anyhow error constructors to their _with_source twins to drop the
duplicated cause-collection bodies. refresh_credential_from_headers now
reuses jwt_auth::bearer_token_from_headers for Authorization parsing.
test_support shares one TEST_DEV_TOKEN-derived bearer header instead of a
hand-pasted literal. Replace for-loops in principal/cli_flow tests with
per-variant cases to honor the no-loops-in-tests rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract run_artifact_entry_from / artifact_entry_from in fabro-server so
the two list-artifact handlers share a single conversion site. Push the
?retry=... query append into stage_artifacts_url in fabro-client so
upload callers don't repeat it. Replace required_filename and
required_retry with one generic required_query_param<T> helper.
(From impls were the cleaner shape but the orphan rule blocks them:
NodeArtifact lives in fabro-store, RunArtifactEntry in fabro-api,
neither is in fabro-server. Free fns achieve the same dedup without
adding a fabro-store -> fabro-api coupling.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defer current_visit_for to the fallback branch in stage_entry_with_current_visit
so events that already carry stage_id avoid an O(N stages) scan per event.
Run state() and list_events() concurrently in build_conclusion_from_store, and
share a single retry- segment prefix between encode and decode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
provider_used, script_invocation, and script_timing become object | null;
parallel_results becomes Array<object> | null. The Rust StageState type is
unaffected because fabro-api/build.rs replaces it with fabro_types::StageState.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract STAGE_RANK_WIDTH and a derived MAX_STAGES_IN_DUMP in fabro-dump so
the path-prefix format and the stage-count cap can't drift, and replace the
two `{rank:03}-...` literals with a shared stage_dir_name helper.
Replace the cli/dump.rs `u32::try_from(artifact.retry)` with the symmetric
inverse of the server's `cast_signed()` emit. The OpenAPI schema declares
`minimum: 0`, so the negative branch is unreachable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove unused RunProjection::stage_mut, share decode_retry_and_filename
between artifact_store decoders, reuse stage_visit() in the artifact
lifecycle, and cache the dump.log entry index so RunDump::add_orphan_notice
no longer rescans entries on every call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>