mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
545 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4cff07373c
|
feat: add server-owned environment store (Task 1 & 2 foundation) (#446)
## Summary
Moves environment definitions out of project/workflow TOML config and
into server-owned files, introducing the `fabro-environment` crate and
enforcing source-aware validation so project/workflow/user configs can
no longer define environment catalogs.
### What changed
**New `fabro-environment` crate** — workspace crate wired into
`fabro-cli` and `fabro-server`. Exposes a `seeded_catalog_layer()` that
CLI commands inject at the call site to fill the environment catalog
that settings resolution requires.
**Config environments are now migration-only** — `defaults.toml` no
longer ships a built-in `[environments.*]` catalog. Instead:
- `SettingsSource` enum tags every parsed layer (ActiveSettings,
Project, Workflow, DirectRun, User).
- `validate_settings_source` rejects `[environments.<id>]` in any source
except `ActiveSettings` with a targeted message: `[environments.<id>] is
now server-managed; move this definition to the server environments
directory`.
- TOML-provided
`run.environment.{image,resources,network,lifecycle,labels,volumes,env}`
overrides are also rejected; only `run.environment.id` survives.
**New migration** (`2026052801_settings_environments_to_server_files`) —
chains after the existing legacy-sandbox migration. Extracts
`[environments.*]` entries from `settings.toml` into sibling
`environments/<id>.toml` files, writes a
`.settings-environments-migration.bak` backup, and fails without
modifying any file if a target already exists.
**Builder API additions** —
`RunSettingsBuilder::load_from_with_catalog`,
`load_default_with_catalog`, `from_toml_with_catalog` let callers inject
a server-side catalog; the bare `from_toml` path now errors if no
catalog is present and a named environment is selected.
`WorkflowSettingsBuilder` test helpers in `src/tests/mod.rs` centralise
catalog injection across all config tests.
**`.fabro/project.toml`** — removed the inline
`[environments.fabro-dev]` block (environment definition now lives
server-side).
### Key design decisions
- CLI offline commands (graph, preflight, validate) use
`seeded_catalog_layer()` as a local stand-in until a running server is
available — matches the pre-existing behaviour without regressing
offline workflows.
- `load_settings_path` no longer runs migrations for non-ActiveSettings
sources, preventing project/workflow files from accidentally triggering
file-system writes.
- The `MigrationReport` type is now the new migration's
`SettingsEnvironmentsMigrationReport` (exposes `contents: String`
instead of a parsed layer), keeping `load.rs` simpler and decoupled from
layer parsing.
### Fabro Details
<details>
<summary>Ran 9 stages in 143m 23s for $105.27</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 11s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 28m 27s | – | 0 |
| simplify_opus | 37m 27s | $53.28 | 0 |
| simplify_gpt | 20m 50s | $12.14 | 0 |
| verify | 6m 3s | – | 0 |
| fixup | 45m 15s | $39.84 | 0 |
| **Total** | **143m 23s** | **$105.27** | **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>
|
||
|
|
29a9a3f7d6
|
refactor: Remove inbound IP allowlisting (#443)
## Summary
Removes Fabro's in-process inbound source-IP allowlist entirely.
`[server.ip_allowlist]` and
`[server.integrations.github.webhooks.ip_allowlist]` are gone from
config parsing, resolved settings types, the OpenAPI spec, generated API
clients, and the Settings > Security UI. Existing `settings.toml` files
containing those keys now fail as unknown fields — this is a hard
removal with no migration path.
Network source restrictions should be enforced upstream via a reverse
proxy, firewall, VPN, Tailscale ACLs, Kubernetes ingress, or platform
policy.
### What changed
- **Config/types** (`fabro-config`, `fabro-types`): Removed
`ServerIpAllowlistLayer`, `ServerIpAllowlistOverrideLayer`,
`ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`,
`IpAllowEntry`, associated resolver functions, GitHub `/meta` hook-range
parsing, and Unix socket trusted-proxy validation. `ipnet` dropped from
`fabro-types`; kept in `fabro-config` for sandbox CIDR validation.
- **Server runtime** (`fabro-server`): Deleted `ip_allowlist.rs`,
removed `IpAllowlistConfig` parameter from `build_router_with_options`
and `RouterOptions`, removed the global allowlist middleware layer, and
removed `GitHubMetaResolver` startup logic. GitHub webhook HMAC
verification is unchanged.
- **OpenAPI + generated clients**: Removed `ServerIpAllowlistSettings`,
`ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,
`LiteralIpAllowEntry`, `GitHubMetaHooksEntry` schemas; removed
`ip_allowlist` from `ServerNamespace` and `IntegrationWebhooksSettings`;
dropped `IpAllowEntry` re-exports from `fabro-api`.
- **Web UI**: Removed IP allowlist row from Settings > Security; updated
nav description and page copy.
- **Docs/changelog**: Security docs explicitly state Fabro provides no
source-IP filtering and direct operators upstream. Changelog entry dated
2026-05-27 documents the breaking removal and annotates the 2026-04-19
entry where the feature was introduced.
### Also in this diff (unrelated to IP allowlisting)
The worker control stream was migrated from reading newline-delimited
JSON on stdin to a reconnecting WebSocket
(`/api/v1/runs/{id}/worker/control-stream`). This adds
`tokio-tungstenite` to `fabro-cli`/`fabro-server`, introduces
`WorkerControlManagerHandle` with backoff reconnection and deduplication
of replayed delivery IDs, and adds `RunPause`/`RunUnpause` message
handling. A new integration test
(`detached_run_cancel_reaches_worker_over_control_websocket`) exercises
the full cancel path over the WebSocket.
### Key decisions
- **Hard removal via `deny_unknown_fields`**: stale config is
immediately visible as a startup error rather than silently ignored.
- **No stub or default pass-through**: `IpAllowlistConfig::default()` is
gone, not left as a no-op wrapper, to avoid keeping the feature shape
alive.
- **Webhook HMAC boundary unchanged**: source-IP filtering on webhook
routes is removed; cryptographic signature verification remains the
security boundary.
### Fabro Details
<details>
<summary>Ran 9 stages in 59m 53s for $27.24</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 22s | – | 0 |
| implement | 33m 54s | $22.81 | 0 |
| simplify_opus | 5m 55s | $0.75 | 0 |
| simplify_gpt | 3m 35s | $2.81 | 0 |
| verify | 8m 34s | – | 0 |
| fixup | 2m 24s | $0.87 | 0 |
| **Total** | **59m 53s** | **$27.24** | **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>
|
||
|
|
475b4ab650
|
Replace stdin JSONL control pipe with WebSocket worker control bus (#440)
## Summary
Workers no longer receive control messages over stdin JSONL. A new
`WorkerControlBus` abstraction (backed by `LocalWorkerControlBus` for
local/single-node deployments) publishes `WorkerControlEnvelope`
messages server-side; a worker-initiated WebSocket at `GET
/runs/{id}/worker/control-stream` delivers them with ordered, replayable
delivery frames. The bus API is designed so a Redis Streams backend can
slot in later without touching API handlers or worker message handling.
### Plan Summary
- **Task 1 – Bus contract:** `WorkerControlBus` trait,
`WorkerControlDelivery`, `WorkerControlCursor` (`Start` / `After(id)`),
bus errors.
- **Task 2 – Local backend:** `LocalWorkerControlBus` — in-memory
per-run stream, replay from `Start`, reconnect via `After(id)`, 1
024-message trim bound, cleanup on terminal runs.
- **Task 3 – Server state:** `Arc<dyn WorkerControlBus>` added to
`AppState`; `LocalWorkerControlBus` constructed at startup.
- **Task 4 – Protocol extension:** `WorkerControlMessage::RunPause` /
`RunUnpause`, `WorkerControlDeliveryFrame`, WebSocket liveness constants
(`WORKER_CONTROL_WS_PING_INTERVAL = 15s`,
`WORKER_CONTROL_WS_LIVENESS_TIMEOUT = 45s`), close-reason strings.
- **Task 5 – Worker message handler:** `apply_worker_control_message`
split out; pause/unpause routing; delivery-id dedupe
(`AppliedWorkerControlDeliveryIds`, capacity 2 048).
- **Task 6 – Worker WebSocket client:** `spawn_worker_control_manager` —
HTTP→ws/wss and Unix-socket connection, backoff 100ms→5s,
first-connection gate before `operations::start/resume`, ping/pong
watchdog, fatal loss wired back to `execute`.
- **Task 7 – Server route:** `GET /runs/{id}/worker/control-stream`,
worker-only auth via new `RequireWorkerRunScoped` extractor,
`Start`/`After` cursor dispatch, 410 on invalid cursor, server-side
ping/pong.
- **Task 8 – Stdin removal:** `RunAnswerTransport::Subprocess` renamed
to `Worker { run_id, bus }`; `pump_worker_control_jsonl` deleted; worker
launched with `stdin(Stdio::null())`; pause/unpause transport methods
added.
- **Tasks 9–10 – E2E & verification:** reconnect, invalid-cursor,
cancel-over-WebSocket, and human-interview regression tests; no Redis
dependency added.
### Key design decisions
**`RunAnswerTransport::Subprocess` → `Worker { run_id, bus }`** — all
existing transport methods (`submit`, `cancel_run`, `steer`,
`interrupt`, `pair_*`) now call `bus.publish(run_id, envelope)` instead
of writing to a channel that fed stdin. The match arms are symmetric, so
the diff is mechanical but large.
**First-connection gate** — `execute()` calls
`control_manager.wait_for_first_connection().await?` before
`operations::start` or `operations::resume`. Temporary failures spin
with backoff; a fatal invalid-cursor or request-build failure propagates
as an error before the workflow starts.
**Fatal vs. reconnectable** — HTTP 410 or a WebSocket close with reason
`"invalid_cursor"` is fatal (infrastructure failure, not user
cancellation). Any other close/error triggers the reconnect loop while
the run is non-terminal.
**`AutomationStore::load` made synchronous** — startup load now uses
`std::fs` under a `clippy::disallowed_methods` exception; async
`tokio::fs` is no longer needed for the one-shot directory scan. Invalid
automation files now fail loudly instead of being silently skipped.
**`canRetry` extended to succeeded runs** — `status.kind ===
"succeeded"` is now retryable (non-archived). Tests and API docs updated
to match.
**Default model bumps** — OpenAI default: `gpt-5.4` → `gpt-5.5`; Gemini
default: `gemini-3.1-pro-preview` → `gemini-3.5-flash`.
### Fabro Details
<details>
<summary>Ran 9 stages in 129m 19s for $58.27</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 10s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 73m 48s | $41.53 | 0 |
| simplify_opus | 22m 55s | $11.75 | 0 |
| simplify_gpt | 7m 19s | $2.74 | 0 |
| verify | 8m 51s | – | 0 |
| fixup | 10m 59s | $2.24 | 0 |
| **Total** | **129m 19s** | **$58.27** | **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>
|
||
|
|
fa565ceaae
|
chore(model): update provider default models (#437)
Updates the built-in catalog so OpenAI default selection now resolves to `gpt-5.5` and Gemini default selection resolves to `gemini-3.5-flash`. The public model defaults table and catalog assertions were updated to pin the new behavior. Verified with `cargo nextest run -p fabro-model`. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (Codex) via [Codex](https://openai.com/codex) |
||
|
|
7b7a2c9044
|
Add fabro variable CLI namespace for server-managed variables (#434)
## Summary
Exposes the existing variables API through a new `fabro variable` CLI
namespace (`list`, `get`, `set`, `rm`), following the same patterns as
`fabro secret`. Variables are intentionally readable — `list` and `get`
show stored values — while `fabro secret` remains write-only. This PR
also ships a significant set of accompanying changes: a refactored
sandbox lifecycle model in the web UI, removal of the
`fabro-devcontainer` crate, and a new `RunSandbox` OpenAPI schema that
models the full planned → initializing → ready/failed lifecycle.
## What Changed
### CLI (`fabro variable`)
- New `fabro variable` namespace with `list` (aliased `ls`), `get`,
`set`, and `rm` subcommands, dispatched through the same
`ServerTargetArgs` pattern as `fabro secret`.
- `fabro-client` gains five new wrapper methods (`list_variables`,
`get_variable`, `create_variable`, `update_variable`, `delete_variable`)
over the generated OpenAPI client.
- `set` is an upsert; `--value-stdin` accepts empty input after
newline-trimming (unlike the secrets equivalent).
- CLI reference docs (`docs/public/reference/cli.mdx`) regenerated;
`docs/public/workflows/variables.mdx` gains a short section explaining
`{{ vars.NAME }}` interpolation and the variables-vs-secrets security
boundary.
### Sandbox lifecycle model (web)
- New `RunSandbox` OpenAPI shape splits the old flat object into `kind`
(planned/initializing/ready/failed) + `plan` + optional `instance` +
optional `failure`.
- `apps/fabro-web/app/lib/run-sandbox-lifecycle.ts` centralises
lifecycle helpers (`sandboxLifecycleKind`, `sandboxInstance`,
`sandboxRuntime`, `sandboxIsReady`, `sandboxTabVisible`,
`SANDBOX_LIFECYCLE_DISPLAY`).
- Run summary panel and sandbox route now show lifecycle state
(Initializing / Failed with causes / Not created) before or instead of
the fully-loaded `SandboxDetails`.
- The sandbox details query is skipped entirely until `sandboxIsReady`
returns true, preventing unnecessary 404 fetches for planned/failed
sandboxes.
- `runHasSandbox` in `tabs-shell.tsx` delegates to `sandboxTabVisible`,
hiding the Sandbox tab for `planned` state and showing it for
`initializing`/`ready`/`failed`.
- Legacy flat sandbox shape (no `kind`) is handled via
backwards-compatible shims in the new helpers.
### `fabro-devcontainer` removal
- The `fabro-devcontainer` crate has been removed from `Cargo.lock` and
all dependent crates.
- References to devcontainer in internal plans, docs, changelog entries,
and event schemas have been cleaned up or reworded to reflect that the
feature is no longer present.
### Plan summary
- **Unit 1:** `fabro-client` variable wrappers
- **Unit 2:** CLI args, dispatch, and `commands/variable/mod.rs`
- **Unit 3:** `list`, `get`, `set`, `rm` behavior modules
- **Unit 4:** Test harness helpers and integration tests
- **Unit 5:** Regenerated CLI docs + `variables.mdx` update
### Fabro Details
<details>
<summary>Ran 8 stages in 58m 50s for $23.81</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 31m 34s | $18.44 | 0 |
| simplify_opus | 8m 48s | $2.60 | 0 |
| simplify_gpt | 4m 1s | $2.77 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **58m 50s** | **$23.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>
|
||
|
|
e18772888e
|
Model run sandbox lifecycle explicitly (#431)
## Summary Fixes sandbox state reporting by separating a requested sandbox plan from an initialized sandbox instance. Runs now project sandbox lifecycle as `planned`, `initializing`, `ready`, or `failed`, and live sandbox operations only proceed once a real instance exists. ## Changes - Introduces `RunSandboxPlan`, `RunSandboxInstance`, and lifecycle-backed `RunSandbox` domain types, with serde validation that prevents `ready` sandboxes without an instance. - Updates store projection behavior so sandbox events transition through planned, initializing, ready, and failed states while preserving requested provider/image/snapshot separately from runtime metadata. - Tightens server sandbox handlers so details/files/services/terminal/VNC helpers require an initialized instance and return a clear 404 when the sandbox was never created. - Updates the OpenAPI contract and regenerated clients so `Run.sandbox` exposes lifecycle state while `SandboxDetails.sandbox` contains only initialized instance metadata. - Updates the web UI to render lifecycle state directly from run summaries, hide the Sandbox tab for pure planned sandboxes, and disable sandbox controls until the instance is ready. - Cleans up duplicated lifecycle display/type logic and duplicate server-side sandbox instance loading found during review. | Lifecycle state | Meaning | Live controls | | --- | --- | --- | | `planned` | Sandbox was requested but no provider instance exists | Hidden/disabled | | `initializing` | Provider setup has started | State view only | | `ready` | Runtime instance exists | Enabled | | `failed` | Provider setup failed with error details | State view only | ## Testing - `cargo check --workspace` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test app/routes/run-detail.test.ts app/routes/run-sandbox.test.tsx app/components/run-summary-panel.test.tsx` - `cargo nextest run -p fabro-types --test sandbox_model_serde` - `cargo nextest run -p fabro-store run_created_projects_planned_sandbox_lifecycle sandbox_lifecycle_events_update_projected_sandbox_state run_failed_before_sandbox_events_leaves_sandbox_planned` - `cargo nextest run -p fabro-server planned_sandbox_returns_404_from_details_endpoint planned_sandbox_rejects_live_operations failed_sandbox_rejects_live_operations local_sandbox_returns_provider_neutral_details` - `cargo nextest run -p fabro-api --test run_sandbox_round_trip` - `cargo nextest run -p fabro-api --test sandbox_details_round_trip` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
e13de9faaf
|
feat(automation): persist automation refs on runs (#428)
## Summary Adds durable automation metadata to workflow runs so automation-triggered runs can carry their automation and trigger references through creation, stored events, projections, summaries, fork, retry, and API surfaces. This also introduces the new `fabro-automation` crate with typed automation IDs, TOML parsing/validation, revision hashing, and a file-backed automation store. The store avoids overwriting malformed existing TOML files on create and keeps read access from being blocked by mutation disk I/O. ## Changes - Add `AutomationRef` propagation through `RunSpec`, `run.created`, store projections, summaries, fork, retry, and related tests. - Add `fabro-automation` domain/store crate for automation TOML definitions, trigger validation, revisions, create/replace/delete, and load behavior. - Update OpenAPI and regenerated TypeScript client types for `RunSpec.automation` and `AutomationRef.trigger_id`. - Add API/type regression coverage for the new automation fields. - Harden automation store create semantics so skipped malformed files still reserve their path. ## Verification - `cargo nextest run -p fabro-automation` - `cargo +nightly-2026-04-14 clippy -p fabro-automation --all-targets -- -D warnings` - `cargo nextest run -p fabro-api` - `cargo nextest run -p fabro-types run_spec_round_trips_templated_settings run_created_props_round_trip_templated_settings` - `cd lib/packages/fabro-api-client && bun run typecheck` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` |
||
|
|
ec1b3f2084
|
feat(sandbox): secure daytona snapshot names (#429)
## Summary Secures Daytona custom snapshot creation by removing user-controlled snapshot/image references and replacing them with deterministic names Fabro computes internally. Docker image selection now uses `image.docker`, while Daytona only accepts `image.dockerfile` for custom snapshots and continues to use `daytona-medium` when no Dockerfile is configured. ## Changes - Replaces public `image.ref` config/API shape with Docker-specific `image.docker` across Rust settings, OpenAPI, generated TypeScript client, docs, defaults, examples, and web samples. - Adds Daytona snapshot identity generation using HMAC-SHA256 over a canonical manifest keyed by the Daytona API key, producing `fabro-<uuid>` snapshot names without exposing Dockerfile text or key material. - Routes Daytona custom Dockerfiles, including devcontainer-generated Dockerfiles, through the same computed identity path before calling Daytona snapshot APIs. - Updates sandbox initialization events and store projections so initialized run state can show the resolved image and computed Daytona snapshot after startup. - Updates legacy config migration behavior so Docker image refs map to `image.docker`, while Daytona legacy snapshot names are not preserved. ## Breaking Changes - `image.ref` is no longer accepted in new environment config. - Docker environments should use `image.docker` for image selection. - Daytona environments reject `image.docker`; use `image.dockerfile` to request a custom computed snapshot. ## Verification - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && bun run generate` - `cd lib/packages/fabro-api-client && bun run typecheck` - `cd apps/fabro-web && bun run typecheck` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `ulimit -n 4096 && cargo nextest run --no-fail-fast -p fabro-cli -p fabro-config -p fabro-sandbox -p fabro-workflow -p fabro-store -p fabro-server -p fabro-api` - `cargo insta pending-snapshots` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
6a94970b21
|
fix(model): retire GPT-5.2 and GPT-5.3 catalog entries (#412)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary 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) |
||
|
|
e8f0aceee8
|
refactor: rationalize server secret scopes (vault-only for optional int… (#401)
## Summary
Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.
## What changed
**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.
**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.
**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).
**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.
**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.
**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.
**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.
### Plan Summary
- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.
### Fabro Details
<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
b15d8b8476
|
feat: Add approve/deny run controls to MCP and CLI (#400)
## Summary
Exposes the existing `POST /api/v1/runs/{id}/approve` and `POST
/api/v1/runs/{id}/deny` REST endpoints through the `fabro_run_interact`
MCP tool and two new top-level CLI commands (`fabro approve`, `fabro
deny`). Workflow agents are explicitly blocked from using these actions
— approval remains a human/user operation.
## What changed
**Client & tool backend** (`fabro-client`, `fabro-tool`): Added
`approve_run` and `deny_run` to `Client` and the `FabroToolBackend`
trait, implemented in `ClientBackend`. `deny_run` passes a
`DenyRunRequest` body; absent, blank, or whitespace-only reasons are
normalised to `None`.
**`fabro_run_interact` MCP tool**: Added `Approve` and `Deny` variants
to `RunInteractAction` / `ValidatedInteractAction`, and an optional
`reason` parameter (only valid for `deny`; validated and trimmed on
input). Both actions return `{ "summary": … }` using the existing shape.
The tool description is updated to list the new actions.
**Workflow-agent guard** (`fabro-workflow`): Before dispatching
`fabro_run_interact`, the handler checks
`validated.action.requires_user()`. If the action is `approve` or
`deny`, it returns an immediate `ToolError` without ever reaching the
backend, keeping the guard explicit and independent of server auth.
**CLI** (`fabro-cli`): Extracted the archive/unarchive batch loop into a
shared `run_resolved_run_batch` helper in `commands/runs/mod.rs`, then
implemented `approval.rs` using the same helper. Both commands follow
the same batch contract as archive: attempt all runs, collect per-run
errors, exit non-zero if any fail, and emit `{ "approved"/"denied": […],
"errors": […] }` in JSON mode.
**Server auth regression** (`fabro-server`): Extended
`run_tools_worker_cannot_call_user_only_non_mcp_routes` to cover `POST
/runs/{id}/deny` alongside the existing `approve` and `timeline` checks.
**Docs** (`mcp.mdx`, `cli.mdx`): Updated the `fabro_run_interact` table
entry and added approve/deny examples, plus reference sections for the
two new CLI commands.
### Plan Summary
- Add `approve_run` / `deny_run` to `Client` and `FabroToolBackend`
- Extend `fabro_run_interact` with `approve`, `deny`, and optional
`reason`
- Block workflow-agent self-approval with an early `ToolError`
- Refactor archive batch loop into shared `run_resolved_run_batch`
helper
- Add `fabro approve` and `fabro deny` CLI commands reusing that helper
- Add integration tests for CLI commands, MCP tool, and server auth
guard
### Fabro Details
<details>
<summary>Ran 9 stages in 63m 57s for $42.33</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 28m 16s | $32.57 | 0 |
| simplify_opus | 10m 59s | $4.24 | 0 |
| simplify_gpt | 6m 13s | $3.96 | 0 |
| verify | 10m 51s | – | 0 |
| fixup | 2m 29s | $1.55 | 0 |
| **Total** | **63m 57s** | **$42.33** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
a306dac381
|
test(cli): stabilize model list snapshots
Move exact model-list table rendering coverage to a fixed mocked catalog response so real catalog metadata edits do not churn CLI snapshots. |
||
|
|
245052db38
|
feat(cli): allow rendering invalid graphs
Keep graph validation diagnostics visible, but let users opt into rendering DOT workflows that fail semantic validation with --allow-invalid. |
||
|
|
6a23014f0b
|
fix: stabilize attach JSON timing snapshot (#385)
Fixes the flaky `attach_json_errors_without_prompting_for_human_input` snapshot by moving elapsed JSON duration redaction into the shared `fabro-test` snapshot filters. `duration_ms`, `wall_time_ms`, `inference_time_ms`, `tool_time_ms`, and `active_time_ms` now use one common normalization path, and the attach, wait, and events integration snapshots use that shared helper instead of hand-rolled per-test regexes. This keeps snapshots focused on event shape and command behavior rather than exact runtime timing, while leaving exact timing relationships to direct assertions in lower-level tests. Verified with `cargo nextest run -p fabro-test`, `cargo nextest run -p fabro-cli --test it cmd::attach`, `cargo nextest run -p fabro-cli --test it cmd::wait`, `cargo nextest run -p fabro-cli --test it cmd::events`, `cargo +nightly-2026-04-14 fmt --check --all`, and `cargo +nightly-2026-04-14 clippy -p fabro-test -p fabro-cli --test it -- -D warnings`. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context unknown, reasoning effort unknown) via [Codex](https://openai.com/codex) |
||
|
|
4ad9827baf
|
Remove retired OpenAI catalog models | ||
|
|
f73f2a53f3
|
Replace queued with pending/runnable and add approval flow (web + API s… (#371)
## Summary
Replaces the single `queued` pre-execution state with explicit `pending`
and `runnable` states, and wires approve/deny actions for
parent-generated child runs that require human approval before they can
execute. This diff covers the web UI and OpenAPI spec layers of that
change.
## What changed
**Run status model**
- `queued` is removed from all TypeScript types, display maps, column
routing, and tests.
- `pending` (awaiting approval) and `runnable` (eligible for the
scheduler) replace it as distinct board columns and `RunStatus` variants
with their own labels and colors (`runnable` gets cyan; `pending` stays
muted).
**Approval actions**
- New `approveRun` / `denyRun` API calls in `run-actions.ts` invoke the
new `POST /runs/{id}/approve` and `POST /runs/{id}/deny` endpoints.
- `canApprove` predicate requires both `status.kind === "pending"` and
`lifecycle.approval?.state === "pending"` — a run whose status is
pending but has no approval record does not expose the action.
- `useApproveRun` / `useDenyRun` mutations in `mutations.ts` follow the
same pattern as `useCancelRun`.
- `ActionsMenu` in `run-detail.tsx` gains Approve (lifecycle group) and
Deny (destructive group) menu items.
**Board and event plumbing**
- `columnForStatus` now routes `pending → pending column` and `runnable
→ runnable column`; `submitted` stays in the pending column.
- `BOARD_STATUS_EVENTS` and `RUN_SUMMARY_EVENTS` replace `run.queued`
with `run.start_requested`, `run.pending`, `run.approved`, `run.denied`,
and `run.runnable`.
- The `pending` column is hidden when empty (same behaviour the old
`queued` column had).
**Waterfall phases (`run-phases.ts`)**
- `queued` phase is removed; `pending` and `runnable` phases are added
in order.
- The submitted phase closes at `run.start_requested` rather than
`run.queued`.
- Each phase derives its timestamps from its own event rather than a
single `firstTs` lookup, making multi-phase pre-execution timelines
accurate.
**OpenAPI spec**
- `POST /api/v1/runs/{id}/approve` and `POST /api/v1/runs/{id}/deny`
endpoints added with 200/404/409 responses.
- `startRun` description updated to describe the pending/runnable
branching behaviour.
- `cancelRun` description updated to reference `pending`/`runnable`
instead of `queued`.
### Plan Summary
- **Task 3** (OpenAPI schema additions for approve/deny endpoints) —
complete in this diff.
- **Task 6** (Web UI surfaces: board columns, run-detail actions,
waterfall phases, event subscriptions) — complete in this diff.
- **Task 7** (doc cleanup: references to `queued` replaced in plans,
brainstorms, and QA docs) — complete in this diff.
### Fabro Details
<details>
<summary>Ran 9 stages in 127m 37s for $104.98</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 29s | – | 0 |
| implement | 92m 10s | $91.53 | 0 |
| simplify_opus | 18m 35s | $10.65 | 0 |
| simplify_gpt | 7m 36s | $2.81 | 0 |
| verify | 3m 42s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **127m 37s** | **$104.98** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", 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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro <fabro@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
7e6052bec3
|
Surface reasoning_effort + speed in stage badge end-to-end (#363)
## Summary
The run page stage badge showed only the model name. This PR plumbs
`reasoning_effort` and `speed` from the LLM call site all the way
through the event stream, store projection, API, and UI so the badge now
renders `gpt-5.5 · high`.
### Plan Summary
- **Event props** — `AgentSessionActivatedProps` and `StagePromptProps`
gain `reasoning_effort: Option<ReasoningEffort>` and `speed:
Option<Speed>` with `serde(default, skip_serializing_if)` for
back-compat.
- **Typed projection** — `provider_used: Option<serde_json::Value>` is
replaced by `Option<StageModelUsage>`, a proper struct in `fabro-types`
with factory methods (`from_prompt_props`,
`from_agent_session_activated`). The freeform JSON bag is gone.
- **Emission sites** — `ActivationLeaseOptions` carries the new fields;
`emit_stage_prompt()` (new shared helper) resolves
`EffectiveRequestControls` via the backend and stamps them on
`Event::Prompt`. `AgentHandler` and `PromptHandler` both call this
helper instead of building the event inline.
- **ACP path** — `AgentAcpStarted` no longer writes `provider_used`; the
canonical source is the later `AgentSessionActivated` event, which is
already emitted for ACP steering sessions. Runs without a hub
legitimately leave `provider_used` unset.
- **OpenAPI** — new `StageModelUsage` and `ReasoningEffort` schemas
replace the `object | null` bag; `build.rs` maps both to the canonical
Rust types; a new `stage_model_usage_round_trip` integration test
enforces the parity requirement.
- **UI** — `extractStageModel` (event-scanning heuristic) is deleted;
replaced by `formatStageModelUsageLabel` and `stageModelUsageTitle` that
read directly off `selectedStage.providerUsed`. `parseFanInOutcome` now
sources the reducer model from `stage.prompt` instead of
`prompt.completed`.
### Key design decisions
**No type sprawl**: `fabro_model::ReasoningEffort` and `Speed` are
reused verbatim via `with_replacement` in `build.rs` — no parallel
enums.
**ACP behavior change**: previously `AgentAcpStarted` wrote a bespoke
`provider_used` blob and a later `AgentSessionActivated` would be
ignored for ACP sessions. Now `AgentSessionActivated` is the single
write path for all modes; ACP runs that never activate a steering hub
correctly leave `provider_used = null`. The integration test (`acp.rs`)
is updated to assert the new shape, and the unit test is renamed
`agent_acp_started_alone_leaves_stage_provider_used_unset` to document
intent.
**`emit_stage_prompt` helper**: both `AgentHandler` and the existing
prompt path share one function to avoid the two call sites drifting
apart again.
### Fabro Details
<details>
<summary>Ran 9 stages in 98m 44s for $65.70</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 30s | – | 0 |
| implement | 42m 19s | $26.85 | 0 |
| simplify_opus | 38m 3s | $35.17 | 0 |
| simplify_gpt | 9m 20s | $3.68 | 0 |
| verify | 3m 38s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **98m 44s** | **$65.70** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", 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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
|
||
|
|
7f84ac5e3f
|
Replace run-scoped sandbox config with named environments (#360)
## Summary
Replaces the `[run.sandbox]` configuration surface with a named,
provider-explicit environment catalog. Runs now select an environment by
slug (`[run.environment] id = "..."`) rather than configuring a sandbox
inline. Fabro resolves the catalog through normal settings precedence,
applies sparse run-level overrides, and creates a concrete sandbox from
the resolved environment.
This is a clean break — no `[run.sandbox]` compatibility layer.
### Plan Summary
- **New config shape:** Top-level `[environments.<slug>]` catalog valid
in `settings.toml`, `.fabro/project.toml`, and `workflow.toml`. Runs
reference a slug via `[run.environment] id = "..."` with optional sparse
overrides under `[run.environment.*]`.
- **Unified environment fields:** `provider`, `image` (ref +
dockerfile), `resources` (cpu/memory/disk), `network` (mode + allow
CIDRs), `lifecycle` (preserve/stop_on_terminal/auto_stop), `labels`,
`volumes`, `env` — replacing the previous split between `[run.sandbox]`,
`[run.sandbox.docker]`, `[run.sandbox.daytona]`, and
`[run.sandbox.daytona.snapshot]`.
- **OpenAPI schema update:** `RunSandboxSettings`, `DockerSettings`,
`DaytonaSettings`, and `DaytonaNetworkLayer` replaced with
`RunEnvironmentSettings`, `EnvironmentSettings`, `EnvironmentProvider`,
`EnvironmentImageSettings`, `EnvironmentResourcesSettings`,
`EnvironmentNetworkSettings`, `EnvironmentLifecycleSettings`, and
`EnvironmentVolumeSettings`.
- **CLI flag rename:** `--sandbox <provider>` → `--environment <slug>`
on `run`, `create`, `preflight`, and `server start/restart`.
- **Provider capability model:** Hard errors for security properties a
provider cannot enforce (local with blocked/CIDR networking; docker with
CIDR allow-lists). Warnings for unsupported resource limits, volumes,
labels, auto-stop, and Docker Dockerfiles.
- **Docs and internal code updated** throughout: `.fabro/project.toml`,
workflow configs, all public docs, CLI args, manifest builders, and the
runner's GitHub credentials check.
### Provider mapping
| Environment field | Local | Docker | Daytona |
|---|---|---|---|
| `image.ref` | Ignored | Docker image | Snapshot name |
| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile
(requires `image.ref`) |
| `resources.cpu/memory/disk` | Warning; ignored | cpu_quota / memory
limit / warning | Snapshot sizing |
| `network.mode = block` | **Error** | `network_mode = none` | Daytona
block |
| `network.mode = cidr_allow_list` | **Error** | **Error** | Daytona
CIDR allow-list |
| `labels` | Warning; ignored | Warning; ignored | Daytona labels |
| `volumes` | Warning; ignored | Warning; ignored | Daytona volume
mounts |
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona
auto-stop interval |
| `env` | Process env overlay | Container env | Sandbox env |
### Fabro Details
<details>
<summary>Ran 11 stages in 217m 39s for $129.86</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 4m 7s | – | 0 |
| preflight_lint | 4m 9s | – | 0 |
| fix_lints | 3m 46s | $1.06 | 0 |
| implement | 76m 6s | $57.39 | 0 |
| simplify_opus | 71m 50s | $38.17 | 0 |
| simplify_gpt | 8m 27s | $2.24 | 0 |
| verify | 6m 10s | – | 0 |
| fixup | 42m 1s | $31.00 | 0 |
| fmt | 3s | – | 0 |
| **Total** | **217m 39s** | **$129.86** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", 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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
|
||
|
|
96fff07a84
|
fix(agent): retry retryable mid-stream LLM failures
Replay retryable stream failures from the last committed turn state, clear partial visible output before retry or terminal failure, and map Anthropic stream error events into structured provider errors. |
||
|
|
0754f1ca4a
|
Add fabro_run_get read-only run inspection tool (#358)
## Summary
Adds a new `fabro_run_get` MCP tool that returns a run's summary,
resolved ID, projection, and pending questions without any mutation
capability. This separates read-only inspection from operational
control, allowing Ask Fabro sessions to inspect runs safely without
access to write operations.
## What changed and why
**New tool (`fabro-tool/src/get.rs`):** `FabroRunGetParams` /
`ValidatedRunGet` / `RunGetResult` follow the same
validation-and-dispatch pattern as other run tools. The implementation
resolves a selector, then fans out to three read-only API calls
(retrieve run, get state, list questions) and assembles them into a
single structured result.
**Tool registry and dispatch:** `FABRO_RUN_GET_TOOL_NAME` is exported
from `common.rs` and `lib.rs`, added to `TOOL_DEFINITIONS`, wired into
the MCP stdio server (`fabro-mcp-server/src/server.rs`), and dispatched
in the LLM agent executor (`fabro-workflow/src/handler/llm/api.rs`).
**Ask Fabro access policy (`fabro-server/.../sessions.rs`):** The
session now registers and allows only `fabro_run_events` +
`fabro_run_get` via the new `ASK_FABRO_RUN_TOOL_NAMES` constant.
`fabro_run_interact` is explicitly moved to the denied set, closing off
mutation from that session type. The policy match arm is refactored from
a hardcoded `|`-chain to a slice `contains` check so the constant is the
single source of truth.
**Docs:** `mcp.mdx` now lists `fabro_run_get` as the inspection tool and
redescribes `fabro_run_interact` as control-oriented.
**Backward compatibility:** `fabro_run_interact` (including `get` and
`get_questions` actions) is unchanged and still fully operational for
contexts that allow it.
### Plan Summary
- New `get.rs` module in `fabro-tool` with validation, async fetch, and
unit tests
- Constants + schema registration in `common.rs` / `lib.rs`
- MCP server and LLM dispatch branches added for
`FABRO_RUN_GET_TOOL_NAME`
- Ask Fabro session swaps `FABRO_RUN_INTERACT_TOOL_NAME` →
`FABRO_RUN_GET_TOOL_NAME` in registry and policy
- MCP integration tests: tool count constant, schema assertions, two new
end-to-end tests
(`mcp_get_resolves_selector_and_returns_summary_projection_and_questions`,
`mcp_get_rejects_blank_run_id_before_auth_or_network`)
- Public MCP docs updated
### Fabro Details
<details>
<summary>Ran 9 stages in 43m 30s for $13.42</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 2s | – | 0 |
| preflight_lint | 2m 14s | – | 0 |
| implement | 19m 43s | $8.85 | 0 |
| simplify_opus | 5m 52s | $1.45 | 0 |
| simplify_gpt | 10m 16s | $3.12 | 0 |
| verify | 2m 53s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **43m 30s** | **$13.42** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
94c657b92e
|
feat: add run.checkpoint.skip_git_hooks to bypass Git commit hooks (#355)
## Summary
Adds an opt-in `skip_git_hooks` boolean to `[run.checkpoint]` that
causes Fabro-managed run-branch checkpoint commits to pass `--no-verify`
to `git commit`, bypassing local hooks such as `pre-commit` and
`commit-msg`. Defaults to `false`. Metadata-branch snapshots and Fabro
`[[run.hooks]]` are unaffected.
```toml
[run.checkpoint]
skip_git_hooks = true
```
### Plan Summary
- `RunCheckpointSettings` (dense, in `fabro-types`) gains
`skip_git_hooks: bool` with `#[serde(default)]`.
- `RunCheckpointLayer` (sparse, in `fabro-config`) gains
`skip_git_hooks: Option<bool>` so layered config can distinguish unset
from explicit `false`.
- `RunCheckpointLayer::combine` is refactored from a wholesale-replace
to field-level merging: `exclude_globs` keeps its existing replace-wins
semantics; `skip_git_hooks` uses `.or()` (highest-priority layer that
sets it wins).
- `resolve_checkpoint` resolves `None → false`.
- `git_checkpoint` / `checked_git_checkpoint` in `sandbox_git.rs` accept
a new `skip_git_hooks: bool` and append `--no-verify` when true.
- `parallel_branch_commit_cmd` (new helper in `handler/parallel.rs`)
replaces the inline format string and accepts the same flag.
- `GitState` carries `checkpoint_skip_git_hooks`;
`RunOptions::checkpoint_skip_git_hooks()` exposes it; `execute.rs` and
`git.rs` thread it through.
- OpenAPI schema, TypeScript API client, and docs are updated.
### Key design decisions
**Field-level merging in `combine`**: the previous
`RunCheckpointLayer::combine` replaced the whole struct when
`self.exclude_globs` was non-empty. The refactor keeps that same replace
rule for `exclude_globs` while adding independent `Option::or` merging
for `skip_git_hooks`, so the two fields don't interfere.
**`--no-verify` only on run-branch commits**: the flag is injected only
in the two Git commit paths Fabro controls for run-branch checkpoints.
Metadata-branch snapshots use `git2` and never fire local hooks
regardless of this setting.
### Fabro Details
<details>
<summary>Ran 9 stages in 48m 3s for $13.91</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 3s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 15s | – | 0 |
| implement | 24m 13s | $10.38 | 0 |
| simplify_opus | 10m 31s | $1.59 | 0 |
| simplify_gpt | 5m 8s | $1.93 | 0 |
| verify | 3m 4s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **48m 3s** | **$13.91** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
511371bd9a
|
feat(mcp): support goal files in run create
Allow fabro_run_create object specs to pass goal_file, reject goal and goal_file together, and preserve file-sourced goal semantics when building run manifests. |
||
|
|
4190e13a20
|
Collapsible run stage sidebar (#352)
## What Makes the run-detail stage sidebar (shown on the Overview and Stages tabs) collapsible with a slide animation. - A toggle button slides the panel between full width (`w-56`) and an icon-only rail (`w-12`), animating `width` over 300ms with the same easing as the Ask Fabro panel. - When collapsed, **stage status icons stay visible** — green check / red X / spinning teal for running — so run progress is still scannable at a glance. Workflow links (Graph Source, Run Logs, etc.) collapse to icons too so they remain reachable. - Labels and durations become `sr-only` with `title` tooltips for hover. - The open/closed choice persists to `localStorage` (`fabro:stage-sidebar-collapsed`), carrying across the Overview and Stages tabs and reloads. ## Layout - The collapse toggle is inline with the `STAGES` heading row (or `WORKFLOW` when a run has no stages yet), so it doesn't push the stage list down. - The stage sidebar's top padding on the Stages tab was reduced (`pt-6` → `pt-3`) so the heading aligns with the adjacent content column and sits closer to the tab nav. ## Notes Self-contained in `StageSidebar` — `run-overview.tsx` and `run-stages.tsx` render it inside flex layouts that already track its width, so the slide works in both with no parent changes (aside from the padding tweak). Verified: `tsc` typecheck passes; `stage-sidebar` lib tests pass (10/10). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Co-authored-by: Fabro <noreply@fabro.sh> |
||
|
|
f5f921aa3c
|
feat: add [run.agent] fabro_tools opt-in for worker run tools (#348)
## Summary
Workflow agents previously always received the `agent:run_tools` JWT
scope and had Fabro run tools registered unconditionally. This PR makes
Fabro run tool access an explicit per-run opt-in via `[run.agent]
fabro_tools = true`, defaulting to `false`.
## What changed
**Config layer** (`fabro-types`, `fabro-config`): `RunAgentSettings`
gains a `fabro_tools: bool` field (serialized with `#[serde(default)]`
for backward compatibility). The corresponding `RunAgentLayer` gets an
optional `fabro_tools: Option<bool>` that resolves to `false` when
absent. Layer merging follows the existing `Combine` macro semantics, so
a workflow-level `false` overrides a user-level `true`.
**Server** (`fabro-server`): `worker_command` gains an
`agent_fabro_tools_enabled: bool` parameter. The server reads
`run_state.spec.settings.run.agent.fabro_tools` from the stored run
before spawning `__run-worker`, then issues the worker JWT with either
`["run:worker"]` or `["run:worker", "agent:run_tools"]` accordingly.
`WorkerScopeSet::run_worker()` loses its `#[cfg(test)]` gate so it's
available in production paths.
**CLI worker** (`fabro-cli`): `FabroRunToolServices` construction is now
gated on `run_spec.settings.run.agent.fabro_tools` rather than being
unconditional. The resolved run spec already carries the setting, so no
env-var parsing is needed in the runner.
**Tests**: The single monolithic
`worker_command_always_sets_worker_token_env` test is replaced by two
focused tests — one confirming the default scope is `["run:worker"]`
only, and one confirming the opt-in scope includes `agent:run_tools`.
Shared assertion logic is extracted into
`assert_worker_command_passes_token_only_by_env` and
`worker_token_claims` helpers. Config resolver tests cover default,
explicit true/false, and layer-override behavior.
**Docs**: `[run.agent]` description and reference tables are updated; a
new `run-configuration.mdx` section explains the opt-in semantics before
the existing `[run.agent.mcps]` section.
### Plan Summary
- 1. Add `fabro_tools` to resolved and layered run config, with resolver
tests.
- 2. Gate worker JWT scope and pass setting from stored run state into
`worker_command`.
- 3. Gate CLI `FabroRunToolServices` construction on the resolved
setting.
- 4. Update docs generator sample and public reference/execution docs.
- 5. Full verification pass (nextest, fmt, clippy).
### Fabro Details
<details>
<summary>Ran 9 stages in 54m 4s for $15.21</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 28m 5s | $10.58 | 0 |
| simplify_opus | 12m 4s | $2.86 | 0 |
| simplify_gpt | 5m 19s | $1.77 | 0 |
| verify | 3m 37s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **54m 4s** | **$15.21** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-agent <agent@fabro.local>
|
||
|
|
c6356cbd77
|
feat: improve run board, thread, and MCP create flows (#347)
## Summary This branch improves several run-management surfaces that agents and users rely on: archived runs now stay visible and ordered correctly in the board view, pair-session messages appear in the stage Thread tab, and the `fabro_run_create` MCP tool accepts the workflow-string shorthand it advertises. ## Changes - Updates the web board cache invalidation and archived-column handling so archive/unarchive actions refresh both active and archived board queries and keep archived runs in a predictable column position. - Adds pair user/system message events to stage activity parsing, Thread rendering, search, details, and DNA timeline items. - Aligns `fabro_run_create` MCP runtime deserialization and `tools/list` schema so each run entry may be either a workflow string or a full create spec object. ## Test Plan - `cargo nextest run -p fabro-tool -p fabro-mcp-server` - `cargo nextest run -p fabro-cli stdio_server_initializes_and_lists_run_tools mcp_create_string_shorthand_deserializes_before_auth mcp_create_validation_errors_happen_before_auth_or_network mcp_create_and_search_manage_real_runs_with_cli_auth` - `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) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f5ec711a2c
|
Stage-based pairing API and fabro_run_pair MCP tool (#344)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
## Summary
Run pairing previously required callers to supply an opaque
`agent_session_id` alongside a `stage_id` to start or target a pair
session. This leaked an internal runtime identifier across the public
HTTP API, generated TypeScript client, and would have bled into any MCP
tooling. This PR removes that coupling: the public pair API now
identifies targets by `StageId` alone, the server resolves the live
session internally, and a new `fabro_run_pair` MCP tool exposes the full
pair lifecycle without ever seeing session identifiers.
### What changed
**Public contract simplification** (`fabro-types`, OpenAPI, generated TS
client)
- `PairTarget` is now `{ stage_id, node_label }` — `node_id`, `visit`,
`agent_session_id`, `provider`, and `model` are removed.
- `PairStartRequest` accepts `{ stage_id }` instead of `{ target:
PairTargetSelector }`.
- `PairTargetSelector` and `PairTranscriptModel` types are deleted
entirely.
- `PairMessageRecord.target` (selector) replaced by
`PairMessageRecord.stage_id`.
- `PairTranscriptAssistantMessage.model` field removed.
- `MAX_PAIR_MESSAGE_BYTES` extracted as a public constant shared between
the server handler and the MCP tool.
**Internal session binding** (`SteeringHub`, server projection)
- `ActivePair` now carries `session_id: String` separately from the
public `PairRecord`. This preserves the stale-session protection that
previously relied on `target.agent_session_id`.
- Transcript matching changed from `(session_id AND stage_id)` to
`stage_id` within the already-scoped pair window sequence range —
simpler and sufficient.
- `active_api_targets` deactivation no longer does a per-target
`agent_session_id` check; it relies on the `active_steerable_stages`
lease already doing that guard.
**New `fabro_run_pair` MCP tool** (`fabro-mcp-server`)
- Actions: `status`, `start`, `get`, `message`, `end`, `transcript`.
- Validation happens before any network call; missing `run_id`, missing
`stage_id` for `start`, missing/invalid `pair_id` for other actions, and
overlong message text all return clean tool-level errors.
- `strum::IntoStaticStr` on `RunPairAction` enables the
`parse_pair_id_for_action` helper to embed the action name in error
messages without a `match`.
- MCP result schema and serialized results are covered by leakage
assertions confirming none of the removed fields surface.
**Tests**
- Negative leakage assertions added to pair DTO tests, event round-trip
tests, control-protocol tests, server handler tests, MCP validation
tests, and MCP schema test.
- Tool count updated from 5 → 6 in all CLI MCP integration tests.
- Steering hub test renamed:
`pair_start_rejects_non_selected_or_missing_target` →
`pair_start_rejects_missing_target` (session-mismatch rejection is now
an internal concern).
### Fabro Details
<details>
<summary>Ran 9 stages in 60m 29s for $31.78</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 26s | – | 0 |
| implement | 33m 14s | $25.51 | 0 |
| simplify_opus | 15m 7s | $4.18 | 0 |
| simplify_gpt | 3m 46s | $2.08 | 0 |
| verify | 3m 9s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **60m 29s** | **$31.78** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
54bc67017e
|
feat: Replace duration/elapsed fields with wall_time_ms and StageTiming (#343)
## Summary
Replaces the ambiguous `runtime_secs`, `elapsed_secs`, and `duration_ms`
timing fields on run/stage public API surfaces with explicit
`wall_time_ms` (elapsed clock time) and a `StageTiming` value object
that also carries `inference_time_ms`, `tool_time_ms`, and
`active_time_ms`.
This is a greenfield breaking change — no compatibility shims are
preserved.
### What changed
**API shape**
- `RunBillingStage.runtime_secs` → `RunBillingStage.timing: StageTiming`
- `RunBillingTotals.runtime_secs` → `RunBillingTotals.timing:
StageTiming`
- `RunSummary.timestamps.duration_ms` / `elapsed_secs` removed; a
top-level `timing: StageTiming | null` field added
- Stage list item `duration_secs` → `wall_time_ms`
**Web app (`apps/fabro-web`)**
- `run-billing.tsx`: `liveRuntimeSecs` → `liveWallTimeMs`; live ticking
now returns milliseconds and the footer total sums `wallTimeMs` across
rows
- `stage-sidebar.ts`: `duration_secs` → `wall_time_ms` for the per-stage
duration display
- `runs.ts`: `elapsed_secs` lookup replaced with `timing.wall_time_ms`
- `formatElapsedSecs` / `formatDurationSecs` call sites replaced with
`formatDurationMs`
**Lockfile / tooling**
- `@openapitools/openapi-generator-cli@2.20.2` added as a dev dependency
to `@qltysh/fabro-api-client` to support regenerating the TypeScript
client after schema edits; several transitive deps pulled in alongside
it.
### Design notes
- **Units are now consistent**: every timing value on run/stage surfaces
is in milliseconds; the old API mixed seconds (`runtime_secs`,
`elapsed_secs`) with milliseconds (`duration_ms`).
- **Live ticking** still works correctly: the in-flight billing row
computes `now - startedAt` in ms and sums across rows for the footer,
avoiding a server round-trip during a running stage.
- **`StageTiming.active_time_ms = inference_time_ms + tool_time_ms`** —
parallel work is summed, so run active time can exceed wall time.
- Subsystem-internal `duration_ms` fields (sandbox setup, devcontainer
lifecycle, hooks) are intentionally left unchanged; only public
run/stage timing surfaces are affected.
### Fabro Details
<details>
<summary>Ran 9 stages in 115m 53s for $108.50</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 80m 53s | $101.97 | 0 |
| simplify_opus | 21m 35s | $4.09 | 0 |
| simplify_gpt | 5m 1s | $2.44 | 0 |
| verify | 3m 11s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **115m 53s** | **$108.50** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
37d6b3dbcd
|
fix(server): count whole storage tree in Fabro-managed bytes
build_disk_usage_response only summed scratch/ run dirs and logs/*.log, omitting objects/ (SlateDB + artifacts), sessions/, and vaults/ — a ~30x undercount of "Fabro managed" storage on the resources page. Measure the whole storage_dir tree for total_size_bytes so it can't drift as new subdirectories are added. Reclaimable stays a curated estimate that matches what `fabro system prune` actually frees. A residual "other" summary row keeps `fabro system df` totals consistent and surfaces as a "Database & artifacts" table row. Also add a KiB tier to formatBytesAsMemory so small storage values render human-readably instead of raw byte counts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
86b1fbef7f
|
feat(api): bind sessions to runs (#336)
## Summary
Ask Fabro sessions are now run-bound instead of standalone. Sessions are
created under their owning run, then accessed by flat session ID routes,
with durable state projected from the run event stream rather than a
separate session store.
## Changes
- Move session creation/listing to `POST/GET /api/v1/runs/{id}/sessions`
while keeping flat session reads, turns, interrupts, and event streams
under `/api/v1/sessions/{id}/...`.
- Add typed `run.session.*` events, ULID-backed session/turn IDs,
read-only default permissions, and a rebuildable SlateDB `session_id ->
run_id` index.
- Remove the old file-backed session store and wire the server, runtime,
Rust client, generated API crates, and TypeScript client around run
event projections.
- Replace the old top-level CLI session command with `fabro run ask` for
chatting with a run.
- Regenerate the TypeScript API client; this also catches up existing
generated models for Pair/run event detail schemas already present in
the OpenAPI spec.
## Validation
- `cargo build -p fabro-api -p fabro-client -p fabro-server -p
fabro-cli`
- `cargo nextest run -p fabro-server --features test-support -E
'test(run_bound_session_is_created_as_run_event_and_resolves_by_flat_id)
| test(sessions_are_listed_only_under_their_owning_run)'`
- `cargo nextest run -p fabro-store
projection_rebuilds_runtime_context_from_run_events`
- `cargo +nightly-2026-04-14 clippy -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-cli --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd lib/packages/fabro-api-client && bun run typecheck && 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)
|
||
|
|
02fabb7b47
|
fix(workflow): capture configured artifacts once (#337)
## Summary Fixes artifact promotion for configured `[run.artifacts].include` globs by collecting matching files after each stage regardless of mtime and surfacing failed discovery commands as collection failures. The workflow lifecycle now keeps a per-run ledger keyed by `(path, content_sha256)`, rebuilt from existing `artifact.captured` events, so unchanged files are persisted and emitted once while changed content at the same path can still be captured again. Fixes fabro-sh/fabro#335. ## Tests - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-workflow artifact_snapshot` - `cargo nextest run -p fabro-cli unchanged_matching_artifact_is_captured_once_across_stages` - `cargo nextest run -p fabro-cli acp_artifacts_are_listed_when_touched_file_mtime_precedes_attempt_start` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com> |
||
|
|
9837a08929
|
refactor(install): share persistence pipeline (#332)
## Summary Unifies installer persistence so CLI and web install paths share the same file/env/vault primitives, while preserving the CLI's server-API secret persistence and auth bootstrap behavior. ## What Changed - Added shared `fabro-install` config writers for installer-owned tagged enum tables, replacing `server.listen` and `cli.target` atomically so stale variant fields cannot survive. - Added `InstallPersistencePlan` for disk-backed settings, server env, and vault writes/removals with the existing rollback semantics for settings and vault failures. - Refactored `fabro install`, `fabro install github`, and `/install/finish` to use the shared persistence plan where their disk behavior overlaps. - Preserved full-install ordering: settings/env first, workflow-visible secrets through the server API second, and CLI `auth.json` only after API secret persistence succeeds. - Preserved web installer failure response fields for leftover and removed env keys, plus the post-success finish hook/shutdown behavior. ## Test Plan - `cargo nextest run -p fabro-install` - `cargo nextest run -p fabro-cli commands::install::tests` - `cargo nextest run -p fabro-cli --test it cmd::install` - `cargo nextest run -p fabro-server --features test-support --test it api::install` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
15ff38fa53
|
feat(template): resolve template error locations (#333)
## Summary Fixes `fabro-sh/fabro#330` by making template partials that reference missing inputs validate structurally with a warning instead of failing the validate command. The template crate now owns MiniJinja semantic error classification and source-location mapping, so workflow diagnostics can consume already-resolved template locations instead of remapping fragment spans itself. ## What Changed - Added `TemplateErrorLocation` and `TemplateSourceOrigin` APIs to report source name, line, column, and span from `fabro-template`. - Classified wrapped MiniJinja errors by their deepest semantic cause, preserving the original source chain for renderer context. - Added fragment-origin rendering paths so attribute fragments embedded in full workflow source report locations in the original source text. - Removed workflow-side source span remapping from template diagnostics; workflow now only adds owner, node/edge, severity, rule, and fix context. - Added regression coverage for include/import/from/extends undefined variables and the CLI `fabro validate` partial fixture. ## Test Plan - `cargo nextest run -p fabro-template` - `cargo nextest run -p fabro-workflow transforms::variable_expansion transforms::file_inlining` - `cargo nextest run -p fabro-cli --test it cmd::validate` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-template -p fabro-workflow -p fabro-cli --all-targets -- -D warnings` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context not reported, reasoning not reported) via [Codex](https://openai.com/codex) |
||
|
|
32015b2226
|
fix(graph): support dotted Fabro graph attributes (#324)
## Summary Graph rendering now accepts documented Fabro dotted DOT attributes end to end while keeping raw Graphviz calls behind `fabro-graphviz`. The new `RenderableDot` boundary applies Fabro render styling and normalization before raw SVG rendering, and both the CLI subprocess and server path now route through that typed boundary instead of calling `graphviz_sys` directly outside the graphviz crate. The branch also adds a small curated DOT compatibility corpus covering ACP agent attributes, human default choices, and subworkflow manager attributes. Those fixtures are exercised by both render and validation tests, and the run overview now shows graph render errors directly instead of falling through to the empty graph state. ## Verification - `cargo nextest run -p fabro-graphviz` - `cargo nextest run -p fabro-validate` - `cargo nextest run -p fabro-cli render_graph` - `cargo nextest run -p fabro-server render_graph_from_manifest_accepts_fabro_dotted_attributes` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-graphviz -p fabro-cli -p fabro-server -p fabro-validate --all-targets -- -D warnings` - `rg -n "graphviz_sys" lib/crates/fabro-cli lib/crates/fabro-server` - `cd apps/fabro-web && bun test` - `cd apps/fabro-web && bun run typecheck` --- [](https://github.com/EveryInc/compound-engineering-plugin) Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com> |
||
|
|
21c408d647
|
fix(workflow): allow workflow-root template partials (#322)
## Summary - Add a CLI validation regression for workflow-root prompt partials included from nested prompt files. - Allow bundled prompt templates to resolve sibling partials from the workflow root instead of jailing each prompt file to its own directory. - Refactor include handling so manifest discovery and runtime rendering share rooted template sources, include normalization, root containment checks, and FileResolver-backed TemplateStore loading. ## Testing - `cargo nextest run -p fabro-template` - `cargo nextest run -p fabro-manifest` - `cargo nextest run -p fabro-workflow` - `cargo nextest run -p fabro-cli cmd::validate` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-template -p fabro-manifest -p fabro-workflow -p fabro-cli --all-targets -- -D warnings` --------- Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com> |
||
|
|
ef70dbc5be
|
fix(cli): print full run id on rm (#315)
When `fabro rm` succeeds, the confirmation output should identify exactly which run was removed. Today the human-readable path prints a shortened run ID, which is less precise than the JSON output and less useful for copy/paste confirmation. ## Summary - print the full run ID after successful `fabro rm` removal - keep `--json` behavior unchanged - update CLI snapshots to expect full IDs on success paths ## Testing - cargo test -p fabro-cli rm_ -- --nocapture - cargo +nightly-2026-04-14 fmt --check --all |
||
|
|
29b7cc0de0
|
feat(workflow): enforce strict api/acp backends (#307)
## Summary
This PR makes agent execution a strict two-backend contract: API-backed
stages use Fabro-owned model/provider auth, while ACP-backed stages
launch a user-supplied stdio process that owns its own auth and tools.
That removes the legacy CLI backend and prevents ACP execution from
accidentally resolving or forwarding provider credentials.
## Changes
- Replaces the old `api`/`cli`/`acp` backend model with `AgentBackend {
api, acp }`, with `backend=\"cli\"` rejected and migrated toward
explicit ACP process configuration.
- Splits ACP process configuration into `acp.command` for shell command
strings and `acp.config` for JSON stdio configs, while rejecting legacy
`acp_command`.
- Restricts ACP to `agent` nodes and rejects API-only attributes such as
`model`, `provider`, `reasoning_effort`, `max_tokens`, and `speed` on
ACP nodes.
- Deletes the workflow CLI runtime, CLI credential resolver surface, CLI
live smoke tests, and `agent.cli.*` event handling.
- Updates ACP events and projections to report process identity
(`command`, optional `config_name`) rather than provider/model metadata.
- Updates import/stylesheet propagation, CLI workflow smoke coverage,
server steering tests, and web model extraction for the new
event/backend contract.
## Validation
- `cargo check -p fabro-auth -p fabro-acp -p fabro-workflow -p fabro-cli
--all-targets`
- `cargo nextest run -p fabro-auth -p fabro-acp -p fabro-validate -p
fabro-store -p fabro-workflow --lib`
- `cargo nextest run -p fabro-acp`
- `cargo nextest run -p fabro-cli --test it
workflow::acp::acp_backend_workflow`
- `cargo nextest run -p fabro-workflow --test it
codergen_without_backend_simulated`
- `cargo nextest run -p fabro-workflow --test it
import_e2e_through_engine`
- `cargo nextest run -p fabro-workflow --test it stylesheet_application`
- `cargo nextest run -p fabro-server
steer_with_active_acp_stage_returns_non_steerable_conflict`
- `cargo nextest run -p fabro-server
active_acp_stage_marker_clears_on_terminal_paths`
- `cargo nextest run -p fabro-types
agent_backend_accepts_only_api_and_acp`
- `cd apps/fabro-web && bun test app/routes/run-stages.test.ts`
- `cd apps/fabro-web && bun run typecheck`
- `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: Peter Bell <4843+PeterBell@users.noreply.github.com>
|
||
|
|
cd74013d06
|
refactor(auth): split credential sources and vault schemas (#306)
## Summary Compared with `origin/main`, this PR splits credential storage and credential references into explicit types. Vault secrets now distinguish `token`, `oauth`, and `file` payloads, while runtime/model configuration points to credentials through explicit `env:<NAME>` and `vault:<NAME>` source refs. ## Changes - Replaces the old `environment`/`credential` secret schema vocabulary with `token`/`oauth`/`file` across OpenAPI, Rust API tests, generated TypeScript models, CLI/docs references, and the changelog. - Updates auth resolution, refresh, provider strategies, workflow LLM handling, server diagnostics, install flows, run manifests, and secret handlers to consume typed vault entries and explicit credential sources. - Updates provider catalog TOMLs and config parsing so provider auth and extra headers use `vault` refs instead of ambiguous `credential` refs. - Updates CLI install/login/run/secret paths and integration tests to write and read the new credential shapes. - Removes the temporary legacy vault migration and empty-vault fallback, then centralizes provider vault secret-name lookup and Codex API credential shaping. ## Verification - `cargo +nightly-2026-04-14 fmt --all` - `cargo +nightly-2026-04-14 clippy -p fabro-auth -p fabro-model -p fabro-config -p fabro-vault -p fabro-server -p fabro-cli --all-targets -- -D warnings` - `ulimit -n 4096 && cargo nextest run -p fabro-auth -p fabro-model -p fabro-config -p fabro-vault -p fabro-server -p fabro-cli` (`1938` passed, `35` skipped) |
||
|
|
6a86ced77c
|
fix(server): persist manifest metadata names (#302)
When Fabro creates a detached run through the server, the resulting run metadata should still read like something a human can trust at a glance. Before this change, those runs could persist with `settings.project.name` and `settings.workflow.name` left `null` even though Fabro already had enough local context to infer them. That made `inspect` output look half-populated and made it harder to tell whether the saved run state was complete. This fixes that trust gap in the server-backed manifest flow. ## Summary - backfill missing manifest-backed project and workflow names during server run preparation - prefer explicit `[workflow].name` from bundled `workflow.toml`, then fall back to graph name or workflow slug - cover both manifest preparation and persisted run-state behavior with server tests ## Testing - cargo test -p fabro-server prepare_manifest_backfills_missing_project_and_workflow_names -- --nocapture - cargo test -p fabro-server prepare_manifest_preserves_explicit_project_and_workflow_names -- --nocapture - cargo test -p fabro-server create_run_persists_backfilled_project_and_workflow_names -- --nocapture --------- Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
492aba7fff
|
fix: MiniJinja can't find partials (#301)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Fixes an issue where use of MiniJinja [`include`](https://jinja.palletsprojects.com/en/stable/templates/#include) control structure (`{% include "filename.ext" %}`) causes a render error `template not found: tried to include non-existing template "filename.ext"` ### Example broken diagram ``` dot digraph ValidatePlan { start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] test_inline_prompt [label="moo" prompt="{% include 'test.tpl.md' %}"] // ^^^^^^^^^^^^^^^^^^^^^^^^^ start -> test_inline_prompt -> exit } ``` ### Fix The core issue was that template rendering knew the source name for diagnostics, but did not have a loader rooted at the prompt/goal file location. Includes therefore failed even when the included file existed next to the rendered file. The fix adds optional loader support to `fabro-template`, then wires workflow rendering to the existing `FileResolver` so includes resolve relative to the file currently being rendered. For `fabro validate`, there was a second manifest-specific problem: validation runs through a bundled manifest, and the manifest builder only bundled explicit `prompt.md` / `goal.md` files, not static MiniJinja `include` dependencies inside those files. The manifest builder now scans prompt/goal template text for literal `{% include "file" %}` / `{% include 'file' %}` references and bundles those files too. Missing or unsafe include names are left for MiniJinja/runtime validation rather than expanding scope. (For clarity: The fix does not support variables or arrays in `include`.) |
||
|
|
302e2445b4
|
refactor(model): move provider facts into catalog (#298)
## Summary Moves provider-specific facts out of `AdapterKind` metadata and into provider catalog data, leaving adapters responsible for runtime protocol behavior. This makes providers that share an adapter mostly TOML-driven while still surfacing adapter construction failures during readiness checks. ## What Changed - Provider TOML now owns auth mode, API-key/header policy, billing policy, agent profile, base URLs/env overrides, extra headers, and probe markers. - Auth, install, config, diagnostics, and server flows resolve provider credentials from catalog auth config, including API-key, header-only, and no-auth providers. - LLM client registration now reports adapter construction failures, validates final adapter requests before HTTP dispatch, and preserves custom primary auth headers. - Billing and docs now use provider-owned billing policy instead of adapter metadata, and the old adapter metadata surface is removed. ## Reviewer Notes OpenAI-compatible `base_url` validation now happens during adapter/client registration rather than catalog build. That keeps catalog parsing adapter-agnostic while still letting readiness and model listing reflect providers that cannot register. ## Verification - `cargo check -p fabro-model -p fabro-auth -p fabro-llm -p fabro-server -p fabro-cli` - `cargo nextest run -p fabro-llm -- adapter_registry` - `cargo nextest run -p fabro-model -- catalog` - `cargo nextest run -p fabro-auth -- api_key` - `cargo nextest run -p fabro-server -- install` - `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) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
73ebb7d28b
|
feat(mcp): support run parent relationships (#295)
## Summary - Add parent metadata (`parent_id`, `children_count`) to Fabro MCP run summaries, search summaries, and created-run results. - Allow MCP clients to create child runs, search direct children, and link or unlink an existing run's parent through the existing run tools. - Update MCP docs and tool descriptions for the parent-aware create/search/interact behavior. ## Test Plan - [x] `cargo +nightly-2026-04-14 fmt --check --all` - [x] `cargo nextest run -p fabro-mcp-server` - [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8d1b5f6c23
|
feat(mcp): add interrupt action to fabro_run_interact (#296)
## Summary
- Adds a standalone `interrupt` action to the `fabro_run_interact` MCP
tool, closing a parity gap with the HTTP API (`POST
/api/v1/runs/{id}/interrupt`).
- Lets MCP callers cancel an API-mode agent's current LLM round and park
it in `SteeringHub`'s `waiting_for_steer` state without committing to
follow-up text in the same call.
- Dispatches through the existing `Client::interrupt_run`; no client or
server-side changes.
## Why not just use `message` with `interrupt: true`?
Combined interrupt+steer remains the right choice when you want to
redirect the agent. Bare interrupt is for "pause the agent while I
decide what to say next." The variant's doc comment steers callers
toward `message` or `cancel` as the usual options, since a bare
interrupt with no follow-up leaves the run idle indefinitely.
## Test plan
- [x] `cargo nextest run -p fabro-mcp-server` — 13/13 pass, including
new `interrupt_action_requires_only_run_id` unit test
- [x] `cargo nextest run -p fabro-cli -E 'test(/mcp_/)'` — 27/27 pass,
including extended
`mcp_interact_actions_resolve_selector_and_call_expected_endpoints` E2E
(mocks `POST /runs/{id}/interrupt`, asserts the tool hits it)
- [x] `cargo +nightly-2026-04-14 clippy -p fabro-mcp-server -p fabro-cli
--all-targets -- -D warnings` — clean
- [x] `cargo +nightly-2026-04-14 fmt --check` on touched crates — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2ba04be181
|
feat(template): add source-aware diagnostics (#292)
## 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> |
||
|
|
2b53917759
|
fix(validate): treat undefined template vars in @file prompts as warnings (#290)
## Summary
`fabro validate` had inconsistent behavior for undefined template
variables depending on whether the prompt was inline or loaded via an
`@file` reference. Inline `{{ inputs.foo }}` produced a warning and
validation passed; the same expression inside a `@file`-imported prompt
produced a hard validation error.
Fixes #286.
## Root cause
Two template-rendering passes with different strictness, applied to
disjoint inputs:
1. **DOT-source pass**
(`lib/crates/fabro-workflow/src/operations/create.rs`) honored
`RenderMode::Structural` for `fabro validate` — undefined variables
downgraded to a `Severity::Warning` diagnostic, then lenient render
finished the job.
2. **Per-attribute pass**
(`lib/crates/fabro-workflow/src/transforms/variable_expansion.rs`)
inside `TemplateTransform` was always strict and had no `RenderMode`
awareness. Because `FileInliningTransform` runs *before*
`TemplateTransform`, expressions inside `@file` content only ever
encountered the strict pass.
## Fix
- Plumb `RenderMode` through `TransformOptions` into
`TemplateTransform`.
- In `RenderMode::Structural`, the transform catches
`TemplateError::UndefinedVariable` per attribute, emits a warning
diagnostic, and falls back to `render_lenient`.
- Diagnostics flow through a new `Transformed.diagnostics` field into
`Validated` alongside lint output.
- Diagnostics now include `node_id` when the undefined variable was
found inside a node attribute, which is more useful than the previous
"at line 1" location.
- `RenderMode` and the shared `template_undefined_variable_diagnostic`
helper moved to `pipeline/types.rs` so the transform layer can reach
them without a circular dep.
Strict mode (`fabro run`, preflight) is unchanged — undefined inputs
still hard-fail before a run is created.
## Behavior
Illustrative output shapes (variable names and line numbers depend on
the fixture):
Inline prompt (unchanged):
```
warning: undefined template variable `inputs.<name>` at line <n> (template_undefined_variable)
Validation: OK
```
`@file`-imported prompt (previously a hard error, now matches inline —
node-attributed instead of line-attributed):
```
warning [node: <id>]: undefined template variable `inputs.<name>` in node `<id>` (template_undefined_variable)
Validation: OK
```
## Test plan
- [x] `cargo nextest run --workspace` — 5773/5773 passing
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings` clean
- [x] `cargo +nightly-2026-04-14 fmt --check --all` clean
- [x] New regression test
`bare_fabro_with_unbound_inputs_in_imported_prompt_validates_structurally_with_warning`
in `lib/crates/fabro-cli/tests/it/cmd/validate.rs` against new fixture
`test/templated_unbound_imported/`
- [x] Existing
`bare_fabro_with_unbound_inputs_validates_structurally_with_warning` and
`strict_render_hard_fails_on_unbound_inputs` still pass — verifies
inline structural and run-start strict behavior are both preserved
- [x] Manual reproduction of the exact inputs from the issue now
succeeds with a warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
56c7f627c4
|
feat(session): add server-backed agent sessions (#278)
## Summary
Adds the first server-backed Fabro agent session slice: persistent
session records, durable turn/event storage, HTTP session APIs, SSE turn
streaming, generated clients, and a new `fabro session -p <prompt>` CLI
path.
## What Changed
- Adds shared session IDs, records, statuses, event envelopes, and
message DTOs in `fabro-types`, with OpenAPI replacements in `fabro-api`.
- Renames the agent runtime transcript item from `Turn` to `Message` and
adds conversion between runtime history and persisted `SessionMessage`
records.
- Introduces a file-backed `SessionStore` for session metadata, turns,
full transcripts, and append-only events under local storage.
- Wires server session routes for create/list/read/update/delete, turn
submission, event replay, interrupt requests, and session-scoped tools.
- Implements streamed turn execution with durable events persisted
before SSE broadcast, active-turn conflict handling, local same-machine
`working_dir` validation, and noninteractive permission denials.
- Adds `fabro-client` helpers and the `fabro session -p` command, plus
regenerated TypeScript API client files.
## Notes
V1 intentionally keeps session execution local to same-machine server
targets. Remote clone-backed session sandboxes, interactive REPL/TUI
behavior, warm session pooling, and real tool discovery for
`/sessions/{id}/tools` remain follow-up work.
## Verification
- `cargo build --workspace`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-store session_store_contract_tests --lib`
- `cargo test -p fabro-agent
history::tests::session_message_roundtrip_preserves_runtime_history
--lib`
- `cargo test -p fabro-server 'session_' --lib`
- `cargo test -p fabro-server --features test-support --test it
openapi_conformance -- --nocapture`
- `cargo test -p fabro-cli --test it cmd::session:: -- --nocapture`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
|
||
|
|
0f1cf4da5c
|
feat(cli): wire run parent commands (#288)
## Summary Add CLI support for run parent relationships now that the server API can store them. This lets users create child runs, filter children, inspect parent metadata, and link or unlink parents without dropping to raw API calls. ## What Changed - Added top-level `fabro parent link` and `fabro parent unlink` commands with selector resolution, text output, and JSON summaries. - Added `--parent` to `fabro run`, `fabro create`, and `fabro ps`; create/run send `parent_id` in manifests and `ps` uses server-side parent filtering. - Surfaced `parent_id` in `ps --json` and `inspect`, with a conditional `PARENT` column for unfiltered tables. - Extended `fabro-client` parent-link APIs and `list_store_runs(parent_id)`. ## Test Plan - `cargo nextest run -p fabro-cli` - `cargo nextest run -p fabro-client` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-client --all-targets -- -D warnings` - `cargo insta pending-snapshots` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
87950295bd
|
refactor(llm): split provider identity from adapters (#280)
## Summary This PR separates provider identity from adapter behavior across the LLM stack. Provider IDs now represent catalog rows and provider metadata, while adapter/profile routing owns protocol behavior for Anthropic, OpenAI, Gemini, and OpenAI-compatible providers. ## Changes - Replace the shared `fabro_model::Provider` enum with open-ended `ProviderId` catalog identity and typed `AdapterKind` metadata. - Route auth, CLI, ACP, workflow, memory selection, profile construction, and LLM client registration through catalog provider rows instead of provider-ID fallbacks. - Move API-key URL/header/env metadata into provider catalog/auth flows and require configured provider rows for credential-backed clients. - Simplify billing to `algorithm`-tagged OpenAI, Anthropic, and Gemini shapes; OpenAI-compatible adapters bill through the OpenAI algorithm. - Remove greenfield compatibility paths for old provider aliases, legacy provider-tagged billing JSON, and the `openai_compatible` pseudo-provider env fallback. - Update fixtures and tests to exercise catalog-driven Kimi/Zai/Minimax/Inception/custom OpenAI-compatible routing. ## Validation - `cargo test --no-run -p fabro-model -p fabro-auth -p fabro-agent -p fabro-workflow -p fabro-server -p fabro-llm -p fabro-api -p fabro-cli -p fabro-store -p fabro-static` - `cargo nextest run -p fabro-model -p fabro-auth -p fabro-agent -p fabro-workflow -p fabro-server --no-fail-fast` - `cargo nextest run -p fabro-llm -p fabro-api -p fabro-cli -p fabro-store -p fabro-static --no-fail-fast` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `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) |
||
|
|
d09e6cde33
|
feat(pr): support GitHub pull request associations (#270)
## Summary
Adds event-sourced pull request association management for runs while
preserving Fabro-created PR creation. A run can now store a current
GitHub PR association, replace it by linking another GitHub PR URL, and
remove it through an unlink event.
## What Changed
- Added `pull_request.linked` and `pull_request.unlinked` events,
projection replay support, and optional PR metadata fields in shared
pull request records.
- Added API, server, and client support for `PUT
/runs/{id}/pull_request` and `DELETE /runs/{id}/pull_request`; linking
accepts GitHub PR URLs, infers owner/repo/number, and captures live
GitHub title and branch metadata when available.
- Added `fabro pr link` and `fabro pr unlink`, updated `fabro pr view`,
and kept create/merge/close behavior guarded to GitHub PRs with usable
coordinates.
- Updated web UI rendering and internal event docs so stored PR links
display cleanly when live GitHub details are unavailable.
## Testing
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-types -p fabro-store -p fabro-server -p
fabro-cli`
- `bun run typecheck` in `lib/packages/fabro-api-client`
- `bun run typecheck` in `apps/fabro-web`
- `bun test` in `apps/fabro-web`
Refs https://github.com/fabro-sh/fabro/issues/235
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
|
||
|
|
b0847180b6
|
Fix custom provider resolution in exec and model list (#276)
## Summary - allow `fabro exec` to use configured custom provider IDs from the resolved LLM catalog - route direct exec sessions through the same catalog-aware provider/profile resolution used by workflow runs - stop `fabro-client::list_models` from rejecting non-built-in provider filters client-side - update CLI snapshots and add regression tests for custom-provider exec and model listing ## Repro With a configured provider like: ```toml [llm.providers.bedrock] adapter = "openai_compatible" base_url = "https://.../v1" [cli.exec.model] provider = "bedrock" name = "bedrock-claude-sonnet-4-6" ``` these paths diverged: - `fabro run ... --model bedrock-claude-sonnet-4-6` worked - `fabro model list` showed `bedrock-*` models - `fabro exec "..."` failed with `unknown provider: bedrock` - `fabro model test --provider bedrock` failed with the same client-side error ## Root cause There were two separate built-in-only assumptions: 1. `fabro-agent` direct CLI paths parsed provider strings into the built-in `Provider` enum and built a default catalog, so configured provider IDs from `settings.toml` were invisible. 2. `fabro-client::list_models()` parsed the optional provider filter into the same built-in enum before calling the server, so custom provider filters never reached the API. ## Validation - `cargo check -p fabro-cli -p fabro-agent -p fabro-client` - `cargo test -p fabro-agent resolve_provider_accepts_custom_catalog_provider -- --nocapture` - `cargo test -p fabro-client list_models_allows_custom_provider_filters -- --nocapture` - `cargo test -p fabro-cli exec_accepts_configured_custom_provider_from_settings -- --nocapture` - `cargo test -p fabro-cli list_invalid_provider_errors -- --nocapture` - `cargo test -p fabro-cli help -- --nocapture` --------- Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
9768651b52
|
feat(model): add opt-in Ollama catalog provider (#268)
## Summary Adds Ollama as a disabled-by-default built-in catalog provider backed entirely by provider TOML. Enabling `[llm.providers.ollama] enabled = true` exposes the bundled `qwen3-coder` sample model through the existing OpenAI-compatible adapter, while other local Ollama models still require explicit model blocks until fabro-sh/fabro#267 adds discovery. The docs now show the opt-in setting and note that local users can set `OLLAMA_API_KEY=ollama` for Ollama's OpenAI-compatible endpoint. ## Verification - `cargo nextest run -p fabro-model` - `cargo nextest run -p fabro-cli cmd::model` - `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) --------- Co-authored-by: roALAB1 <233429779+roALAB1@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |