## Summary
Adds a workflow-visible variables store and HTTP API for managing
non-sensitive run variables, then wires those variables into run config
interpolation before run creation, validation, and preflight.
## What Changed
- Adds `/api/v1/variables` CRUD endpoints backed by a JSON variable
store and generated Rust/TypeScript API types.
- Supports `{{ vars.NAME }}` interpolation alongside existing `{{
env.NAME }}` handling for run-owned config fields, including
environment, MCP, hook, artifact, checkpoint, SCM, and notification
settings.
- Reuses canonical `fabro-types` variable DTOs in `fabro-api` and adds
OpenAPI name patterns so clients see the same env-style variable
contract enforced by the server.
- Keeps variable updates store-owned with `update_existing`, avoiding
duplicated not-found/update semantics in the HTTP handler.
- Shares env-style name validation between variables, interpolation
parsing, and vault token names to avoid grammar drift.
Variables are intentionally non-sensitive: list/get responses include
values, unlike vault secrets.
## Validation
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-types`
- `cargo test -p fabro-variable`
- `cargo test -p fabro-api --test variable_round_trip`
- `cargo test -p fabro-server --features test-support --test it
api::variables`
- `cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-variable -p
fabro-vault --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy -p fabro-server --features
test-support --all-targets -- -D warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex/)
Run Events no longer leaves the pre-execution Initializing bar open when
a run fails before `run.running`. This addresses the waterfall symptom
in fabro-sh/fabro#426.
The phase derivation now records terminal `run.completed` / `run.failed`
events and uses them as fallback boundaries for Submitted, Pending,
Runnable, and Initializing phases. The existing `run.running` handoff
still takes precedence once execution actually starts.
Tested:
- `cd apps/fabro-web && bun test app/lib/run-phases.test.ts`
- `cd apps/fabro-web && bun run typecheck`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
Production code must not panic without a clear explanation of *why* the
failure is impossible. This PR upgrades panic-adjacent messages across
the codebase to meet that standard, and converts two genuine runtime
panics into proper error handling.
## What changed
**Invariant-explaining `expect` messages** — all existing `expect("short
label")` calls that guarded hard-coded literals, just-inserted map
entries, just-pushed Vec elements, or hard-coded regex/template strings
now carry a sentence explaining *why* the None/Err path cannot be
reached (e.g. `"node was just inserted by ensure_node, so get_mut cannot
return None"`). No behavior changes.
**`assert_eq!` → `panic!` with justification** in `strategy.rs` — the
bare assert is replaced with an explicit `panic!` whose message names
every existing call site that enforces the `CodexDevice ↔ OpenAI`
invariant, making future regressions easier to diagnose.
**Genuine runtime errors converted to `Result`** — `select_backend` /
`select_backend_for_gh_command` in the upgrade command previously called
`.expect()` on `http_client()`, which can fail due to TLS or environment
issues. Both functions now return `Result<Backend>` and propagate the
error to the CLI boundary.
**Signal handler panics degraded to warnings** in `serve.rs` —
`ctrl_c()` and `unix::signal()` failures no longer panic the server;
instead they log a warning and park the future, allowing the server to
keep running without graceful-shutdown support rather than crashing on
startup.
**Telemetry thread spawn failure** in `fabro-telemetry` — instead of
panicking, a failure to spawn the background thread logs a debug message
and silently disables telemetry, which is the correct degradation for an
optional observability feature.
**OS RNG `expect` messages** — three sites (`random_secret`,
`random_auth_code`, `generate_dev_token`) now explain that a failure
means the system RNG is broken and the security of the generated value
would be compromised, justifying the panic boundary.
### Fabro Details
<details>
<summary>Ran 3 stages in 45m 16s for $11.65</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| work | 34m 22s | $9.00 | 0 |
| audit | 10m 35s | $2.65 | 0 |
| **Total** | **45m 16s** | **$11.65** | **0** |
</details>
<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>
```dot
digraph Goal {
graph [
goal="Complete the user-provided goal",
rankdir=LR,
max_node_visits=30
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
work [
label="Work",
thread_id="goal",
fidelity="full",
max_visits=12,
prompt="@prompts/continue.md"
]
audit [
label="Completion Audit",
thread_id="goal",
fidelity="full",
goal_gate=true,
retry_target="work",
output_schema="routing",
output_retries=2,
max_visits=12,
prompt="@prompts/audit.md"
]
start -> work -> audit
audit -> exit [label="Done", condition="outcome=succeeded"]
audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
audit -> work [label="No clear verdict"]
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
## Summary
Implements the React Effects Policy by creating the approved hook
surface in `hooks/effects.ts` and migrating a broad set of direct
`useEffect` calls across the codebase to either purpose-named hooks or
non-effect patterns.
### Plan Summary
- Add `hooks/effects.ts` exporting `useMountEffect`, `useInterval`,
`useTimeout`, `useDebouncedValue`, `useWindowEvent`, `useDocumentEvent`,
`useDocumentTitle`, `useMediaQuery`, `useLocationHash`, and
`useResizeObserver`
- Extract large imperative effects into purpose-named hooks:
`useTerminalSession`, `useFloatingTooltipMeasurements`,
`useAnnotatedRunGraphSvg`, `useInstallEffects`, and others
- Move install session fetch from a component effect into a SWR query
(`install-query.ts`)
- Replace `useEffect` + `useState` state-derivation patterns with
render-time computation or ref callbacks
- Replace `AskFabroLayoutProvider`/`useAskFabroLayout` context with a
prop callback
## What changed and why
**`hooks/effects.ts`** — the new approved primitive surface. All
internal `useEffect` calls here are intentional; the hooks expose the
*external system* they manage rather than leaking `useEffect` to
component code. `useMediaQuery` and `useLocationHash` use
`useSyncExternalStore` instead of effect + state.
**`useTerminalSession`** — the largest extraction. The 130-line
xterm/WebSocket/ResizeObserver setup block moves from
`terminal-view.tsx` into its own hook, which now owns the `terminalRef`,
`fitRef`, and `socketRef` that previously cluttered the component.
`TerminalConnectionError` and `ConnectionStatus` types are exported from
the hook.
**`useFloatingTooltipMeasurements`** — extracts the `useLayoutEffect` +
ResizeObserver + window resize listener out of `FloatingTooltip`. The
`FloatingTooltipSize` type moves with it so consumers don't need to
import from the component.
**`useInstallSessionQuery` + `useInstallEffects`** — the install session
fetch moves from a component effect to SWR (`install-query.ts`). The
three remaining install effects (token URL scrubbing, GitHub error URL
scrubbing, health-poll restart) move into
`hooks/use-install-effects.ts`. The root-redirect effect is replaced
with a render-time `<Navigate>` gate. The `SessionState` discriminant
now carries `token` so stale query results can be discarded without an
effect chain.
**`SelectionCheckbox`** — `useEffect` setting `input.indeterminate` is
replaced with a ref callback, which runs synchronously after the node is
attached and avoids a stale-frame flash.
**`event-debug.tsx`** — the manual `window.addEventListener("keydown",
...)` pattern is replaced with `useWindowEvent`, removing the
`react-doctor-disable` suppression comments.
**`run-waterfall.tsx`** — the local `useTickingNow` is deleted;
`RunWaterfall` now calls the shared `useTickingNow` from `lib/time` with
the new `active` parameter signature.
**`toast.test.tsx`** — `useEffect(() => onReady?.(api), ...)` in the
test helper is replaced with a direct call during render, which is valid
because `onReady` has no side effects that React cares about.
**`AskFabroSidebar`** — `setIsResizing` from the layout context is
replaced with an `onResizeActiveChange` prop, removing the
`useAskFabroLayout` call and the hidden context coupling from the
sidebar.
### Fabro Details
<details>
<summary>Ran 3 stages in 114m 5s for $95.71</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| work | 103m 3s | $80.42 | 0 |
| audit | 10m 19s | $15.29 | 0 |
| **Total** | **114m 5s** | **$95.71** | **0** |
</details>
<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>
```dot
digraph Goal {
graph [
goal="Complete the user-provided goal",
rankdir=LR,
max_node_visits=30
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
work [
label="Work",
thread_id="goal",
fidelity="full",
max_visits=12,
model="gpt-55",
reasoning_effort="xhigh",
prompt="@prompts/continue.md"
]
audit [
label="Completion Audit",
thread_id="goal",
fidelity="full",
goal_gate=true,
retry_target="work",
output_schema="routing",
output_retries=2,
max_visits=12,
model="gpt-55",
reasoning_effort="xhigh",
prompt="@prompts/audit.md"
]
start -> work -> audit
audit -> exit [label="Done", condition="outcome=succeeded"]
audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
audit -> work [label="No clear verdict"]
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Opt the /runs route into the shell's full-height flex chain, then propagate
height through the page root, the columns scroll container, and the list view
wrapper. Previously the board only extended to its content height, so the
empty space below was non-interactive — you could only scroll horizontally
from the top half of the page.
## Summary
Replaces ~285 lines of hand-rolled Tooltip, HoverCard, and Toast code in
`fabro-web` with battle-tested primitives — gaining real keyboard
accessibility, Radix collision detection, and Sonner's toast lifecycle —
while keeping all 13+ call sites unchanged.
### Plan Summary
- **Tooltip + HoverCard → Radix wrappers**: `@radix-ui/react-tooltip`
and `@radix-ui/react-hover-card` replace the DIY `useHoverAnchor` hook.
A `TooltipProvider` is mounted in `app-shell.tsx` (200ms delay, 300ms
skip-delay for grouped sidebar hovers). `<Tooltip>` self-wraps in a
local provider when rendered outside the shell (tests, isolated mounts).
- **Toast system → Sonner**: `toast.tsx` shrinks to a ~30-line shim
preserving the `{ push, dismiss, clear }` API. `ToastProvider` becomes a
no-op pass-through in DOM contexts; in non-DOM test environments it
renders an `aria-live` fallback backed by `useSonner` so test assertions
still work. The `action` field is dropped (was test-only).
`toast.test.tsx` is rewritten against observable rendered text.
- **CSS-only tooltips → `<Tooltip>`**: Two inline `group-hover/*` blocks
in `settings-models.tsx` are swapped for the new wrapper, gaining
keyboard focus + Esc dismiss + collision avoidance.
- **SVG-anchored hovers → `FloatingTooltip`**: A new
`app/components/floating-tooltip.tsx` helper portals to `document.body`
and computes collision-avoiding `top`/`bottom` placement from a raw
`DOMRect` (no wrappable trigger). It absorbs `hover-card-style.ts`
(deleted) and is used by `run-overview.tsx` and `event-debug.tsx`.
### What changed and why
**`FloatingTooltip`** handles the two SVG/Graphviz hover sites where
there is no React trigger element to wrap — only a `DOMRect` measured
from DOM events. It uses `useLayoutEffect` + `ResizeObserver` to measure
its own rendered size before applying final position, so it never clips
at viewport edges. This is the one place a `useLayoutEffect` is
intentional and documented.
**`Tooltip` provider fallback**: Radix throws if `<Tooltip>` renders
without an ancestor `TooltipProvider`. Rather than requiring every test
to mount the shell, the component detects provider presence via context
and injects a local one when needed.
**Toast shim backward-compat**: `ToastProvider` previously accepted
`autoDismissMs` as a prop; that prop is silently dropped. The `action`
field on `ToastInput` is removed (only one test referenced it —
`run-detail.test.ts` is updated accordingly). All other consumers
compile without changes.
**CSP fix** (bundled): `img-src` gains
`https://avatars.githubusercontent.com` to allow GitHub avatar images,
with the corresponding integration-test assertion updated.
### Fabro Details
<details>
<summary>Ran 8 stages in 54m 1s for $32.86</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 21m 38s | $22.48 | 0 |
| simplify_opus | 13m 51s | $7.00 | 0 |
| simplify_gpt | 3m 54s | $3.38 | 0 |
| verify | 9m 35s | – | 0 |
| **Total** | **54m 1s** | **$32.86** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Switches the goal workflow's work and audit nodes from the default
claude-sonnet to gpt-55 with xhigh reasoning, matching the implement
node in implement-plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recent CSP enforcement blocked avatars.githubusercontent.com images
used by run cards in the web UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Fixes shared-thread workflow stages that compact their session before
routing/audit bookkeeping finishes. The workflow backend now records
token usage from each `Session::process_input` call as it happens,
instead of slicing assistant turns out of the final session history
after the session may have been compacted or replaced.
## Changes
- Track per-input token usage inside `fabro-agent::Session` alongside
the existing timing data.
- Use the recorded per-input usage in the workflow LLM backend for
initial prompts, retry-after-compaction prompts, and schema repair
prompts.
- Keep the invariant panic message for inconsistent session history
explicit with `expect(...)`.
- Add a black-box workflow integration test that drives a shared-thread
audit through pre-routing compaction and asserts the audit still
succeeds.
## Verification
- `ulimit -n 4096 && cargo nextest run --workspace`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cd apps/fabro-web && bun test --isolate`
- `cd apps/fabro-web && bun run typecheck`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- After rebasing onto current `origin/main`: `ulimit -n 4096 && cargo
nextest run -p fabro-workflow --test it
integration::shared_thread_compaction_before_routing_audit_succeeds`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Enforces Fabro's CSP by switching from
`Content-Security-Policy-Report-Only` to `Content-Security-Policy` while
preserving the SPA sources we know are required. The policy now hashes
the install-mode inline bootstrap and allows `ws:`/`wss:` connections so
terminal WebSockets do not regress under enforcement.
The security headers integration test now asserts enforced CSP behavior,
and the public security docs now describe the default headers Fabro
emits.
## Verification
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-server csp::tests --lib`
- `cargo test -p fabro-server security_headers::tests --lib`
- `cargo test -p fabro-server --features test-support --test it
security_headers_are_applied_to_all_responses`
- Browser QA against an enforced local server: login, runs list,
settings, and automation diagram rendered with no CSP console violations
or page errors.
- Live listener on `127.0.0.1:32276` restarted and verified to emit
`content-security-policy` with no report-only header.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
The terminal WebSocket origin check (`origin_allowed` in
`handler/sandbox.rs`) rejected browser requests when the `Host` header
omitted the default port for the scheme. Result: clicking the
**Terminal** tab on `/runs/<id>/sandbox` returned **403 Forbidden** and
the UI showed "Terminal WebSocket connection failed." Other tabs
(Services, Filesystem, VNC) worked because their WebSockets either don't
traverse the server (VNC connects directly to Daytona's signed preview
URL) or aren't WebSocket upgrades.
## Root cause
Browsers send `Origin: https://example.com` and `Host: example.com` (no
`:443`) on default HTTPS. The previous logic always constructed the
origin authority *with* the default port, then string-compared against
the raw `Host` header:
```rust
let origin_authority = match origin_url.port_or_known_default() {
Some(port) => format!("{origin_host}:{port}"),
None => origin_host.to_string(),
};
origin_authority.eq_ignore_ascii_case(host)
```
So `"example.com:443"` got compared against `"example.com"` and never
matched. Every browser-driven WS upgrade to a default-port HTTPS
deployment failed.
## Fix
Parse the `Host` header through the origin's scheme into another `Url`,
then compare `host_str()` and `port_or_known_default()` on both sides.
This normalizes default ports symmetrically.
```rust
let Ok(host_url) = url::Url::parse(&format!("{}://{host}", origin_url.scheme())) else {
return false;
};
origin_url.host_str() == host_url.host_str()
&& origin_url.port_or_known_default() == host_url.port_or_known_default()
```
Reproduced in a production deployment of the nightly image behind Caddy
doing TLS termination on a public IP. Before the fix the terminal WS
handshake returned 403 every time; with the fix the handshake completes
and the terminal session attaches.
## Tests
Added four new cases alongside the existing two:
- `origin_validation_allows_default_https_port_omitted_from_host` — the
bug case (browser-style `Origin: https://host` + `Host: host`).
- `origin_validation_allows_default_http_port_omitted_from_host` — same
for plain HTTP.
- `origin_validation_allows_explicit_default_port_in_host` — `Host:
example.com:443` still matches `Origin: https://example.com`.
- `origin_validation_rejects_scheme_mismatch_on_default_port` — `Origin:
http://example.com` + `Host: example.com:443` is still rejected
(different effective ports).
All six `origin_validation_*` tests pass; the full `fabro-server` suite
stays green (679/679).
## Test plan
- [x] `cargo nextest run -p fabro-server origin_validation` — 6 passed
- [x] `cargo nextest run -p fabro-server` — 679 passed
- [x] `cargo +nightly-2026-04-14 fmt --check --all`
- [x] `cargo +nightly-2026-04-14 clippy -p fabro-server --all-targets --
-D warnings`
- [x] Manual: terminal tab in the SPA against a TLS-terminated
default-port deployment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Adds an internal React effects policy for `apps/fabro-web` so direct
component effects are exceptional and real external integrations move
behind purpose-named hooks.
The policy covers preferred alternatives such as render-time derivation,
SWR query hooks, mutation callbacks, URL/router primitives, keyed
resets, and `useSyncExternalStore`. It also documents guardrails for
`useMountEffect`, React 19 `useEffectEvent`, one-shot telemetry effects,
migration workflow, current hotspots, and review checklist.
## Verification
Not run; docs-only change.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Settings > Integrations now reflects the server's actual integration
readiness instead of only static `settings.toml` booleans. This adds
`/api/v1/system/integrations` as the runtime source of truth, covering
server config, vault credential presence, and Slack Socket Mode
connection state.
## What Changed
- Added shared `fabro-types` integration status models and reused them
from `fabro-api` to avoid duplicate API/domain types.
- Added `GET /api/v1/system/integrations` to the OpenAPI spec, Rust
server routes, demo routes, and generated TypeScript client.
- Reports GitHub and Slack status as `disabled`, `missing_credentials`,
`configured`, `connecting`, `connected`, or `error`, with non-secret
metadata and missing credential names.
- Tracks Slack Socket Mode runtime state from the Slack connection loop
and respects explicit `server.integrations.slack.enabled = false` even
when vault tokens exist.
- Updated the Integrations settings page to read the new runtime
endpoint, so a vault-configured Slack setup no longer appears simply as
disabled.
## Verification
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-api system_integrations`
- `cargo nextest run -p fabro-config
resolved_server_integrations_are_slack_only_for_chat`
- `cargo nextest run -p fabro-slack
run_event_loop_notifies_connected_status`
- `cargo nextest run -p fabro-server --features test-support --test it
get_system_integrations`
- `cargo nextest run -p fabro-server`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd apps/fabro-web && bun test
app/routes/settings-integrations.test.tsx app/lib/query-keys.test.ts`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun run build`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
The `/setup` screen rendered after GitHub redirects post-install (params
`installation_id` + `setup_action=install`) showed copy that assumed the
user was retrying a failed run:
> **Retry the run** — Start the run or preflight again so Fabro can
clone the repository and push checkpoint branches with the new
installation.
But this screen is also where **first-time installers** land during
onboarding, when there is no prior run to retry. The "retry" framing is
confusing in that path.
## Fix
Rewrite step 2 to be neutral between onboarding and retry-after-failure:
> **Use the new installation** — Sign in and start a run or preflight.
Fabro can now clone repositories and push checkpoint branches using the
new installation.
The CTA below the steps ("Continue to sign in") and step 1 ("Return to
Fabro / The GitHub App is installed for the selected account or
repositories") already work for both paths — only step 2 was over-fit.
No structural changes; the route still keys off the same query params.
Update `setup.test.ts` to assert the new title.
## Test plan
- [x] `bun test app/routes/setup.test.ts` — 1 pass
- [x] `bun run typecheck` — clean
- [x] Manual: behavior unchanged for first-time-setup path (no install
params); only the post-install variant text changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Audit and remediation pass enforcing the project's
no-panic-in-production policy. Every `unwrap()` on a mutex/RwLock in
reachable runtime code is replaced with `expect()` carrying a message
that explains *why* the lock cannot be poisoned (no code panics while
holding it). Bare `unreachable!()` and `panic!()` calls are updated with
messages that name the invariant being asserted. One genuine bug is
fixed in the process.
## What changed
**`unwrap()` → `expect()` on locks** (`fabro-core`, `fabro-oauth`,
`fabro-util`, `fabro-workflow/*`, `fabro-server`): Every
`Mutex`/`RwLock` `.unwrap()` in production paths now carries the
standard justification pattern: `"<name> mutex/RwLock should not be
poisoned: no code panics while holding this lock"`.
**`unreachable!()` and `panic!()` message quality**: Bare
`unreachable!()` calls in `subagent.rs`, `wait.rs`, `condition.rs`,
`event/convert.rs`, and `server.rs` now name the structural invariant
(e.g. "outer match arm already verified…"). The `panic!` in `tools.rs`
now includes the offending name and the expected format, making it
actionable.
**`sha_newtype` / `short_sha_newtype` in `run_files.rs` — actual bug
fix**: These helpers previously called `unwrap_or_else(|e| panic!(…))`
on git output, meaning a malformed SHA from a real git subprocess would
panic in a request handler. They now return `Result<T, ApiError>` and
propagate errors to callers, which in turn propagate with `?`. This is
the only change that alters observable behavior under failure.
**Demo-only panics in `fabro-server/src/demo/mod.rs`**: Panic messages
updated to clarify that these paths operate on hardcoded compile-time
constants, so the panic is a programming-error guard rather than a
runtime failure guard.
## Design note
The lock-poisoning `expect` messages all follow a single template so
reviewers can quickly verify the claim: if you ever add code that can
panic inside a lock guard scope, the message becomes a lie and that must
be caught in review. The uniformity is intentional.
### Fabro Details
<details>
<summary>Ran 0 stages in 64m 54s for $14.28</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **64m 54s** | **$14.28** | **0** |
</details>
<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>
```dot
digraph Goal {
graph [
goal="Complete the user-provided goal",
rankdir=LR,
max_node_visits=30
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
work [
label="Work",
thread_id="goal",
fidelity="full",
max_visits=12,
prompt="@prompts/continue.md"
]
audit [
label="Completion Audit",
thread_id="goal",
fidelity="full",
goal_gate=true,
retry_target="work",
output_schema="routing",
output_retries=2,
max_visits=12,
prompt="@prompts/audit.md"
]
start -> work -> audit
audit -> exit [label="Done", condition="outcome=succeeded"]
audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
audit -> work [label="No clear verdict"]
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Visible by default to the right of Status; toggleable via the column
picker. Extracts the principal avatar/label helper out of the run
summary panel so both surfaces share one renderer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status filter operates on the eight non-archived BoardColumn lanes and
filters both the board (hides whole lanes) and the list (hides rows).
Show archived remains a standalone toggle alongside it; an `archived`
token in a previously-saved status string is migrated into the toggle on
read so the two controls stay independent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Retires the built-in OpenAI catalog rows for GPT-5.2 and GPT-5.3-era
models while preserving compatibility through aliases on the closest
remaining replacements.
`gpt-5.2`, `gpt5`, `gpt-5.3-codex`, and `codex` now resolve through
`gpt-5.4`; `gpt-5.3-codex-spark` and `codex-spark` now resolve through
`gpt-5.4-mini`. The Rust tests that pinned individual declarative
catalog rows were removed so future catalog updates stay data-only.
## Verification
- `cargo nextest run -p fabro-model`
- `cargo nextest run -p fabro-server list_models`
- `cargo +nightly-2026-04-14 fmt --check --all`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
- Render the Test column as an icon-only status
(queued/testing/ok/failed) so a long error message no longer expands the
column width.
- Move the failure message into a hover/focus tooltip — wider,
monospaced, and preserving newlines for readable multi-line errors.
## Test plan
- [ ] Visit `/settings/models`, run "Test models", and confirm the Test
column stays narrow regardless of error length.
- [ ] Hover/focus a failed row's icon and verify the tooltip shows the
full multi-line error in monospace.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
`fabro model test` (CLI) probes every configured model with a cheap "Say
OK" prompt and prints a results table. Until now, the equivalent on
`/settings/models` was "open a terminal." This PR adds a single **Test
models** button in the section header that runs the same sweep against
the visible rows and renders per-row results inline. Wire format is the
existing `POST /api/v1/models/{id}/test` — no backend changes.
## Behavior
- One button beside the provider filter + search. Tests *whatever the
table currently shows* (filter + search applied at click time).
- Concurrency cap of 4 to mirror the CLI's `--jobs 4` default.
- Rows render `Queued` → `Testing…` → `Ok` (mint check) or red X +
truncated error (full message on hover via `title`).
- After each sweep, a small `N ok · M failed` chip appears next to the
button (mint when clean, coral on failures).
- Re-clicking starts a fresh sweep over the current view.
## Out of scope (deliberately)
- **No deep-test toggle** — page calls basic mode only; `fabro model
test --deep` still covers that case from the CLI.
- **No per-row Test button** — the page-level sweep replaces it.
- No cancellation, no result persistence across navigation/refresh, no
toast — the inline state *is* the feedback.
## Files
- `apps/fabro-web/app/routes/settings-models.tsx` — `RowState`/`Sweep`
types, `runSweep` worker pool, header button + summary chip, new "Test"
column, `TestStatusCell` component.
- `apps/fabro-web/app/components/state.tsx` — `Spinner` is now exported
(was previously private).
## Test plan
- Click "Test models" with several configured providers → rows flip in
waves of 4; summary lands as `N ok · 0 failed`.
- Revoke a provider's API key, click again → that provider's rows end in
red X with the upstream error in the cell (full text on hover).
- Apply a provider filter, click → only filtered rows test.
- DevTools Network panel → at most 4 in-flight `/models/<id>/test`
requests at any time.
---
[](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
Improves the web UI's React Doctor audit score by separating reusable
helpers from React component modules, tightening effect/state ownership,
and extracting real component boundaries in the install wizard, stage
activity view, run-files diff browser, RunDetail route, and Runs
workspace. The branch removes the previously deferred RunDetail and Runs
giant-component diagnostics without changing RunDetail UX, route
contracts, action ordering, or Runs workspace behavior.
| Metric | Main baseline | Initial PR | Current PR |
|--------|---------------|------------|------------|
| React Doctor score | 63 | 71 | 99 |
| React Doctor errors | 123 | 0 | 0 |
| React Doctor warnings | 241 | 163 | 3 |
| React Doctor diagnostics | 364 | 163 | 3 |
## Changes
- Moves exported helper logic out of component files so Fast
Refresh/component-export rules no longer dominate the audit.
- Adds a targeted React Doctor config exception for React Router route
modules, where non-component exports like route metadata are
intentional.
- Refactors low-risk state/effect patterns: keyed interview question
state, reducer-backed editable run title state, event-owned preview
opening, route-keyed insights editor initialization, refresh timer
ownership, and selection/derived list cleanup.
- Reworks `InstallApp` around an install reducer, a controller hook for
install lifecycle state, and focused wizard step components for LLM,
server, object-store, sandbox, and GitHub setup.
- Moves `RunStages` selected-stage activity into a keyed boundary for
panel/debug detail state while preserving stage activity filters across
navigation.
- Extracts the `RunFiles` loaded diff-browser view from route/query
coordination so the route owns data/URL state and the loaded view owns
rendering.
- Splits `RunDetail` into route-local header, actions, tab shell, docked
controls, model, and lifecycle-toast modules; the actions menu now uses
grouped descriptors instead of a large boolean/callback prop matrix.
- Extracts Runs workspace preference ownership into
`useRunsWorkspacePreferences` and moves toolbar rendering into
`RunsToolbar`, leaving the route focused on data, DnD state, filtering,
and view selection.
- Guards `InsightsEditor` query execution with a latest-run id and
timeout cleanup so stale or unmounted mock query runs cannot overwrite
newer results.
- Adds regression coverage for archived-run deletion from RunDetail and
stale-result handling in InsightsEditor.
- Improves semantic/accessibility coverage with labeled controls, native
meter/section semantics, decorative status dots, and clearer unavailable
copy.
- Removes dead UI code and applies local suppressions only where the
rule is a documented false positive or an intentional imperative
integration boundary.
## Remaining React Doctor warnings
Current score is 99 with 0 errors and 3 warnings. The remaining warnings
are intentionally left for separate judgment rather than mechanical
churn:
- `prefer-useReducer` (3): `AutomationsNew`, `InsightsEditor`, and
`CreateSecretForm` need reducers only if they encode real coupled
transitions, not simple field setters.
## Verification
- `cd apps/fabro-web && bun test app/routes/run-detail.test.ts` -> `22
pass`, `0 fail`
- `cd apps/fabro-web && bun test app/routes/insights-editor.test.tsx
app/routes/runs.preferences.test.tsx` -> `7 pass`, `0 fail`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test --isolate` -> `490 pass`, `0 fail`
- `cd apps/fabro-web && bunx react-doctor@latest --full --json >
/tmp/fabro-react-doctor-runs-insights.json` -> score `99`, `0` errors,
`3` warnings
- Earlier branch verification also included `cd apps/fabro-web && bun
run build`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context not reported, default reasoning) via
[Codex](https://openai.com/codex)
## Summary
Every `stage.completed` and `run.completed/failed` event has reported
`inference_time_ms: 0, tool_time_ms: 0` since timing fields were wired
up in #343. Two independent bugs caused this: handlers never populated
`Outcome.timing`, and engine-failure terminal paths discarded the
rolled-up conclusion entirely.
## What changed and why
### Bug 1 — Handlers never populated `Outcome.timing`
**`fabro-agent/session.rs`**: Added `SessionInputTiming { inference:
Duration, tool: Duration }` accumulators to `Session`.
`run_single_input` now takes a `&mut SessionInputTiming` and records
elapsed time at every exit point of the `'streamattempts` loop (stream
open, retry, cancel, error, normal completion) plus a `tool_start` /
`tool_elapsed` wrap around `execute_tool_calls`. The per-input total is
exposed via `session.last_input_timing()` after
`process_input_with_runtime` returns, even on error.
**`CodergenResult::Text`**: Added a `timing: StageTiming` field. All
backends now populate it:
- `AgentApiBackend::run` accumulates `session.last_input_timing()`
across inputs and any structured-output repair turns (repair turns now
use `process_input_with_runtime` instead of `process_input` so timing is
captured there too).
- `AgentApiBackend::one_shot` wraps `complete_one_shot_request` with
`Instant`/`elapsed` across repair iterations; all time is attributed to
inference.
- `AgentAcpBackend::run` uses `result.duration_ms` attributed entirely
to inference (ACP is opaque about the split).
**`AgentHandler`, `PromptHandler`, `FanInHandler`, `CommandHandler`**:
Each now sets `outcome.timing = Some(timing)` from the backend result
before returning. `CommandHandler` attributes `result.duration_ms` to
tool time (`StageTiming::active_only(0, duration_ms)`). The failure
branches (structured-output exhausted retries) also carry timing forward
so no timing is lost on partial success.
**`StageTiming::active_only`**: New constructor added to `fabro-types`
for the handler→executor hop where wall time is ignored (executor's own
stopwatch is authoritative for wall).
### Bug 2 — Engine-failure paths discarded the conclusion
**`start.rs`**: Introduced `emit_workflow_run_failed` as a shared helper
that calls `build_conclusion_from_store` (which already does the full
per-stage rollup) and uses `conclusion.timing` and `conclusion.billing`
when emitting `WorkflowRunFailed`, instead of
`RunTiming::wall_only(...)` and `None`.
All three terminal failure paths now go through this helper:
- `persist_terminal_engine_failure` — main
`VisitLimitExceeded`/engine-error path
- `DetachedRunBootstrapGuard::drop` — takes `RunStoreHandle` as a new
field (cloned in at arm time)
- `DetachedRunCompletionGuard::drop` — same
- `persist_detached_failure` — now accepts `&RunStoreHandle` and
delegates to `emit_workflow_run_failed`
### Refactoring
`test_usage` helper was duplicated across `billing_rollup` and
`event/convert` test modules; both now import from
`crate::test_support`. `scheduler_capacity` predicate
(`counts_toward_scheduler_capacity`) was extracted from the inline
closure in `spawn_scheduler` and reused in the `GET /system/info`
handler for the new `scheduler_slots_used` field — a pre-existing
separate fix included in this changeset.
### Plan Summary
- **A1** — `SessionInputTiming` accumulators in `Session`;
`last_input_timing()` getter
- **A2** — `CodergenResult::Text { timing }` field; all three backends
populate it
- **A3** — All four active-work handlers (`agent`, `prompt`, `fan_in`,
`command`) set `outcome.timing`
- **B1** — `persist_terminal_engine_failure` uses conclusion's rolled-up
timing + billing
- **B2** — Both drop guards and `persist_detached_failure` also use
`emit_workflow_run_failed`
### Fabro Details
<details>
<summary>Ran 8 stages in 76m 38s for $39.81</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 3s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 35m 53s | $17.74 | 0 |
| simplify_opus | 22m 51s | $17.18 | 0 |
| simplify_gpt | 4m 0s | $4.89 | 0 |
| verify | 8m 59s | – | 0 |
| **Total** | **76m 38s** | **$39.81** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
## Summary
Adds a reusable `goal` workflow that runs an immutable user goal through
a Work -> Completion Audit loop. The work prompt keeps the full
objective intact, while the audit prompt uses validated routing JSON to
either exit when the goal is proven complete or loop back with concrete
remaining work.
## Workflow Diagram

## Verification
- `cargo run -q -p fabro-cli -- validate
.fabro/workflows/goal/workflow.fabro`
- `cargo run -q -p fabro-cli -- run goal --goal "Test the reusable goal
workflow" --dry-run`
- `cargo run -q -p fabro-cli -- preflight
.fabro/workflows/goal/workflow.toml --goal "Test the reusable goal
workflow"`
- `xmllint --noout .fabro/workflows/goal/workflow.svg`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via Codex
## Summary
Fixes the Settings Resources concurrency meter so it reports scheduler
capacity usage instead of all non-terminal runs. `/api/v1/system/info`
now exposes `runs.scheduler_slots_used`, computed from the same status
predicate the scheduler uses, while `runs.active` remains unchanged for
existing lifecycle semantics.
The settings page uses only the new slot count, so pending approval runs
and runnable queued runs no longer make the concurrency meter look full.
## Verification
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-server --features test-support
worker_started_child_run_requires_approval_before_becoming_runnable`
- `cargo nextest run -p fabro-server --features test-support
scheduler_capacity_counts_only_runs_occupying_slots`
- `cargo nextest run -p fabro-server --features test-support
get_system_info_returns_runtime_fields`
- `cargo nextest run -p fabro-server --features test-support
test_app_state_with_options_respects_max_concurrent_runs`
- `cargo nextest run -p fabro-server --features test-support
openapi_conformance`
- `bun test app/routes/settings-monitoring.test.tsx`
- `bun run typecheck`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context unknown, reasoning unknown) via
[Codex](https://openai.com/codex)
Show the pager only when there's actually more than one page or the
user is past page 1, replacing the hardcoded total >= 25 threshold.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.
## What changed
**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.
**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.
**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).
**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.
**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.
**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.
**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.
### Plan Summary
- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.
### Fabro Details
<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
## Summary
Updates the built-in Gemini catalog for the current Gemini API lineup by
adding `gemini-3.5-flash` and promoting `gemini-3.1-flash-lite` to the
canonical small default. The old `gemini-3.1-flash-lite-preview` ID
remains accepted as an alias and resolves to the stable API ID, avoiding
a breaking change for existing workflows.
The catalog test changes remove Gemini-specific data assertions and keep
only a generic small-default invariant, so future declarative catalog
updates do not require Rust test churn.
## Verification
- `cargo nextest run -p fabro-model`
- `cargo +nightly-2026-04-14 fmt --check --all`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, default reasoning) via
[Codex](https://openai.com/codex)
## Summary
Adds three small informational labels to `/settings/models`, all driven
from existing fields on `Provider` and `Model` (no API changes):
- **Priority** — on the configured provider with the highest catalog
`priority`
- **Default** — next to each provider's default model (`model.default`)
- **Small** — next to models flagged as the provider's small default
(`model.small_default`)
A single shared `Label` helper renders them in a subtle uppercase pill
style consistent with other section accents on the page.
## Test plan
- [ ] Visit `/settings/models` and confirm one configured provider shows
a "Priority" label next to its name
- [ ] Confirm each provider has at most one model labeled "Default" in
the Models table
- [ ] Confirm models with `small_default = true` show a "Small" label
(alongside "Default" if both)
- [ ] Confirm unconfigured providers are unaffected (filtered out before
the Models table)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Demo mode remains available via the X-Fabro-Demo header or the
fabro-demo=1 cookie set manually in browser devtools, but the UI
button and the POST /api/v1/demo/toggle endpoint are gone. The
fixture machinery and the auth/me demoMode flag (used by the SPA to
render Automations and the /start landing) are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors `fabro model list` output below the existing Providers panel.
Server-side provider + query filters, debounced search, sortable
columns, and a hover/focus popover that surfaces model aliases.
Genericizes SortHeader so non-runs tables can reuse it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the Models default with an overview landing at /settings that
shows each settings page as a card with icon, name, and one-line
description, grouped by General / Administration with a divider before
Live Events. Settings nav metadata is restructured into navSections and
exported so the sidebar and landing share a single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group sidebar items under General and Administration section labels;
default Settings landing page to Models; rename General page to Server
(now at /settings/server); rename Resources to Monitoring (now at
/settings/monitoring) with ChartBarSquare icon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PrCard stacked stats, actions+elapsed, and diff stats as three sibling
rows, so +adds/-dels rendered below elapsed. Consolidate into a single
PrCardFooter component so future inline metadata extends one row instead
of stacking another.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the run t-shirt size (XS/S/M/L/XL) in both the main runs
list and the Children sub-tab, visible by default. L renders in
amber and XL in coral to flag risky and unhealthy runs at a glance.
Extracts a shared SizeChip component used by the run header and the
table cell, derives Ord on RunSize so the new sort key (server-side
ListRuns sort) orders by bucket, and reorders TOGGLEABLE_COLUMNS so
the column picker mirrors the visible table order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that Size is a first-class column in the runs list, Elapsed is
redundant with it for at-a-glance scanning. Hide Elapsed by default
alongside Updated and Changes; users can still reveal it via the
column picker.
Existing users with stored prefs from the previous "updated,changes"
default keep their stored value, so they'll see both Elapsed and Size
until they toggle Elapsed off (or clear localStorage). New users get
the cleaner default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>