Commit graph

678 commits

Author SHA1 Message Date
Bryan Helmkamp
bb77806900
Sync Cargo.lock for fabro-mcp-store 2026-06-25 16:43:26 -04:00
Scott Werner
4b7c2690c8
feat(mcp): add server-side MCP server store (fabro-mcp-store) (#521)
## What

Adds the storage foundation for server-managed MCP servers: a durable
store plus its domain model. No server wiring, HTTP API, or UI yet —
this is standalone scaffolding that later PRs build on.

- New **`fabro-mcp-store`** crate: a concrete, filesystem-backed
`McpServerStore` — one TOML file per definition under
`{active-config-dir}/mcps/`, an in-memory cache, and a SHA-256
content-hash revision for optimistic concurrency. Modeled directly on
`AutomationStore`. Includes an id-only `ids()` accessor for cheap
listing that avoids cloning the (potentially sensitive) env/header maps
a full definition carries.
- New **`McpServerDefinition` / `McpServerDraft` / `McpServerReplace`**
domain model (plus `McpServerId` / `McpServerRevision` and structural
validation) in `fabro-types`, reusing the existing `McpTransport`. These
stay persistence-independent; the on-disk TOML DTO and the filesystem
plumbing live in `fabro-mcp-store`.

Nothing in the workspace depends on the new crate yet. Wiring
`McpServerStore` into the server, the HTTP API, and the UI are follow-up
PRs.

## Testing

- `fabro-mcp-store`: 7/7 (empty/missing dir, non-TOML ignored,
malformed/invalid-filename fail load, CRUD round-trip, stale-revision
and duplicate-create rejected).
- `fabro-types`: `mcp_store` validation and round-trip tests pass.
`cargo build --workspace`, fmt, and clippy all green.

## Notes

- The domain model derives `PartialEq` but not `Eq` because
`McpTransport` carries `HashMap`s (differs from `Automation*`, matches
the transport's capabilities).
- Validation is structural for now (id format, non-empty name,
well-formed transport); credential-literal validation is deliberately
deferred to the API layer (flagged TODO).
- The store is concrete by design (no trait): a future move off per-file
TOML is a one-time migration, not a runtime backend choice. The revision
is currently derived from the canonical TOML bytes — the one
storage-coupled detail to revisit if that move happens.
- Part of a short series adding server-managed MCP servers; independent
of the sibling PRs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:50:02 -04:00
fabro-releases[bot]
ba56a170d8 Bump version to 0.275.0-nightly.0
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
2026-06-25 12:49:18 +00:00
fabro-releases[bot]
1626240220 Bump version to 0.267.0-nightly.0
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
2026-06-17 16:50:57 +00:00
Ryan Neal
d5b2220ed3
feat(llm): Amazon Bedrock provider — Converse codec, SigV4 + API-key auth (#459)
Adds **Amazon Bedrock** as an opt-in built-in provider, over Bedrock's
unified **Converse / ConverseStream** API. One codec serves every
Converse-capable family — Claude, Amazon Nova, Meta Llama, Mistral,
DeepSeek, Moonshot Kimi, Z.AI GLM, MiniMax, NVIDIA Nemotron, and OpenAI
gpt-oss — because AWS translates the envelope to each model's native
dialect server-side. Auth is either **AWS SigV4** (the default
credential chain — env / profile / IMDS / IRSA / SSO, resolved per
request so sessions refresh) or a **Bedrock API key**
(`AWS_BEARER_TOKEN_BEDROCK`, bearer). Disabled by default (the Ollama /
OpenRouter opt-in pattern).

This is the redo of #459's original Claude-only `InvokeModel` adapter,
rebuilt on the gateway-refactor seams (#481–#497). @depopry's SigV4
signer, AWS event-stream frame decoder, `BedrockAuth`, the `aws_sigv4`
credential grammar, `AdapterKind::Bedrock`, region-from-base_url, and
the lean-deps decision are preserved and authored by him on the first
two commits; the per-family `BedrockCodec` trait he wrote turned out to
be the crate-wide `Codec` seam in miniature, so the refactor promoted
exactly that shape. The original Claude-only description is preserved in
a comment below.

## What's here

- **`AdapterKind::Bedrock` × `CodecKind::BedrockConverse`** on the
route, plus the `aws_sigv4` credential source (no static secret — the
adapter signs at request time; `fabro-auth` stays AWS-free).
*(@depopry)*
- **SigV4 signer + AWS event-stream `FrameDecoder`** on the lean AWS
stack (no `aws-sdk-bedrockruntime`; transport stays on `fabro-http`).
Re-targeted at Converse's direct-JSON stream frames; the signer resolves
credentials per request. *(@depopry)*
- **`bedrock_converse` codec** — Converse envelope (`system[]`, typed
content blocks, `inferenceConfig`, `toolConfig`), prompt caching via
`cachePoint`, thinking-signature round-trip through `reasoningContent`,
usage mapped onto the disjoint `TokenCounts` buckets,
`provider_options.bedrock` passthrough. Plus the adapter shell and an
event-stream byte loop beside the transport's shared SSE loop.
- **Catalog**: `bedrock.toml` (Claude incl. Fable 5, Nova 2, Llama 4,
Mistral, DeepSeek, Kimi, GLM, MiniMax, Nemotron, gpt-oss — cross-region
inference-profile ids, per-model `billing_policy` so Claude bills
Anthropic-style) and a companion **`bedrock-openai`** provider for
GPT-5.5/5.4 over the `bedrock-mantle` Responses endpoint (pure config
over the existing `openai_responses` codec, zero new code).
- Secrets registry (`AWS_BEARER_TOKEN_BEDROCK`), gitleaks rules for both
Bedrock key formats, the `docs/integrations/bedrock` guide, and live e2e
tests.

## Live verification (confirmed end-to-end against a real AWS account)

Verified on a real Bedrock account (us-east-2, SigV4 + bearer):

- **SigV4 + Converse** — multiple families (Claude, Nova, DeepSeek, …)
via the full settings → catalog → route → adapter → codec path.
- **ConverseStream** — streaming deltas through the workflow engine.
- **Multi-turn tool use** — agent loop with tool calls round-tripping
(no-arg tools included).
- **Multi-model routing** — Claude + DeepSeek pinned in one run through
the single Converse codec.
- **mantle Responses** — `openai.gpt-5.5` answered via the
`bedrock-openai` provider (bearer auth).

The exercise caught and fixed several issues that unit tests (static
creds, mocked transports) could not — see the follow-up commits below.

## Follow-up fixes from live testing (commits on top of the foundation)

1. **Worker AWS env** — the workflow worker scrubs its env to an
allowlist, so SigV4 (which re-resolves from the ambient chain per
request) couldn't work through `fabro run`. The AWS credential-chain
inputs now cross into the worker.
2. **Vault bearer key** — Bedrock was the only key-based provider
missing a `vault:` credential ref, so `fabro secret set
AWS_BEARER_TOKEN_BEDROCK` silently didn't feed it. Now resolves env →
vault → SigV4.
3. **Converse tool-encoding hardening** — a no-arg tool call's
`toolUse.input` is now a `{}` object (Bedrock rejects null), and every
tool `inputSchema` gets a top-level `type: "object"` (strict families
like DeepSeek reject a typeless schema Claude tolerates).
4. **Nova output cap** — `amazon.nova-2-lite` max_output 65536 → 65535
(Bedrock's per-request limit).

Earlier fixes already folded into the foundation commits: the
`aws-config` sleep-impl (default chain panicked) and AWS error-body
decoding (top-level `message`/`Message`/`__type` → proper messages
instead of "Unknown error").

## Manual testing & setup

See `docs/integrations/bedrock` — now documents the non-obvious account
setup that live testing surfaced: the per-Region Anthropic use-case
approval, `aws-marketplace:Subscribe` for third-party models, the Fable
5 / Mythos-class data-sharing opt-in, and the bearer-vs-SigV4 precedence
override for running Converse + mantle side by side.

## Open decision / discussion

- **Model-id naming** — Bedrock rows use dotted ids mirroring Bedrock's
native inference-profile ids (`us.anthropic.claude-sonnet-4-6`,
`openai.gpt-5.5`), which also makes them the wire `api_id`. Third scheme
alongside bare ids and OpenRouter's `vendor/model` slashes. No collision
risk (enforced at catalog build). Open to a uniform scheme if preferred.
- **`BEDROCK_API_KEY` alias** — see the comment thread; the AWS console
hands some users `export BEDROCK_API_KEY=` while the SDK-standard var is
`AWS_BEARER_TOKEN_BEDROCK`. Question of whether to accept both.

## Deferred (named follow-ups)

- **`qwen.qwen3-coder-next`** — omitted pending a verified Bedrock
model/inference-profile id (its fabro id isn't a valid Bedrock
identifier; needs an explicit `api_id`). Re-add once confirmed via `aws
bedrock list-inference-profiles`.
- **Claude Mythos 5** — Anthropic-Messages-only on `bedrock-mantle`
(limited preview).
- **Converse structured output** (`response_format` rejected with a
clear error).
- **`reasoning_effort` on Converse rows** via
`additionalModelRequestFields` (the `bedrock-openai` GPT rows already
accept effort levels).
- **CountTokens** route (`count_input_tokens` returns `None`).

## Verification

`cargo nextest run --workspace`: green except the pre-existing
environment-dependent fabro-workflow failures (identical on main).
clippy `-D warnings` + pinned-nightly fmt clean. Codec unit tests +
adapter httpmock tests + frame-decoder/signer locks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Scott Werner <scott@sublayer.com>
Co-authored-by: Scott Werner <stwerner@vt.edu>
2026-06-16 11:46:49 -04:00
Bryan Helmkamp
bc0bda73a6
feat(web): add server-managed Environments CRUD settings UI (#462)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (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
## What

Adds a CRUD interface for **server-managed Environments** at
`/settings/environments`, driven by the `/api/v1/environments` REST API
(list / create / retrieve / replace / delete), and reshapes how built-in
environments are provisioned and protected.

The page lives in the **Workflows** settings nav section (also
introduced in this branch), positioned before Variables.

## Why

The Environments REST API shipped (#453) but had no UI — environments
could only be managed via the API/CLI. This gives operators a web UI
alongside Variables and Secrets, and along the way tightens the model:
environments are seeded at install time (not silently re-created on
every boot), and the `default` fallback is an ordinary, deletable
environment.

## Web UI

**Pages & component**
- `settings-environments.tsx` — list view: provider badge,
image/resource summary, row actions (Edit/Delete). **"New environment"
is a dropdown** of the enabled sandbox providers; the chosen provider is
fixed for the environment's lifetime.
- `settings-environments-new.tsx` / `settings-environments-edit.tsx` —
create/edit flows; create reads the provider from a query param.
- `environment-form.tsx` — shared form, reorganized:
- **General** panel (merged identity + image): id, and an **image-source
selector** (Image reference *vs* inline Dockerfile) that shows,
requires, and sends only the selected, mutually-exclusive source.
- **Resources**: CPU / memory / disk as **range sliders** (CPU 1–8,
memory 1–16 GB, disk 1–20 GB), each always writing a concrete value.
  - **Environment variables** key/value editor.
- **Advanced** progressive-disclosure section holding **Network** (a
single "Block all network access" toggle — allow-all vs block) and
**Lifecycle** (preserve / stop-on-terminal / auto-stop). Opens by
default when any advanced value is non-default.
- The in-form **provider control and the Labels editor were removed** —
labels remain API-managed and are round-tripped untouched so UI edits
never clear them.

**Data layer**: `environmentsApi` client, `queryKeys.environments`,
`useEnvironments` / `useEnvironment` SWR hooks.

**Nav & routing**: "Environments" item in the Workflows section before
Variables; routes registered in `router.tsx`.

## Backend: seed at install, deletable `default`

- **Seeding moved to install time.** The server no longer seeds
built-ins on startup; `EnvironmentStore::load_or_seed` → `load`
(load-only). A new public `seed_environments(dir)` (idempotent,
preserves operator edits) is called by both the web installer and the
CLI installer. An uninstalled instance therefore has no managed
environments, and a run selecting an absent environment fails explicitly
(`unknown environment: default`) rather than resurrecting a built-in.
- **`default` is no longer protected.** The delete guard and the
`Protected` error variant are gone; deleting `default` succeeds (204)
and removes the run fallback on purpose — forcing an explicit choice.
`local` is unchanged (reserved, in-memory).
- **`volumes` removed** from environment settings across the OpenAPI
spec, generated Rust + TS clients, config layers,
sandbox/server/workflow plumbing, docs, and tests.

## API contract details honored
- Edit sends the environment `revision` as `If-Match`; 409 conflicts
surface a "changed since you opened it" message.
- The REST API accepts inline Dockerfiles only — the form never sends a
Dockerfile path.

## Verification
- Rust: `cargo build` (touched crates) ✅, `cargo nextest -p
fabro-environment` 21/21 ✅, server env unit + `tests/it` integration 2/2
+ 15/15 ✅, `clippy` (nightly, touched crates, all targets) clean ✅, `fmt
--check` clean ✅. Full `--workspace` suite not run here — worth a CI
pass.
- Web: `bun run typecheck` ✅, `bun run build` ✅,
`environment-form.test.ts` 5/5 ✅. Web suite: 512 pass / 1 unrelated
pre-existing `RunDetail` failure.
- **Not visually verified in-browser** — the local app is login-gated
and automated loads redirect to `/login`; rendering of the form, the
New-environment dropdown, and `default` delete should be confirmed in a
logged-in session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (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>
Co-authored-by: Release Repro <release-repro@example.com>
2026-06-13 08:44:38 -04:00
fabro-releases[bot]
786d2953a1 Bump version to 0.260.0-nightly.0 2026-06-10 10:40:28 +00:00
fabro-releases[bot]
4083c3ef10 Bump version to 0.259.0-nightly.0 2026-06-09 10:29:35 +00:00
fabro-releases[bot]
7d951930dd Bump version to 0.256.0-nightly.0
Some checks failed
Rust / Format (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Generated Docs (push) Has been cancelled
Rust / Test (Linux) (push) Has been cancelled
Rust / Test (macOS) (push) Has been cancelled
2026-06-06 09:55:50 +00:00
fabro-releases[bot]
3c6ac9e6e1 Bump version to 0.255.0-nightly.0 2026-06-05 10:37:02 +00:00
Bryan Helmkamp
497aaba6f2
Bump version to 0.254.0 2026-06-04 08:51:34 -04:00
fabro-releases[bot]
84d9f282ab Bump version to 0.254.0-nightly.0 2026-06-04 10:33:58 +00:00
fabro-releases[bot]
d132b96fe7 Bump version to 0.253.0-nightly.0 2026-06-03 10:49:54 +00:00
Bryan Helmkamp
f69677cf82
Bump version to 0.252.0
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
2026-06-02 20:45:09 -04:00
fabro-releases[bot]
083bbf8b30 Bump version to 0.252.0-nightly.0 2026-06-02 23:27:41 +00:00
fabro-releases[bot]
f76d191a48 Bump version to 0.251.0-nightly.1
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
2026-06-01 23:57:37 +00:00
fabro-releases[bot]
0f3ef1c63d Bump version to 0.251.0-nightly.0 2026-06-01 22:40:10 +00:00
fabro-releases[bot]
228387e53b Bump version to 0.250.0-nightly.0
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
2026-05-31 10:07:23 +00:00
fabro-sh-0530[bot]
17cae07e5a
Add scheduled automation executor with in-memory cron planner (#457)
## Summary

Automation schedule triggers now fire automatically at their UTC cron
times. A new long-lived server task (`spawn_automation_scheduler`) owns
an in-memory planner that tracks one cursor per enabled schedule trigger
and creates/starts a normal Fabro run whenever a cursor comes due —
reusing the same materialization and run-creation path as API-triggered
runs.

### Plan Summary

- **Planner** (`AutomationSchedulePlanner`) — pure in-memory, no
persistent state. Reconciles cursors from the live automation list on
every tick; advances each due cursor _before_ spawning the fire task so
a failed materialization never hot-loops.
- **Executor loop** (`spawn_automation_scheduler`) — wakes on the
nearest cursor due time (capped at 30 s), on automation store mutations
(via `Notify`), or on shutdown. Spawns one Tokio task per due trigger so
slow materializations don't block other triggers.
- **Run firing** (`fire_scheduled_automation_run`) — materializes, calls
`create_run_from_manifest` with `Principal::System { Engine }`, then
calls `queue_run_start`. Warnings on any failure; next attempt waits for
the next cron occurrence.
- **Wiring** — `automation_scheduler_notify: Notify` added to
`AppState`; `create_automation`, `replace_automation`, and
`delete_automation` handlers call `notify_automation_scheduler()` so the
planner reacts immediately to changes.
- **Visibility widening** — `handler/lifecycle.rs` (`queue_run_start`)
and `handler/mod.rs` (`lifecycle`) promoted from `pub(super)` to `pub(in
crate::server)` so the scheduler (a sibling of `handler`) can call the
same start path.
- **Shared cron parser** — `parse_schedule_expression` extracted to
`fabro-automation` and re-exported so both validation and the scheduler
use the same parser configuration (no seconds, no year).

### Key design decisions

| Decision | Rationale |
|---|---|
| Cursor advances before fire task spawns | Guarantees at-most-one
attempt per occurrence even if materialization panics |
| No backfill on startup | Matches locked spec; `next_occurrence(expr,
now)` always starts from the current time |
| `Principal::System { Engine }` for actor | Avoids adding new public
enum variants or OpenAPI surface |
| `automation_temp_root()` extracted to `AppState` | Removes duplicated
`Storage::new(…).scratch_dir().join("automations")` from the automations
handler |
| Tests use `run_due_schedules_once` helper | Drives the planner
directly with fixed `DateTime<Utc>` values; no wall-clock sleeps in
tests |


### Fabro Details

<details>
<summary>Ran 9 stages in 60m 12s for $22.11</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 26s | – | 0 |
| implement | 20m 14s | $12.44 | 0 |
| simplify_opus | 12m 29s | $6.34 | 0 |
| simplify_gpt | 3m 53s | $2.79 | 0 |
| verify | 11m 5s | – | 0 |
| fixup | 6m 46s | $0.54 | 0 |
| **Total** | **60m 12s** | **$22.11** | **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>
2026-05-29 22:19:47 -04:00
fabro-sh-0530[bot]
037073d2b2
feat: Add Environment REST CRUD API under /api/v1/environments (#453)
## Summary

Adds a server-managed Environment CRUD API at `/api/v1/environments`,
modeled after the existing Automations API and backed by
`EnvironmentStore`. The API manages only server-side environment
definitions in `environments/*.toml`; client-side catalogs (workflow,
project TOML, run inputs) are unaffected.

### Plan Summary

- **OpenAPI contract**: new `Environments` tag, `EnvironmentId` path
parameter, five CRUD paths, list envelope, and REST-specific inline-only
image schema (`EnvironmentApiImageSettings`)
- **Server handler** (`environments.rs`): mirrors `automations.rs` —
auth guard, ETag/If-Match, and `EnvironmentStoreError → ApiError`
mapping
- **Shared handler utilities**: `parse_required_if_match` and
`json_with_etag_response` extracted from `automations.rs` into
`handler/mod.rs` so both modules share them
- **Inline-only Dockerfile enforcement**: `ApiDockerfileSource::Path` is
parsed and immediately rejected with `422`; the file is never read
- **Manifest refresh**:
`refresh_manifest_run_settings_from_environment_catalog()` called after
create, replace, and delete so `/system/info` and default run settings
stay consistent
- **Client regeneration**: TypeScript Axios client regenerated with
`EnvironmentsApi` and new model files; Rust `fabro-api` type aliases
updated
- **Tests**: integration suite in `tests/it/api/environments.rs`
covering all CRUD paths, error cases, and the manifest-refresh
invariant; OpenAPI conformance test verifies generated surfaces

## Key Design Decisions

**Inline-only Dockerfile at the REST boundary.** Allowing `path` sources
over REST would let callers silently read arbitrary server-local files
into the environment catalog. The handler recognizes the `path`
discriminant so it can return a descriptive `422` rather than a generic
parse error, but the payload is discarded via `IgnoredAny` — no disk
access occurs.

**Shared ETag utilities instead of per-handler helpers.** The original
`parse_required_if_match` and ETag header builder in `automations.rs`
were duplicated for environments. They're now generic over any `FromStr`
revision type in `handler/mod.rs`, making future resource handlers
cheaper to add.

**`Environment` response type aliased to domain type.** The
OpenAPI-generated `Environment` response struct is replaced with
`fabro_environment::Environment` via `build.rs` `with_replacement`. A
compile-time function-cast witness in
`fabro-api/tests/environment_round_trip.rs` confirms the alias holds.
Request types (`CreateEnvironmentRequest`, `ReplaceEnvironmentRequest`)
stay API-specific because their image schema differs from the
workflow/settings schema.

**Stale revision → `409`.** Consistent with Automations; `428` is
reserved for missing `If-Match` only.


### Fabro Details

<details>
<summary>Ran 8 stages in 59m 23s for $30.41</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 25s | – | 0 |
| implement | 25m 43s | $19.79 | 0 |
| simplify_opus | 14m 34s | $6.98 | 0 |
| simplify_gpt | 4m 39s | $3.64 | 0 |
| verify | 9m 14s | – | 0 |
| **Total** | **59m 23s** | **$30.41** | **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>
2026-05-29 17:30:57 -04:00
Bryan Helmkamp
0f6da7d5cc
chore: bump fabro-environment to 0.247.0-nightly.0 in Cargo.lock
Cargo.lock churn from the merge with origin/main; the workspace bumped
fabro-environment's version but Cargo.lock still pointed at the
0.246.0-nightly.0 entry until a build refreshed it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:52:24 -04:00
fabro-sh-0530[bot]
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>
2026-05-28 17:10:59 -04:00
fabro-releases[bot]
6b26915a09 Bump version to 0.247.0-nightly.0 2026-05-28 10:41:43 +00:00
fabro-sh-0530[bot]
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>
2026-05-27 22:29:08 -04:00
fabro-sh-0530[bot]
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>
2026-05-27 20:24:25 -04:00
fabro-sh-0530[bot]
2d78f96107
Wire automation store into AppState and expose CRUD REST API (#439)
## Summary

Loads `AutomationStore` into `AppState` at server startup and exposes
five authenticated REST endpoints (`GET/POST /automations`,
`GET/PUT/DELETE /automations/{id}`) backed by the existing
`fabro-automation` crate.

### Plan Summary

- Add `fabro-automation` as a dependency of `fabro-server` and mount
`Arc<AutomationStore>` on `AppState`, computed from a sibling
`automations/` directory next to the active config file.
- Change `AutomationStore::load` from `async` to synchronous (`std::fs`)
so it can run before the Tokio runtime needs to make progress; malformed
files now fail startup instead of being silently skipped.
- Implement `src/server/handler/automations.rs` with shared helpers for
path-ID parsing, `If-Match` (quoted/unquoted) parsing, ETag formatting,
and `AutomationStoreError → ApiError` mapping.
- HTTP semantics: 201 on create, 404 on missing, 409 on duplicate or
stale revision, 422 on domain validation failure, 428 on missing
`If-Match`.
- Update `TestAppStateBuilder` to derive `active_config_path` from the
vault path so each test gets an isolated sibling `automations/`
directory; add `try_build()` to allow startup-failure assertions.
- Update the OpenAPI spec and generated TypeScript client to include
`AutomationListMeta` with a `total` field.

## Key design decisions

**Sync load path.** `AutomationStore::load` is now `fn` (not `async
fn`), using `std::fs`. A `#[expect(clippy::disallowed_methods)]`
annotation explains the rationale: this runs once at startup before the
runtime needs to yield, and avoids requiring a Tokio handle at the call
site in `build_app_state`.

**Fail-fast on malformed files.** Previously, corrupt TOML files were
logged as warnings and skipped. Now any parse or validation error during
load aborts server startup. The old `warn_load_failure` helper is
deleted; tests that relied on skip behaviour are replaced with tests
that assert `Err(AutomationStoreError::Parse { .. })` and
`Err(AutomationStoreError::InvalidFilename { .. })`.

**ETag / If-Match handling.** `parse_required_if_match` strips optional
surrounding quotes before parsing the revision, so both `"<rev>"` and
bare `<rev>` are accepted from clients. Missing `If-Match` on PUT/DELETE
returns **428 Precondition Required**, not 400.

**Test isolation.** `TestAppStateBuilder::build` now derives
`active_config_path` from `vault_path.with_file_name("settings.toml")`
instead of a random temp path, so the sibling `automations/` directory
is predictable and cleaned up with the same temp dir.


### Fabro Details

<details>
<summary>Ran 10 stages in 85m 20s for $33.66</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 19s | – | 0 |
| preflight_lint | 2m 5s | – | 0 |
| fix_lints | 33s | $0.15 | 0 |
| implement | 30m 33s | $17.35 | 0 |
| simplify_opus | 20m 27s | $11.82 | 0 |
| simplify_gpt | 6m 41s | $3.88 | 0 |
| verify | 15m 44s | – | 0 |
| fixup | 6m 8s | $0.46 | 0 |
| **Total** | **85m 20s** | **$33.66** | **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>
2026-05-27 18:45:59 -04:00
fabro-sh-0530[bot]
c767db897f
Add Automations API contract to OpenAPI spec and update generated clien… (#436)
## Summary

Defines the public Automations REST API contract in the OpenAPI spec,
updates the `RunSandbox` schema to reflect the new sandbox lifecycle
model, adds `fabro-automation` as a dependency to `fabro-api` for type
reuse, and removes the retired `fabro-devcontainer` crate and all
references to it.

## What changed

### Automations API (`fabro-api.yaml`)
Seven new paths under `/api/v1/automations` covering the full CRUD
surface plus run sub-resources:

```
GET/POST   /automations
GET/PUT/DELETE /automations/{id}
GET/POST   /automations/{id}/runs
```

New schemas: `Automation`, `AutomationTarget`, `AutomationTrigger`
(discriminated oneOf on `type`), `AutomationApiTrigger`,
`AutomationScheduleTrigger`, `CreateAutomationRequest`,
`ReplaceAutomationRequest`, `AutomationListResponse`.

Key contract decisions:
- `AutomationTrigger` uses an OpenAPI discriminator (`propertyName:
type`); unknown discriminator values → HTTP 422, not 400.
- `PUT` and `DELETE` require an `If-Match` header (428 if absent, 409 on
mismatch); `GET` and `PUT` responses carry an `ETag`.
- `POST /automations/{id}/runs` fires the automation's enabled API
trigger; 409 if the automation is disabled or lacks one.
- Run sub-resource responses reuse the existing `Run` and
`PaginatedRunList` schemas.

### `RunSandbox` schema refactor
The sandbox schema is restructured to express the full lifecycle rather
than only the ready state:

| Before | After |
|---|---|
| Flat object with `provider`, `image`, `snapshot`, `runtime` |
Discriminated by `kind`: `planned`, `initializing`, `ready`, `failed` |
| `runtime` was nullable | Moved into `RunSandboxInstance`
(non-nullable); present only when `kind = ready` |
| No failure detail | New `RunSandboxFailure` schema with `error`,
`causes`, `duration_ms` |

`SandboxDetails.sandbox` now references `RunSandboxInstance` (the ready
state), which preserves the existing shape for the details endpoint
while the richer `RunSandbox` type appears on run responses.

### Web UI (`run-sandbox-lifecycle.ts`)
New helper module that bridges the old flat-object sandbox wire shape
and the new lifecycle-keyed shape, with display metadata for each
lifecycle state. Consumers (`RunSummaryPanel`, `TerminalView`,
`RunSandbox` route, `run-detail` header/tabs) updated to route through
these helpers so both old and new wire shapes are handled transparently.

### `fabro-devcontainer` removal
The `fabro-devcontainer` crate is removed from `Cargo.lock`,
`AGENTS.md`, nextest config, and all doc references. Public-facing
changelog entries for devcontainer-specific features are removed or
retitled.

### Plan Summary
- Add Automations CRUD + run sub-resource paths and schemas to the
OpenAPI spec
- Restructure `RunSandbox` schema to model lifecycle states (`planned →
initializing → ready | failed`)
- Add `fabro-automation` dependency to `fabro-api` for domain-type
reuse; add JSON parity round-trip tests
- Regenerate Rust API types and TypeScript client
- Remove `fabro-devcontainer` crate and all references
- Add `run-sandbox-lifecycle.ts` helper module in the web UI and update
all sandbox-state consumers


### Fabro Details

<details>
<summary>Ran 9 stages in 83m 29s for $35.47</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 22s | – | 0 |
| preflight_lint | 2m 40s | – | 0 |
| implement | 45m 36s | $28.67 | 0 |
| simplify_opus | 7m 46s | $2.46 | 0 |
| simplify_gpt | 6m 7s | $3.92 | 0 |
| verify | 12m 8s | – | 0 |
| fixup | 5m 52s | $0.43 | 0 |
| **Total** | **83m 29s** | **$35.47** | **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>
2026-05-27 14:07:01 -04:00
Bryan Helmkamp
352b7c5de4
refactor: remove devcontainer support (#433)
## Summary

Remove devcontainer support from the product surface and codebase: the
parser crate, workflow bridge, lifecycle execution path, typed events,
CLI progress rendering, generated client field, and public/internal
documentation references are all gone.

## What Changed

- Deleted the dedicated parser crate and removed its Cargo dependencies
and lockfile entries.
- Removed workflow initialization paths that resolved repository
devcontainer metadata, applied Daytona snapshots from it, merged
environment variables from it, or ran its lifecycle commands.
- Removed the typed event variants and CLI progress handlers for the
retired lifecycle events while leaving shared unknown-event handling
intact.
- Cleaned the generated TypeScript client and tracked docs so repository
search has no remaining devcontainer references outside git history.

## Verification

- `cargo +nightly-2026-04-14 fmt --all`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo build --workspace`
- `cargo nextest run -p fabro-types`
- `cargo nextest run -p fabro-workflow`
- `cargo nextest run -p fabro-cli run_progress`
- `cd lib/packages/fabro-api-client && bun run generate && bun run
typecheck`
- `cargo metadata --no-deps --format-version 1 | rg -i
"fabro-devcontainer|devcontainer"`
- `rg -n -i "devcontainer|dev
container|dev-container|dev_container|fabro-devcontainer|\\.devcontainer"
. --glob '!target/**' --glob '!.worktrees/**'`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-27 12:48:40 -04:00
Bryan Helmkamp
cf124be413
fix(types): finish image.ref → image.docker rename in env var substitution
Commit ec1b3f2 (#429) renamed `EnvironmentImageSettings::reference` to
`docker` but missed the variable-substitution call site in
`substitute_environment` and its companion test, breaking the workspace
build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:15:14 -04:00
Bryan Helmkamp
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`
2026-05-27 11:52:57 -04:00
Bryan Helmkamp
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`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-27 11:52:35 -04:00
Bryan Helmkamp
b1bd2f522c
feat(server): add variables API (#430)
## Summary

Adds a workflow-visible variables store and HTTP API for managing
non-sensitive run variables, then wires those variables into run config
interpolation before run creation, validation, and preflight.

## What Changed

- Adds `/api/v1/variables` CRUD endpoints backed by a JSON variable
store and generated Rust/TypeScript API types.
- Supports `{{ vars.NAME }}` interpolation alongside existing `{{
env.NAME }}` handling for run-owned config fields, including
environment, MCP, hook, artifact, checkpoint, SCM, and notification
settings.
- Reuses canonical `fabro-types` variable DTOs in `fabro-api` and adds
OpenAPI name patterns so clients see the same env-style variable
contract enforced by the server.
- Keeps variable updates store-owned with `update_existing`, avoiding
duplicated not-found/update semantics in the HTTP handler.
- Shares env-style name validation between variables, interpolation
parsing, and vault token names to avoid grammar drift.

Variables are intentionally non-sensitive: list/get responses include
values, unlike vault secrets.

## Validation

- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-types`
- `cargo test -p fabro-variable`
- `cargo test -p fabro-api --test variable_round_trip`
- `cargo test -p fabro-server --features test-support --test it
api::variables`
- `cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-variable -p
fabro-vault --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy -p fabro-server --features
test-support --all-targets -- -D warnings`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex/)
2026-05-27 11:46:36 -04:00
fabro-releases[bot]
9b8d7b798a Bump version to 0.246.0-nightly.0 2026-05-27 10:39:37 +00:00
fabro-releases[bot]
7148e37bbe Bump version to 0.245.0-nightly.1 2026-05-26 10:39:31 +00:00
fabro-releases[bot]
43291a6317 Bump version to 0.245.0-nightly.0 2026-05-26 02:58:03 +00:00
fabro-sh-0530[bot]
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>
2026-05-25 17:26:01 -04:00
Bryan Helmkamp
290a1a4168
Bump version to 0.244.0 2026-05-25 12:31:12 -04:00
fabro-releases[bot]
3cd8a670d4 Bump version to 0.244.0-nightly.0 2026-05-25 15:32:33 +00:00
Fabian Kukuck
990324710c
Add legacy SSE MCP transport support (#386)
## Summary

Adds support for MCP servers that use the SSE-based HTTP transport,
including Playwright MCP.

Fabro already supports stdio and Streamable HTTP MCP servers. Some MCP
servers still expose the SSE transport shape where the client opens an
SSE stream, receives an `endpoint` event, and sends JSON-RPC requests
back to that endpoint. This PR adds an explicit `protocol = "sse"`
option while keeping Streamable HTTP as the default.

## What Changed

- Added `McpHttpProtocol` with `streamable_http` as the default and
`sse` as an opt-in protocol.
- Added an SSE MCP client transport implementation.
- Wired HTTP MCP setup to choose Streamable HTTP or SSE based on config.
- Added `protocol = "sse"` support for both `http` and `sandbox` MCP
entries.
- Updated sandbox MCP resolution so SSE sandbox servers connect through
the preview `/sse` path.
- Documented `protocol = "sse"` for Playwright MCP.
- Added an integration test covering SSE initialize, tool listing, and
tool calls.

## Example

```toml
[run.agent.mcps.playwright]
type = "sandbox"
protocol = "sse"
command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless", "--browser", "chromium"]
port = 3100
startup_timeout = "60s"
tool_timeout = "2m"
```

## Compatibility

Existing MCP configs are unchanged because `protocol` defaults to
`streamable_http`.

## Validation

- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-mcp`
- `cargo check -p fabro-agent -p fabro-workflow -p fabro-config`

---------

Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:29:01 -04:00
Bryan Helmkamp
858904e7ac
Fix token count provider log context
Keep background context-window token count requests inside the current tracing span so run-scoped logs retain run_id. Label OpenAI input token count provider failures with operation=input_token_count for easier diagnosis.
2026-05-24 14:01:38 -04:00
fabro-releases[bot]
fd5d932346 Bump version to 0.243.0-nightly.1 2026-05-24 09:53:18 +00:00
fabro-releases[bot]
bf101fd4b5 Bump version to 0.243.0-nightly.0 2026-05-24 04:02:52 +00:00
fabro-sh-0530[bot]
3fb4b5bc1b
Add output_schema validation with same-context repair for agent and pro… (#374)
## Summary

Adds `output_schema` and `output_retries` node attributes that validate
structured LLM output and perform corrective repair turns inside the
same conversation context before failing the node. Also adds sortable
columns (Repo, Title, Workflow, Changes) to the runs list view and hides
the pager when the result set is small.

### Plan Summary

- **Task 1**: `Node::output_schema()` / `Node::output_retries()`
accessors in `fabro-types`, with `@`-prefix file-reference support in
static validation and file inlining.
- **Task 2**: New `handler/structured_output.rs` module —
`OutputSchemaKind` (Routing / JsonSchema), balanced JSON scanning,
validation, repair-message generation, `apply_validated_output`, and
`exhausted_failure_outcome`.
- **Task 3**: `extract_status_fields` moved to `structured_output.rs`;
agent routing fallback chain (response → `status.json` → last file
touched) preserved and delegated to `validate_agent_output_sources`.
- **Task 4/5**: `one_shot` (prompt) and `run` (agent) both loop over LLM
calls, appending the prior assistant response and a corrective user turn
on validation failure, up to `output_retries` times.
- **Task 6**: ACP backend rejects `output_schema` immediately with a
clear error before launching any process.
- **Task 7**: `outputs.mdx` and `dot-language.mdx` updated with
attribute docs, repair semantics, and `output.{node_id}` context key.

## What changed and why

```mermaid
TB
  graph

  A[Node attrs\noutput_schema / output_retries] --> B[structured_output.rs\nparse / validate / repair]
  B --> C{OutputSchemaKind}
  C -->|Routing| D[validate routing fields\n→ outcome routing]
  C -->|JsonSchema| E[jsonschema validator\n→ context_updates.output.node_id]
  B --> F[exhausted_failure_outcome\nterminal, non-retryable]

  G[prompt handler\none_shot loop] --> B
  H[agent handler\nrun loop + session.process_input] --> B
  I[ACP backend] -->|output_schema present| J[Validation error\nno process launched]
```

**`output_schema="routing"`** tightens existing loose routing
extraction: malformed fields now fail validation and trigger a repair
turn rather than being silently ignored. The fallback priority (response
text → `status.json` → last file touched) is preserved but only for the
`NoJsonObject`/`NoRelevantJsonObject` error kinds that allow it.

**Custom schemas** (`@path` inlined to JSON Schema) validate the last
JSON object in the response against a precompiled
`jsonschema::Validator`. On success, the parsed value is stored at
`output.{node_id}` in `context_updates` for downstream nodes.

**Repair loop** — prompt nodes keep the prior assistant response in the
message list and append a corrective user message; agent API sessions
call `session.process_input` on the live session. Both paths aggregate
token usage across all turns. Exhausting `output_retries` returns a
terminal `OutputSchemaValidation` error (non-retryable, deterministic
failure category) that does not consume `max_retries`.

**ACP guardrail** rejects `output_schema` before spawning any
subprocess, with a clear `"output_schema is not supported with
backend=\"acp\" in this release"` message.

The `one_shot` refactor also extracted `complete_one_shot_request` and
`OneShotCompletion` to separate fallback-chain logic from the repair
loop, removing duplication.


### Fabro Details

<details>
<summary>Ran 9 stages in 74m 9s for $31.92</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 16s | – | 0 |
| preflight_lint | 2m 28s | – | 0 |
| implement | 24m 59s | $17.96 | 0 |
| simplify_opus | 16m 36s | $9.95 | 0 |
| simplify_gpt | 3m 20s | $1.75 | 0 |
| verify | 6m 29s | – | 0 |
| fixup | 17m 13s | $2.26 | 0 |
| **Total** | **74m 9s** | **$31.92** | **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 && ! 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: Fabro <fabro@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 19:45:16 -04:00
fabro-sh-0530[bot]
c81fcc2a27
Auto-migrate legacy [run.sandbox] config files to named-environment syn… (#375)
## Summary

On startup, Fabro now detects confidently migratable pre-v1.0
`[run.sandbox]` config files and rewrites them in-place to the
`[run.environment]` + `[environments.default]` named-environment syntax.
The original file is preserved as a sibling
`*.legacy-sandbox-migration.bak` before any write. Unsupported or
ambiguous keys produce a targeted error listing exact key paths rather
than a generic TOML unknown-field failure.

### Plan Summary

- **New module** `legacy_sandbox_migration.rs` owns all detection,
rewriting, backup logic, and unsupported-key diagnostics — isolated so
it can be deleted before v1.0.
- **`load.rs` hook** catches parse failures on file loads and attempts
migration before re-raising the original error, leaving in-memory
`SettingsLayer` parsing strict and unchanged.
- **Field mappings** cover Daytona (snapshot, volumes, labels,
lifecycle, `auto_stop_interval`) and Docker (image, `memory_limit`,
`cpu_quota` divisible by 100 000, `skip_clone`).
- **Ambiguity guard** rejects files that already contain
`[run.environment]` or `[environments.default]` alongside
`[run.sandbox]`.
- **Docs** add a `<Warning>` block to `environments.mdx` and a new
`2026-05-23.mdx` changelog entry.

This PR also bundles two unrelated improvements that landed in the same
branch: additional sort keys (`repo`, `title`, `workflow`, `changes`)
for the runs list API and UI, and a test isolation fix in `user.rs` that
wraps path assertions in `with_var` to avoid `FABRO_HOME` leakage.

### Migration flow

```mermaid
flowchart TB
    A[load_settings_path] --> B{parse SettingsLayer}
    B -- ok --> G[resolve paths / return]
    B -- err --> C{migrate_settings_path}
    C -- no legacy sandbox --> D[return original parse error]
    C -- has new env config --> E[error: ambiguous, manual fix required]
    C -- unsupported keys --> F[error: list unsupported keys]
    C -- success --> H[write .bak, rewrite file, warn]
    H --> I[parse migrated SettingsLayer]
    I --> G
```


### Fabro Details

<details>
<summary>Ran 0 stages in 53m 26s for $16.82</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **53m 26s** | **$16.82** | **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 && ! 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: Fabro <fabro@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 19:34:43 -04:00
Bryan Helmkamp
1cd5d89316
Runs list view: table layout, server-side sort/pagination, redesigned toolbar (#367)
## Summary

Overhauls the `/runs` list view and consolidates the two runs endpoints
that backed it.

**API**
- Removes `GET /api/v1/boards/runs`, `PaginatedBoardRunList`, and
`BoardColumnDefinition`. The board view is now a pure frontend
rendering.
- `GET /api/v1/runs` gains `status` (repeatable `BoardColumn`), `sort`
(`created_at | updated_at | status | elapsed`, default `created_at`),
and `direction` (`asc | desc`, default `desc`).
- `BoardColumn` enum gains `removing`; default behavior hides
Removing-status runs, opt in with `?status=removing`.
- `PaginationMeta` gains an optional `total: int64`; `list_runs` fills
it in (free — it already filters all runs in memory before paging).

**List view UI**
- Renders as a real `<table>` with column headings instead of horizontal
cards.
- Sortable Status, Elapsed, Created, and Updated headers — click to
toggle direction, click another to switch sort key (resets to desc). URL
params drive `sort`/`direction`/`page`/`size`.
- New pager footer with rows-per-page selector (10/25/50/100), `Page X
of Y`, and first/prev/next/last icon buttons.
- Toolbar redesigned into left (search + filter buttons for
Time/Repo/Workflow + archived toggle) and right (column picker + view
toggle) sections. Filter buttons use Headless UI `Menu` popovers; the
column picker uses Headless UI `Listbox` with `multiple` for
multi-select. Hidden columns persist via `?hide=...`.

**Tests**
- 589 server tests pass, including new coverage for status filter
(single + repeated), Removing opt-in, sort × direction with `id desc`
tiebreak, and status-bucket sorting.
- Frontend tests updated for the matcher-based cache invalidation and
the new `buildBoardColumns` signature; 435 pass (3 pre-existing
`RunDetail full-height` failures unrelated to this change).

## Test plan

- [ ] `cargo build --workspace`
- [ ] `cargo nextest run -p fabro-server`
- [ ] `cd lib/packages/fabro-api-client && bun run generate` — no diff
(already regenerated and committed)
- [ ] `cd apps/fabro-web && bun run typecheck && bun test`
- [ ] Manual: visit `/runs` — board view still renders all columns in
canonical order, Removing runs hidden, archived toggle works.
- [ ] Manual: visit `/runs?view=list` — table renders with sortable
headers; clicking a header updates URL; pager advances; changing
rows-per-page resets to page 1; column picker hides/shows columns and
round-trips via `?hide=`.
- [ ] Manual: `curl '/api/v1/boards/runs'` → 404; `curl
'/api/v1/runs?status=removing'` returns only removing runs; `curl
'/api/v1/runs?sort=status&direction=asc'` returns runs grouped by status
bucket.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:25:58 -04:00
fabro-sh-0530[bot]
651eae6b34
Add Slack run lifecycle notifications via [run.notifications] (#365)
## Summary

Extends the Slack integration to post `run.started`, `run.completed`,
and `run.failed` notifications, configured per-run or per-workflow
through `[run.notifications]` rather than server config. Interview
behavior is unchanged and keeps its own state.

### What changed and why

**`SlackService` is now started whenever Slack credentials are
present**, regardless of whether `default_channel` is set. Previously,
the service required `default_channel` to initialize, which blocked
lifecycle notifications for users who have no interview default.
`default_channel` is now `Option<String>` and is only consulted in the
`InterviewStarted` path.

**`handle_event` receives the full `EventEnvelope` and `AppState`**
instead of just the `RunEvent`. Lifecycle handling needs to read the
cached run projection (for `[run.notifications]` routes) and scan prior
events (for PR details and the `run.started` event name), both of which
require `AppState`.

**Lifecycle path in `handle_event`** (`RunStarted` / `RunCompleted` /
`RunFailed`):
1. Reads the run projection to find enabled Slack routes whose `events`
list contains the current event name.
2. For terminal events, scans prior run events to recover
`PullRequestCreated` details and the `run.started` event name.
3. Resolves each route's channel (supporting `{{ env.VAR }}`
interpolation); warns and skips on missing/empty/unresolved channels
without affecting other routes.
4. Posts once per matching route concurrently via `join_all`; post
failures are logged, never propagated.

**`fabro-slack/src/blocks.rs`** adds `run_lifecycle_blocks` and helpers
separate from the interview builders:
- `RunLifecycleKind` uses `strum::IntoStaticStr` for the title string.
- All untrusted fields go through `escape_slack_controls` +
`truncate_to_limit`.
- `compact_duration` formats milliseconds into human-readable strings
(`1.2s`, `1m 5s`, `2h 30m`, …).
- PR line includes number, optional URL link, and optional HTML-escaped
title.

**`SlackClient::with_api_base_and_http`** is added as a test constructor
so server tests can point the client at a `MockServer` without going
through the normal builder path.

### Design decisions

- Lifecycle notifications are fire-and-forget and never touch
`posted_messages` or `thread_registry`, keeping interview and
notification state fully separate.
- `default_channel` is only used for interviews; lifecycle channel
always comes from `[run.notifications.<name>.slack].channel`. This
matches the goal of not promoting per-run config into server config.
- PR title is sourced only from prior `PullRequestCreated` events — no
GitHub API call is made at notification time. If only a
`PullRequestLink` is available in the projection, number and URL are
included but title is omitted.
- Workflow label resolution follows a priority chain: workflow name →
workflow slug → graph name → `run.started` event name → raw event name.

### Plan Summary

- Make `SlackService` start without `default_channel`; gate interview
path on `default_channel` presence.
- Add `handle_lifecycle_event` that filters routes, loads prior events,
builds blocks, resolves channels, and fans out posts.
- Add `run_lifecycle_blocks` Block Kit builder with escaping,
truncation, and `compact_duration`.
- Add server integration tests covering: started/completed/failed
posting, route filtering, missing/unresolved channel skipping, PR
details from prior events, and interview/lifecycle state isolation.
- Update public docs for Slack integration and run configuration.


### Fabro Details

<details>
<summary>Ran 9 stages in 53m 38s for $22.76</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 58s | – | 0 |
| preflight_lint | 2m 11s | – | 0 |
| implement | 23m 3s | $14.18 | 0 |
| simplify_opus | 16m 37s | $6.36 | 0 |
| simplify_gpt | 5m 30s | $2.23 | 0 |
| verify | 3m 31s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **53m 38s** | **$22.76** | **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 <bhelmkamp@users.noreply.github.com>
2026-05-23 13:07:42 -04:00
fabro-sh-0530[bot]
c987766641
agent: use API usage baseline for compaction context estimate (#366)
## Summary

Replaces the whole-history `chars / 4` compaction trigger with a Claude
Code-style hot-path estimator: find the latest assistant turn with real
provider-reported `usage.total_tokens()`, use that as the baseline, and
add local char estimates only for turns appended after it. This makes
compaction sensitive to actual provider-reported context usage
(including cache read/write and reasoning tokens) without adding any
provider token-count API calls.

### Plan Summary

- **New estimator** (`estimate_active_context_usage`): returns a
`ContextEstimate` with both a token count and an `ContextEstimateMethod`
enum tag (`ApiUsagePlusLocalDelta` or `LocalEstimate`).
- **`check_context_usage`** now returns `Option<ContextEstimate>`
instead of `bool`, and includes `estimate_method` in warning `details`.
The caller passes the estimate directly into `compact_context`, avoiding
a double-compute.
- **`compact_context`** signature drops `system_prompt` (no longer
needed) and takes the pre-computed `ContextEstimate`. The
`CompactionStarted` event is now emitted *after* the `turns.len() <=
preserve_count` no-op guard, so a no-op can never emit `Started` without
`Completed`.
- **`History::compact`** invalidates preserved assistant `usage` (resets
to `TokenCounts::default()`) so a preserved turn's pre-compaction
provider baseline never becomes the next estimate's anchor. Content,
tool calls, provider parts, and response IDs are untouched.
- **`session.compact_if_needed`** restructured to early-return on `None`
from `check_context_usage` or on compaction disabled, simplifying the
nesting.

## Key design decisions

**Why invalidate preserved assistant usage?** After compaction the prior
turns are gone, so a stored `total_tokens` from before compaction would
overstate the new context. The authoritative billing record is in
emitted run events, not in mutable runtime history.

**Why return `Option<ContextEstimate>` from `check_context_usage`?**
Avoids recomputing the estimate in `compact_context`. It also makes the
call-site idiom (`let Some(estimate) = ... else { return; }`) an
explicit gate, which is cleaner than a separate bool-then-compact
pattern.

**Why `strum::IntoStaticStr` on the method enum?** Lets the variant
serialize to a `&'static str` for the JSON `details` field without a
manual `match` or adding `serde` derives.


### Fabro Details

<details>
<summary>Ran 9 stages in 30m 54s for $7.93</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 52s | – | 0 |
| preflight_lint | 2m 6s | – | 0 |
| implement | 8m 11s | $3.47 | 0 |
| simplify_opus | 11m 7s | $3.53 | 0 |
| simplify_gpt | 2m 39s | $0.93 | 0 |
| verify | 4m 4s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **30m 54s** | **$7.93** | **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>
2026-05-23 12:55:23 -04:00
fabro-releases[bot]
bcebc4508f Bump version to 0.242.0-nightly.1 2026-05-23 10:17:18 +00:00
fabro-sh-0530[bot]
bf7ce485e1
feat(fabro-types): promote transcript primitives and extend agent event… (#357)
## Summary

This is the foundational step of the unified agent transcript
implementation: it establishes one canonical set of replay types in
`fabro-types` and threads them into the existing `agent.message`,
`agent.tool.started`, and `agent.tool.completed` event shapes — without
breaking any existing producers or consumers.

## What changed

**New `fabro-types::transcript` module** owns `ContentPart`,
`ImageData`, `AudioData`, `DocumentData`, `ThinkingData`, `ToolCall`,
`ToolResult`, `MessageKind`, `MessageSource`, `PairMessageRef`,
`TranscriptMessage`, and `MessageId`. These were previously defined in
`fabro-llm::types`; they now live at the canonical layer.

**`fabro-llm::types`** drops its local definitions and re-exports from
`fabro-types` so every existing `fabro_llm::types::*` import keeps
compiling without change.

**`AgentMessageProps`** gains an optional `message:
Option<TranscriptMessage>` field; `AgentToolStartedProps` gains
`tool_call`, `turn_id`, and `parent_message_id`;
`AgentToolCompletedProps` gains `tool_result` and `turn_id`. All new
fields use `#[serde(default, skip_serializing_if = "Option::is_none")]`
so existing stored events deserialize cleanly.

**All current event emitters** (`fabro-workflow/event/convert.rs`, demo
fixtures, test helpers) are updated to set the new fields to `None` —
this is a mechanical compatibility update; actual enrichment comes in
later tasks.

### Design decisions worth noting

- `MessageKind` captures LLM role semantics (system / user / reasoning /
agent); `MessageSource` captures audit provenance (steer, pair,
loop_detection, …). They are intentionally kept separate so a steering
message can be `kind=user, source=steer` without collapsing the
distinction.
- `TranscriptMessage` is named with the `Transcript` prefix specifically
to avoid import ambiguity with `fabro_agent::Message` and
`fabro_llm::types::Message`.
- `ProviderAnswer` and `ProviderReasoning` are included as
`MessageSource` variants so committed model outputs carry a first-class
audit label distinct from user-originated inputs.
- The new fields are additive-only; no narrow legacy fields were
removed. Consumer migration is a separate step.


### Fabro Details

<details>
<summary>Ran 9 stages in 47m 27s for $12.79</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 20m 6s | $8.53 | 0 |
| simplify_opus | 13m 36s | $2.68 | 0 |
| simplify_gpt | 4m 17s | $1.58 | 0 |
| verify | 4m 7s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **47m 27s** | **$12.79** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-22 20:40:10 -04:00
fabro-releases[bot]
fa474e36f7 Bump version to 0.241.0-nightly.1 2026-05-22 19:06:56 +00:00