## Summary
Adds Anthropic Claude Fable 5 as a first-class Fabro model without
changing the default Anthropic model. The catalog now exposes
`claude-fable-5` with `fable` and `claude-fable` aliases, 1M context,
128k max output, effort levels, vision/tools, prompt caching, and the
documented pricing.
The Anthropic adapter now handles Fable's API behavior directly: it uses
the `claude-fable-5` API ID, omits the legacy 1M context beta header,
avoids injecting default `thinking`, preserves `output_config.effort`,
omits deprecated `temperature`/`top_p` sampling fields for Fable, and
rejects unsupported manual enabled/disabled thinking configs locally.
Fable refusals are converted into content-filter LLM errors with
`stop_details` preserved. Those refusal errors are fallback-eligible, so
existing `run.model.fallbacks` chains work for both prompt and agent
paths, while no-fallback refusals surface clearly as LLM errors.
## Live QA
Manually exercised the PR branch against a live Anthropic API key from
`~/.fabro.bak/.env.bak` using a temporary local harness that was removed
before commit. The run covered non-streaming completion via `fable`,
token counting via `claude-fable`, streaming completion, the deep
model-test path with tools/reasoning, local rejection of manual thinking
config, and a live refusal probe. The live run initially exposed
Anthropic's Fable rejection of `temperature`; this PR now strips
deprecated sampling fields for Fable and the live harness then passed
6/6 checks.
## Testing
- `cargo test -p fabro-llm --test live_fable_manual -- --nocapture
--test-threads=1` -> 6 passed against live Anthropic, temporary harness
removed afterward
- `cargo nextest run -p fabro-llm
encode_fable_uses_api_id_effort_and_omits_1m_beta`
- `cargo nextest run -p fabro-model -p fabro-llm -p fabro-workflow` ->
1808 passed, 41 skipped
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo insta pending-snapshots` -> no pending snapshots
- `git diff --check`
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Remove devcontainer support from the product surface and codebase: the
parser crate, workflow bridge, lifecycle execution path, typed events,
CLI progress rendering, generated client field, and public/internal
documentation references are all gone.
## What Changed
- Deleted the dedicated parser crate and removed its Cargo dependencies
and lockfile entries.
- Removed workflow initialization paths that resolved repository
devcontainer metadata, applied Daytona snapshots from it, merged
environment variables from it, or ran its lifecycle commands.
- Removed the typed event variants and CLI progress handlers for the
retired lifecycle events while leaving shared unknown-event handling
intact.
- Cleaned the generated TypeScript client and tracked docs so repository
search has no remaining devcontainer references outside git history.
## Verification
- `cargo +nightly-2026-04-14 fmt --all`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo build --workspace`
- `cargo nextest run -p fabro-types`
- `cargo nextest run -p fabro-workflow`
- `cargo nextest run -p fabro-cli run_progress`
- `cd lib/packages/fabro-api-client && bun run generate && bun run
typecheck`
- `cargo metadata --no-deps --format-version 1 | rg -i
"fabro-devcontainer|devcontainer"`
- `rg -n -i "devcontainer|dev
container|dev-container|dev_container|fabro-devcontainer|\\.devcontainer"
. --glob '!target/**' --glob '!.worktrees/**'`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
Use raw sandbox reads for memory and skills, keep line-numbered reads focused on display, and share retry-delay handling across agent and LLM code.
Trim task tool descriptions, bound multi-file read concurrency, restore Docker's text read path, and add the reviewed implementation plan docs.
## Summary
Adds `/ask-fabro`, a prototype route that brings the right-docked "Ask
Fabro" assistant into the real web app. It graduates the sidebar design
from the `docs/superpowers/prototypes/2026-05-16-chats-new` prototype: a
placeholder workspace page with an "Ask Fabro" trigger that toggles an
animated 420px docked panel. The panel streams scripted, **fake** AI
replies through assistant-ui — no real model calls — matching the
behavior of the existing `/chats` prototype.
## What changed
- **`routes/ask-fabro.tsx`** — the route. A placeholder "Runs" workspace
(stat cards + recent-runs list) whose only job is to host the trigger
button, plus the docked sidebar. Uses `handle = { hideHeader,
fullHeight, wide }` and the edge-bleed wrapper copied from the shipping
`chats-layout`.
- **`components/chats/ask-fabro-sidebar.tsx`** — animated-width 420px
assistant panel rendering assistant-ui's `<Thread>`.
- **`components/chats/sidebar-composer.tsx`** — compact single-line
composer pill for the narrow column.
- **`app.css`** — the `.ask-fabro-sidebar` CSS block (narrow-column
overrides, layered into `assistant-ui` to beat its unlayered defaults),
ported verbatim from the prototype.
- **`router.tsx`** — registers the route under the AppShell.
The components and CSS are faithful, near-verbatim ports of the
prototype, which was carefully constructed. The runtime is fully reused
— `chats-runtime`, `chats-script`, `chats-types`, and `tool-fallback`
already graduated with `/chats`, so this PR adds no new chat plumbing.
## Decisions
- **Route-local state, not context.** The prototype used an app-level
`AskFabroContext` so the sidebar could mount above the top nav. This
route is self-contained, so a plain `useState` passed as props is
simpler and equivalent.
- **Sidebar sits below the top nav** (within the route), rather than
spanning the full window like the prototype. Intentional — keeps the
route self-contained.
- **Not added to the nav.** Reachable directly at `/ask-fabro`; it is
not `demoOnly`, so it renders regardless of demo mode.
## Verification
- `bun run typecheck`, `bun test` (403 pass), and `bun run build` all
clean.
- Rendered side-by-side against the prototype's `/sample`: empty state
and active thread (user bubble + streamed markdown assistant reply)
match.
---
[](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>
## Summary
Adds the server-side run pairing surface for joining one active API-mode
agent session, sending pair messages, reading a compact transcript, and
ending pairing explicitly before workflow release continues.
This PR wires the feature end to end:
- adds OpenAPI paths and shared `fabro-types` DTOs for pair lifecycle,
messages, transcript entries, and run event details
- adds typed `RunEvent` variants for pair lifecycle and pair-scoped
user/system messages
- extends the workflow steering hub and agent session drain path with
typed pair control items, single-target validation, pair parking, and
pair end/resume behavior
- extends worker JSONL control and server transports for pair
start/message/end while preserving existing
steer/interrupt/answer/cancel behavior
- adds Axum handlers for `/api/v1/runs/{id}/pair`, pair messages, pair
transcript, and `/api/v1/runs/{id}/events/{seq}`
- adds `fabro-client` helpers for the new endpoints
## Notes
The subprocess path does not add a bidirectional worker ack channel in
this PR. Instead, the HTTP pair handlers only return lifecycle/message
success after the corresponding durable runtime event is observed, so
mpsc enqueue success alone is not treated as API success.
The plan checklist in
`docs/superpowers/plans/2026-05-18-server-side-run-pairing-api-events.md`
is included with that distinction left visible.
## Verification
- `cargo build -p fabro-api`
- `cargo check -p fabro-api -p fabro-client -p fabro-agent -p
fabro-workflow -p fabro-interview -p fabro-server`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-api pair
run_event_round_trips_pair_lifecycle_events
run_event_round_trips_agent_pair_messages`
- `cargo nextest run -p fabro-workflow pair`
- `cargo nextest run -p fabro-interview pair`
- `cargo nextest run -p fabro-server pair
subprocess_answer_transport_pair_commands_enqueue_control_messages steer
interrupt`
## Summary
Fabro-created Daytona sandboxes now carry the same managed-resource
labels Docker containers already use: `sh.fabro.managed=true` and
`sh.fabro.run_id=<run-id>` when a run id is available.
This moves the Docker label constants into a shared sandbox helper,
keeps Docker behavior unchanged, and applies the helper when Daytona
create params are built. User-provided Daytona labels are preserved, but
Fabro's reserved keys are authoritative on collisions. Daytona snapshot
behavior is unchanged because the snapshot API does not expose labels.
## Testing
- `cargo test -p fabro-sandbox managed_labels --no-default-features
--features docker,daytona`
- `cargo test -p fabro-sandbox
docker::tests::real_run_container_gets_name_and_labels
--no-default-features --features docker`
- `cargo test -p fabro-sandbox daytona::tests::base_params
--no-default-features --features daytona`
- `cargo test -p fabro-sandbox daytona_managed_labels_live_smoke
--no-default-features --features daytona`
- `cargo test -p fabro-sandbox --no-default-features --features
docker,daytona`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 clippy -p fabro-sandbox --all-targets
--no-default-features --features docker,daytona -- -D warnings`
The live Daytona smoke test remains ignored; it compiles under the
Daytona feature but was not run against live credentials.
## Post-Deploy Monitoring & Validation
- Log queries/search terms: `Failed to create Daytona sandbox`,
`Daytona`, `labels`, `sh.fabro.managed`, `sh.fabro.run_id`, and sandbox
initialization errors for `provider=daytona`.
- Metrics or dashboards: Daytona sandbox creation success/error rate,
Fabro run initialization failures for Daytona runs, and Daytona resource
inventory filtered by `sh.fabro.managed=true`.
- Expected healthy signals: new Fabro-created Daytona sandboxes include
`sh.fabro.managed=true`, run-owned sandboxes include the matching
`sh.fabro.run_id`, user labels remain visible, and Daytona sandbox
creation failure rates stay at baseline.
- Failure signals and rollback trigger: any sustained increase in
Daytona sandbox creation failures, API validation errors around labels,
or missing managed labels on newly created sandboxes. Roll back this PR
or hotfix the label merge to omit Daytona labels if Daytona rejects the
keys in production.
- Validation window and owner: release owner watches the first 24 hours
after deploy, with an immediate manual Daytona dashboard/API spot-check
after the first managed Daytona run.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context unknown, reasoning enabled) via
[Codex](https://openai.com/codex)
## Summary
The run Overview tab's "Created by" cell rendered every user as a
colored circle with the first letter of their login. Reviewers and run
owners expected the same GitHub avatar shown on `/profile` and in the
top-right nav. The cell only had `login` to work with — the
`PrincipalUser` schema carried no avatar URL.
This threads an optional `avatar_url` through `UserPrincipal`
end-to-end: schema, server auth, and frontend. The avatar is captured at
action time from the request's auth context and persisted with the run's
`created_by` principal — a point-in-time snapshot, the same pattern as
audit logs and chat apps.
## What changed
- **`fabro-types`** — `UserPrincipal` gains `avatar_url: Option<String>`
with `#[serde(default, skip_serializing_if)]`, plus a
`Principal::user_with_avatar` constructor. The existing
`Principal::user` constructor is unchanged (sets `None`), so test
fixtures and CLI/replay call sites need no edits.
- **OpenAPI** — `PrincipalUser` gains an optional nullable `avatar_url`;
Rust (progenitor) and TypeScript clients regenerated.
- **`fabro-server`** — `auth_context_from_session` (cookie auth) and
`classify_user_token` (JWT auth) populate the principal's avatar from
the session/JWT, treating an empty string as `None`.
- **`fabro-web`** — the `run-summary-panel` "Created by" cell renders an
`<img>` when `avatar_url` is present, falling back to the initial circle
otherwise.
## Compatibility
The field is optional with serde defaults, so old persisted runs and
`RunEvent.actor` payloads deserialize unchanged — they show the
initial-circle fallback. No migration or backfill.
## Known gap
CLI-initiated runs (`fabro run ...`) still show the initial circle: the
CLI auth flow hardcodes an empty `avatar_url` in the JWT subject
(`cli_flow.rs:508`). Wiring the avatar through CLI login
(`~/.fabro/auth.json`, JWT claims, refresh-token chain) is a deliberate
follow-up. Web-initiated runs get the avatar today.
## Test plan
- `cargo nextest run --workspace` — 5,832 tests pass, including new
`principal.rs` and `principal_round_trip.rs` cases covering avatar
serialization and legacy-JSON (no-field) deserialization.
- `cd apps/fabro-web && bun test run-summary-panel` — 13 tests pass,
including a new case asserting the `<img>` renders with the avatar src.
- `bun run typecheck`, `cargo +nightly-2026-04-14 fmt --check --all`,
and `clippy --workspace --all-targets -- -D warnings` all clean.
- Manual: restart `fabro server`, create a run from the web UI, confirm
the real avatar renders on the Overview tab; confirm an older run falls
back to the initial circle.
---
[](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>
## Summary
Template failures from `fabro run` and structural warnings from `fabro
validate` now preserve source provenance through rendering, workflow
transforms, API serialization, and CLI display. Diagnostics can point at
the actual workflow, import, or prompt file with node/attribute context
instead of surfacing MiniJinja's generic `<string>` source.
## What Changed
- Added named MiniJinja render APIs plus miette-aware `TemplateError`
metadata for source names, source text, spans, and labels.
- Reworked workflow template expansion so inline attributes, imported
workflows, and `@prompt` files render with file and owner context.
- Split strict run behavior from structural validate behavior: run-start
still hard-fails on missing inputs, while validate emits source-aware
warnings and continues linting.
- Extended validation diagnostics through Rust structs, OpenAPI, server
DTO mapping, and CLI rendering with optional source path, line, column,
span, and related metadata.
- Added regression coverage across template rendering, workflow
transforms, CLI output, and the server validate endpoint.
## Verification
- `cargo nextest run -p fabro-template`
- `ulimit -n 4096 && cargo nextest run -p fabro-workflow --no-fail-fast`
- `cargo nextest run -p fabro-cli
bare_fabro_with_unbound_inputs_validates_structurally_with_warning
run_rejects_unbound_template_inputs_before_creating_remote_run`
- `cargo nextest run -p fabro-server
validate_endpoint_returns_template_source_coordinates`
- `cargo build -p fabro-api`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
## Summary
Ports the validated `/chats/new` and `/chats/:id` chat surface from
`docs/superpowers/prototypes/2026-05-16-chats-new/` into
`apps/fabro-web`. Client-side scripted prototype mounted inside the
existing `AppShell`; replaces `/start` as the planned new "kick off
agent work" entry point (but does not delete `/start` in this phase).
- New routes: `/chats/new` (empty-state composer) and `/chats/:chatId`
(active conversation with assistant-ui's `<Thread>`, scripted streaming
replies, markdown + tool-call rendering).
- Drives `@assistant-ui/react` + `@assistant-ui/react-ui` via
`useLocalRuntime` and a custom `ChatModelAdapter` that cycles a 6-entry
scripted reply bank.
- Tailwind v4 cascade fix: assistant-ui CSS is now imported via `@layer
assistant-ui` so v4 utilities cascade above the package's unlayered
scoped preflight. Includes a discovered Bun-specific tweak — see Notable
Deviations below.
- StrictMode-safe first-message handoff: store seeds the user message
into `seedMessages` with a `pendingResponse: true` flag, and
`chats-detail` triggers a single `runtime.thread.startRun({ parentId:
null })` then consumes the flag. Avoids the prototype's
autorespond-lost-stream race under React 19 StrictMode.
The Ask-Fabro right sidebar (also in the prototype) is **out of scope**
for this PR.
Companion spec:
[`docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md`](../tree/chats-new-port/docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md)
Implementation plan:
[`docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md`](../tree/chats-new-port/docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md)
## Screenshots
Captured from a local debug `fabro server` running this branch's binary,
signed in via GitHub.
### `/chats/new` (empty state)

### `/chats/:chatId` (active conversation)

## Files
**New** (under `apps/fabro-web/`):
- `app/lib/chats-types.ts` — `Chat` wrapper + `ChatContentPart`
discriminated union over the API client's `CompletionContentPart`
- `app/lib/chats-script.ts` — 6-entry scripted reply bank
(`CompletionMessage[]`)
- `app/lib/chats-store.tsx` — Context + `useReducer` for chat metadata,
`pendingResponse` flag, scriptIndex
- `app/lib/chats-runtime.ts` — `createScriptedAdapter` +
`toThreadMessages` boundary converter
- `app/lib/test-utils.tsx` — minimal `renderHook` shim (lifts the
duplicated `IS_REACT_ACT_ENVIRONMENT` + dep-warning silencing pattern
out of `install-app.test.tsx`)
-
`app/components/chats/{tool-fallback,composer-chips,custom-composer}.tsx`
- `app/routes/{chats-layout,chats-new,chats-detail}.tsx`
- Tests: `chats-store.test.tsx` (5), `chats-runtime.test.ts` (4),
`chats-router.test.tsx` (3)
**Modified:**
- `package.json` — adds `@assistant-ui/{react,react-ui,react-markdown}`
(pinned exactly to versions verified in the prototype)
- `app/app.css` — `@layer` declaration + assistant-ui CSS imports into
`layer(assistant-ui)` + `.fabro-chat` `--aui-*` variable overrides
mapping to the Fabro palette
- `app/root.tsx` — removed `import "./app.css"` (see Notable Deviations)
- `app/router.tsx` — wires the chats routes under the AppShell tree
## Notable deviations from the plan
Two intentional deviations, both explained in their commit bodies:
1. **`apps/fabro-web/app/root.tsx` no longer imports `./app.css`.**
Bun's CSS bundler (used by `Bun.build` on `entry.tsx`) rejects
spec-valid `@layer name, name;` ordering between `@import` rules, even
though Tailwind's CLI accepts it. The CSS is built standalone by the
Tailwind CLI step in `scripts/build.ts` and linked from
`index.template.html`, so dropping the JS-side import bypasses Bun's
parser without any runtime change. A safety-net comment at the top of
`app.css` warns future engineers against re-adding the import. Commit:
`c37690be9`.
2. **`!` non-null assertions removed** in two places where the verbatim
prototype copy violated the global CLAUDE.md rule banning `!` in
production code: `chats-script.ts` now uses a typed `FALLBACK_REPLY` and
`??` coalescing; `composer-chips.tsx` lifts the first option of each
chip into a `DEFAULT_*` constant. `chats-runtime.test.ts`'s `for await`
drain loops were also replaced with `Array.fromAsync(...)` per the
no-loops-in-tests rule. Commits: `ace6ac6d4`, `652ad97af`.
## Test plan
- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `bun test` — 383 pass / 0 fail (12 new tests for chats)
- [x] `cd apps/fabro-web && bun run build` — succeeds; assistant-ui CSS
bundled into `dist/assets/app.css`
- [x] **Manual browser smoke test** — debug `fabro` binary running this
branch served `/chats/new` and `/chats/seed_email` correctly inside the
real AppShell with GitHub-OAuth auth (screenshots above).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Makes LLM setup explicitly skippable in both the web installer and
`fabro install`, without making omission accidental. A skipped LLM step
lets install complete with zero LLM credentials; later LLM-dependent
workflows keep using the existing provider-not-configured behavior.
`fabro doctor` is intentionally unchanged.
Plan: `docs/superpowers/plans/2026-05-14-optional-llm-install.md`
## Key changes
**Server + API**
- `PUT /install/llm` now accepts `{"providers":[]}` as "LLM step
completed, skipped" — the empty-list rejection is removed; per-provider
validation for non-empty lists is retained.
- OpenAPI: dropped `minItems: 1` from
`InstallLlmProvidersInput.providers`, updated schema descriptions so
empty = skipped and `llm: null` = incomplete. TypeScript client
regenerated.
- `/install/finish` still requires the LLM step to be present, but
tolerates zero credentials — it writes settings, runtime auth secrets,
and GitHub secrets normally and writes no LLM vault entries.
**Web installer**
- New "Skip LLM setup" secondary action on the LLM step (via a
`secondaryAction` prop on `StepPanel`) that records an empty provider
list and advances to GitHub.
- Review screen shows `LLM providers: Skipped` (step completed, empty)
vs `Not configured` (step never completed), via a new
`describeLlmSummary` helper.
- Continue with no API keys still shows the existing validation error —
skipping is only reachable through the explicit skip action.
**CLI**
- Interactive `fabro install` asks "Configure LLM providers now?"
(default yes) before provider selection; declining returns an empty
selection and continues to GitHub.
- Hidden non-interactive `--skip-llm` flag, mutually exclusive with
`--llm-provider` / `--llm-api-key-stdin` / `--llm-api-key-env` via clap
`conflicts_with_all`. Missing LLM flags are still validation errors
unless `--skip-llm` is present. Non-interactive usage text updated with
a skip example.
## Code review
Ran a 12-reviewer `ce:review` pass (correctness, testing,
maintainability, project-standards, agent-native, learnings, security,
api-contract, reliability, adversarial, cli-readiness,
kieran-typescript). No P0/P1 findings; agent-native parity PASS. Applied
fixes in `40a29c591`:
- Re-entrancy guard on `runStepSubmit` so a fast double-click on "Skip
LLM setup" can't fire two requests.
- `validate()` only suggests `--skip-llm` in the missing-provider error
when no credential flag is set (it conflicts with those flags).
- Added tests: all three `--skip-llm` conflict arms, the review screen's
"Not configured" branch, and the skip-button failure path.
One advisory finding left as report-only: an empty `PUT /install/llm`
overwrites previously-saved credentials if a user navigates Back and
clicks Skip — judged acceptable since the button is explicitly labeled
and clicking it is deliberate.
## Testing
- `cargo nextest run -p fabro-server -p fabro-cli -p fabro-install` —
1521 passed
- `cargo build -p fabro-api`, `cargo fmt --check`, `cargo clippy`
(changed crates) — clean
- `bun test` (install-app) — 14 passed; `bun run typecheck` — clean
- New coverage: server accepts empty providers + session shows `llm`
complete with `providers:[]`; finish with skipped LLM persists no LLM
vault credentials but keeps GitHub secrets; web skip button PUTs
`providers:[]` and navigates to GitHub; review renders Skipped / Not
configured; CLI `--skip-llm` requires `--non-interactive`, conflicts
with all credential flags, `validate()` succeeds with `--skip-llm`,
usage text documents `--skip-llm`.
Not added (out of plan scope): an automated test for the interactive
`InstallInputSource` skip branch — `InteractiveInstallInputSource` is
TTY-coupled and has no existing tests; the non-interactive `--skip-llm`
path is fully covered.
## Post-Deploy Monitoring & Validation
This change is install-time only; there is no continuous runtime impact.
Validate during the next install/release smoke:
- **Web installer:** run a fresh browser install, click "Skip LLM setup"
on the LLM step, confirm it advances to GitHub and the review screen
reads `LLM providers: Skipped`. Finish the install and confirm the
server restarts into normal mode with no LLM credentials in the vault
(`secrets.json` has no credential entries) and
GitHub/server/object-store/sandbox settings written normally.
- **CLI:** run `fabro install --non-interactive --skip-llm
--github-strategy token --github-username <user>` and confirm it
completes; run interactive `fabro install` and confirm declining
"Configure LLM providers now?" continues to GitHub.
- **Healthy signals:** install completes (web `/install/finish` → 202;
CLI exits 0), server boots in normal mode, `fabro doctor` runs and
reports no LLM providers configured (expected, unchanged behavior).
- **Failure signals / rollback trigger:** install fails to finish,
server fails to boot after a skipped install, or `/install/finish`
rejects a completed-but-empty LLM step. Rollback = revert this PR;
install behavior returns to requiring at least one LLM provider.
- **Validation window/owner:** next install smoke / release
verification, owned by whoever runs the release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Finish phase 9 of the configurable LLM provider/model work by aligning
public docs, release notes, and guardrails with the implementation
already landed in phases 0-8.
- documents settings-driven providers/models, OpenAI-compatible gateway
examples, typed `extra_headers`, model `api_id`, controls, and per-speed
costs
- adds the 2026-05-13 changelog entry and provider string migration note
- updates the internal phase plan ledger to reflect current
implementation status
- adds a workspace policy test blocking direct production
`Catalog::builtin()` usage outside catalog owner/test code
- clarifies `Provider` as a built-in compatibility enum while open-ended
identity is `ProviderId`
## Verification
- `cargo nextest run -p fabro-dev --features dev --test it policy`
- `cargo dev docs check`
- `cargo nextest run -p fabro-model -p fabro-config -p fabro-auth -p
fabro-llm`
- `cargo build --workspace`
- `cargo nextest run --workspace` (5717 passed, 182 skipped, nextest
reported 1 leaky test)
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `git diff --check`
## Summary
Supports `dockerfile = { path = "..." }` for Daytona snapshots declared
in `.fabro/project.toml` and workflow-local `workflow.toml`, resolving
each path relative to the TOML file that declared it. Manifest building
now bundles project-level Dockerfiles into the target workflow file
bundle, and server manifest preparation rewrites bundled Dockerfile
paths to inline content before settings reach sandbox creation.
The repo Daytona snapshot config now uses `.fabro/Dockerfile` instead of
embedding the Dockerfile in TOML, preserving the prior Dockerfile
content exactly.
## Testing
- `cargo nextest run -p fabro-manifest
build_manifest_bundles_project_config_daytona_dockerfile_relative_to_project_config`
- `cargo nextest run -p fabro-manifest`
- `cargo nextest run -p fabro-server
prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle
prepare_manifest_errors_when_project_config_dockerfile_bundle_is_missing`
- `cargo nextest run -p fabro-server`
- `cargo nextest run -p fabro-config`
- `cargo nextest run -p fabro-manifest -p fabro-server -p fabro-config`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
Optional credentialed Daytona live smoke was not run.
## Post-Deploy Monitoring & Validation
- Log queries/search terms: `missing bundled dockerfile`, `unsupported
dockerfile reference`, `invalid manifest project config path`,
`dockerfile path should have been resolved to inline content before
sandbox creation`, Daytona snapshot creation failures.
- Metrics/dashboards: run submission/preflight failure rate, Daytona
sandbox startup failure rate, snapshot creation failure rate, and run
validation errors for manifests using bundled files.
- Healthy signals: runs using `.fabro/project.toml` with `dockerfile = {
path = "Dockerfile" }` progress past manifest preparation and Daytona
snapshot creation without path-resolution errors.
- Failure signals and rollback trigger: any sustained increase in
manifest preparation failures or Daytona snapshot failures containing
the log terms above; rollback by reverting this PR or temporarily
restoring inline Dockerfile TOML for affected deployments.
- Validation window and owner: first 24 hours after deploy; owner is the
deploying operator/on-call engineer.

## Summary
Adds the Phase 1 settings surface for gateway-backed LLM providers. This
was prompted by @haroldolivieri's Portkey/Bedrock field report on PR
#207, which showed that gateway auth and routing often live in custom
headers rather than the adapter's primary API-key header.
This PR is schema and seam work only. It does not make settings-defined
providers runnable yet; later phases still own ProviderId migration,
catalog construction, auth resolution, and production adapter
registration.
## Changes
- add typed `extra_headers` values to `[llm.providers.<id>]`
- support explicit `literal`, `env`, and `credential` header value forms
while rejecting bare strings, empty values, ambiguous tables, and
unknown keys
- cover whole-map header merge behavior and adapter header pass-through
tests
- update the settings-driven LLM plan with the Phase 1 gateway header
attribution and completion notes
## Non-goals
- does not make settings-defined providers runnable yet
- does not migrate ProviderId/OpenAPI/auth resolver/runtime catalog
plumbing
- does not route Codex OAuth through custom provider settings
## Tests
- `cargo nextest run -p fabro-config -p fabro-llm`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-config -p fabro-llm
--all-targets -- -D warnings`
- `git diff --check origin/main...HEAD`
## Post-Deploy Monitoring & Validation
No additional operational monitoring required. This is schema and
adapter-seam coverage only; production provider registration and runtime
credential/header resolution remain deferred.
## Attribution
Motivated by @haroldolivieri's Portkey/Bedrock report on PR #207:
https://github.com/fabro-sh/fabro/pull/207#issuecomment-4377929769
Commits include `Co-authored-by: Haroldo Olivieri
<6575718+haroldolivieri@users.noreply.github.com>`.
---
Compound Engineered: Codex, `ce:work`.
---------
Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
## Summary
This lays the groundwork for settings-driven LLM providers and models
without switching production routing yet. The new schemas and shared
vocabulary let later catalog construction treat provider/model identity
as data while keeping adapter behavior and control values Rust-owned.
## What changed
- Added `[llm.providers]` and `[llm.models]` settings layers with sparse
per-entry merging, whole-array replacement for credential/alias/control
lists, TOML date support for `knowledge_cutoff`, and typed `credential:`
/ `env:` references that reject literal secrets.
- Added `ProviderId`, `ModelId`, and a shared `ReasoningEffort` enum in
`fabro-model`, plus adapter metadata for `anthropic`, `openai`,
`gemini`, and `openai_compatible`.
- Added a matching `fabro-llm` adapter factory registry with parity
tests to keep metadata keys and factory keys in sync.
- Added `[run.model.controls]` defaults through config resolution and
runtime settings types.
- Added a workspace policy test to prevent future `bootstrap_catalog`
use outside install/test-support paths.
### Plan Summary
- This is the foundation slice of the settings-driven catalog plan.
- Production still uses the existing `Provider` enum and
`Catalog::builtin()` call paths.
- ProviderId routing, OpenAPI regeneration, auth resolver changes,
resolved `Arc<Catalog>` injection, typed request speed, and per-speed
billing are deferred follow-ups.
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-bot <fabro-bot@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Expose browser and CLI auth sessions through a normalized API, and allow revoking active CLI refresh-token chains while keeping browser sessions non-revocable for v1.
Replaces the /settings/live-events placeholder with a working page that
streams server-wide events from /api/v1/attach. Shares the leader-owned
cross-tab EventSource so additional tabs subscribe without opening
parallel connections.
The page keeps an in-memory ring buffer (newest first, max 1,000) with
id or run_id:seq dedupe and resets on remount; live-only by design,
nothing is replayed on connect or persisted in the browser. Reuses the
existing event-debug filters, search, and details panel, and links each
row's run_id to /runs/:id. The category filter is the static set of
DebugCategory values so "All types" always matches.
Settings layout is now fullHeight-aware so the events page can fill the
viewport alongside the sub-nav. DebugEventDetailsPanel's event prop is
broadened to a shared EventDisplayPayload shape so it accepts both
EventEnvelope and the live payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists backend-discovered TCP services for a run's sandbox between the
Terminal and Filesystem tabs. Previewable ports open a signed Daytona
URL in a new browser tab; the rest render as Unavailable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Store live per-stage token counts on StageProjection, carry typed billing model identity through agent.message events, and derive billing rollups from the projection so in-flight stages can report usage before terminal events arrive.
Add committed, uncommitted, and all scope handling for run files with source reporting for sandbox and final patch responses.
Wire the run files page to persist scope in the URL and cache each scope independently.
Persist resolved run titles on creation, expose title update events, and add the run title PATCH API. Regenerate API clients and refresh web/server invalidation so title changes are reflected across run detail and board views.
Make local sandbox execution direct by removing the public worktree mode and in-place controls from CLI, config, run state, API surfaces, docs, and UI. Keep worktree support only for internal parallel-node isolation.
Project workflows now resolve from the discovered .fabro directory instead of honoring project.directory. Keep the legacy field parse-only while removing it from resolved settings and API/client shapes.
Expand the OpenAPI contract for frontend auth and workflow routes, regenerate the TypeScript Axios client, and route web API calls through generated client classes while preserving SSE and install exceptions.
Route async API failures through the body-preserving classifier and add run-create context so CLI output keeps server response details in the cause chain.
## 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>
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.
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.