## Summary
Fixes the 33 `fabro-llm::it wire::*` snapshot failures currently red on
`main`.
These are a **semantic merge conflict** between two PRs that landed in
parallel, not a behavior regression:
- **#450** made the canonical `fabro_types::Message` omit absent
optional
fields (`name`, `tool_call_id`) via `#[serde(skip_serializing_if =
"Option::is_none")]`,
to match the OpenAPI completions wire contract.
- **#471** added the per-dialect wire snapshots in parallel, authored
against the older shape that emitted explicit `"name": null` /
`"tool_call_id": null`.
Each PR was green on its own branch (#450 never contained #471's
snapshots; #471 predated #450's serde change). They only collided once
both sat on `main` together — and because the serde attribute and the
snapshots live in different files, there was no textual git conflict to
flag it at merge time.
## What changed
Regenerated the 33 affected snapshots (anthropic / gemini /
openai_compatible / openai_responses) via `cargo insta accept`. The
**only** change in every snapshot is the removal of the two trailing
null fields:
```diff
- ],
- "name": null,
- "tool_call_id": null
+ ]
```
No decode/stream behavior changed; the new shape is the intended
canonical serialization.
## Test plan
- [x] `cargo nextest run -p fabro-llm` — 515 passed, 0 failed
- [x] Verified the diff across all 33 snapshots is uniformly the
null-field omission (plus the `],`→`]` reflow), nothing else
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Adds a new `/playground` route where users build a Fabro workflow by
chatting with Ask Fabro on the right while watching a live canvas
re-render on the left. The workflow can be downloaded as a `.fabro.zip`
or — eventually — launched as a real Fabro run; today the "Run for
real" button POSTs to `/api/v1/runs` and redirects to the resulting
`/runs/{id}` page, with a placeholder project/repo/folder picker.
The feature is built as a standalone component subtree under
`apps/fabro-web/app/components/playground/` with no `AppShell` or
`react-router` dependencies, so it can be re-embedded in other contexts
later by passing `chatEndpoint`, `authMode`, and an optional
`realRunRedirect` prop.
## What changed
**Frontend (`apps/fabro-web/`)**
- New `/playground` route + `<Playground>` component tree.
- Live SVG canvas via `@viz-js/viz` with click-to-inspect (read-only
node detail panel), pan, zoom, fit-to-window, and a simulated walk
through the graph driven by a Play button.
- Docked chat sidebar (assistant-ui) wired to the new
`/api/v1/playground/chat` endpoint, with auto-retry on parse failure
and a playground-specific tool-call summary that reads
`Wrote workflow.fabro (N nodes, M edges)`.
- File tabs (`workflow.fabro` / `workflow.toml` / `README.md`),
`.fabro.zip` download via `fflate`, and a "Run for real" toolbar
button that POSTs an inline `RunManifest` to `/api/v1/runs`.
- Draft persists across page refreshes via `localStorage`.
**Backend (`lib/crates/fabro-server/`)**
- New `POST /api/v1/playground/chat` SSE endpoint. Server is stateless
across turns: each request carries the full draft, the server runs
the LLM with a single `write_workflow_file` tool, streams
`StreamEvent` frames back, and lets the client own diffing/animating
the result into the canvas.
- Request-size caps before the LLM call (50 messages, 100 nodes, 200
edges) so a misbehaving or malicious client can't drag multi-MB
transcripts through token billing.
**Spec / wire contract**
- OpenAPI: new `playground/chat` operation + four new schemas
(`CreatePlaygroundChatRequest`, `PlaygroundWorkflowDraft`,
`PlaygroundWorkflowNode`, `PlaygroundWorkflowEdge`).
- `lib/packages/fabro-api-client` not regenerated yet (the playground
uses raw `fetch`); reviewers who want the TS client to pick up the
new types can run `bun run generate` in that package.
## Key design decisions
1. **Single `write_workflow_file` tool, not six per-op tools.** The
first cut exposed `add_node`/`update_node`/`connect`/etc. as
discrete tool calls. The model would routinely add nodes without
wiring them up, leaving the canvas in a broken half-state. Pivoted
to a single tool that takes the full new `workflow.fabro` content;
the browser parses the DOT, diffs it against the local draft, and
animates the resulting reducer ops in. The model only has to "get
the file right", and the canvas still paints node-by-node thanks
to the client-side animator.
2. **Stateless server.** Each chat turn POSTs the full current draft;
nothing is persisted server-side. Keeps the endpoint cheap, makes
refresh-resumption trivial (browser owns the truth), and means the
same endpoint can later sit behind a rate-limited anonymous variant
without growing per-session state.
3. **Standalone component subtree.** `<Playground>` has no
`AppShell`/router/store dependencies. All cross-cutting concerns
flow in as props (`chatEndpoint`, `authMode`, `realRunRedirect`).
This is the structural hook that makes future re-embedding possible
without a refactor.
4. **Chat is the only mutation path.** Click-to-inspect on the canvas
is read-only. Bi-directional canvas editing was explicitly cut from
scope to keep one source of truth for "how the workflow changed."
5. **Inline `RunManifest` instead of temp-dir-then-clone.** The
playground has no project to run against, so the `Run for real`
modal builds a `RunManifest` that carries the full DOT and
`workflow.toml` source inline (`workflows[key].{source, config}`).
`cwd` is pinned to a fixed `/tmp/fabro-playground` constant — no
LLM-controlled segment in a filesystem-looking field.
6. **React effects policy compliance.** All `useEffect` calls in
playground component code go through the existing primitives in
`app/hooks/effects.ts` (`useDocumentEvent`, `useInterval`) or a
purpose-named hook (`useCanvasRender`).
## Still outstanding (planned follow-ups)
- [ ] **Actually kicking off the ad-hoc run.** "Run for real" today
POSTs a manifest with a placeholder project/repo/folder
fieldset. The intent is to reuse the project-picker pattern
being introduced on the in-flight automations branch — once
that pattern lands, the disabled inputs in
`run-for-real-modal.tsx` become the live surface.
- [ ] **Header link to `/playground`.** No nav entry yet; users have
to type the URL directly.
- [ ] **Live SSE-driven canvas overlay** via
`GET /api/v1/runs/{id}/attach` — currently the modal redirects
to the standard run-view page; the "watch it build on the
playground canvas" experience comes when the `stage.*` events
are wired through.
- [ ] **Regenerate `lib/packages/fabro-api-client`** so the new types
ship to TS consumers.
- [ ] **Smoke test:** end-to-end download → unzip →
`fabro run <name>` round-trip.
- [ ] **`scripts/build.ts` dist-symlink bug:** `pruneOldBuilds` can
delete the directory `apps/fabro-web/dist` points at, which
pins the dev server in 503 "build in progress" forever.
Workaround documented; the real fix is a separate PR.
## Test plan
- [ ] `cd apps/fabro-web && bun run test app/components/playground/` —
111 tests pass
- [ ] `cd apps/fabro-web && bun run typecheck` — clean
- [ ] `cargo test -p fabro-server playground` — 6 tests pass
- [ ] Visit `/playground`; the canvas renders the welcome `start → ??? →
exit` ghost.
- [ ] Type "build me a release-notes workflow" in chat; nodes/edges
animate in; ack reads `Wrote workflow.fabro (N nodes, M edges)`.
- [ ] Click a node → inspector panel populates; click empty canvas →
deselects.
- [ ] Click `Simulate`; nodes light up `start → ... → exit` along the
resolved path.
- [ ] Click `Download .fabro`; unzip; `cd <unzipped> && fabro run
<name>` runs locally.
- [ ] Click `Run for real` → modal opens → confirm → POST succeeds →
redirected to `/runs/{id}` → run executes.
- [ ] Refresh the page; the draft persists from localStorage.
- [ ] Click `Start over` → `Yes`; canvas resets to welcome state.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## What
Threads the configured provider name through the response and error
paths so custom-named providers report their real identity. Previously
several sites used hardcoded literals:
- streamed `Response.provider` always said `"anthropic"` / `"openai"` /
`"gemini"` regardless of the configured name;
- OpenAI's **non-stream** `Response.provider` ignored `with_name`
entirely;
- `ProviderErrorDetail.provider` in error paths (stream error events,
HTTP/gRPC status mapping, request error contexts) carried the same
literals.
Now the name flows through anthropic's `StreamAccumulator`, openai's
`SseStreamState` / error-json mapper / complete + error paths, and
gemini's stream state and error helpers.
## Why
One adapter code path already serves multiple providers — e.g. Kimi runs
through the anthropic adapter via `with_name`, and the seven compat
providers share one adapter. The "this file == this provider" assumption
baked into the literals is wrong for those routes: a Kimi request that
429s reported `"Server error from anthropic"`, and its usage/error
records were misattributed. The fix was also inconsistent before this
change — some paths already used `provider_name` while the stream paths
didn't, so the same request could be attributed differently depending on
whether it streamed.
This is foundational for an upcoming gateway refactor that makes
codec/transport/provider orthogonal, where identity must travel with the
route as data rather than being hardcoded per adapter.
## Behavior change
The one intentional, behavior-visible delta: **custom-named providers**
now report their configured name in `Response.provider` and
`ProviderErrorDetail.provider`. Built-in default-named providers are
byte-identical — the wire snapshot suite from #471 passes unmodified.
Failover/retry policy keys on `ProviderErrorKind` and the `retryable()`
/ `failover_eligible()` flags, never on the provider string, so the
error-detail change is display/log/signature-only (confirmed by a
consumer sweep).
## Tests
New per-dialect `custom_named_*` wire tests pin the intentional deltas,
including a capture of the Kimi-over-anthropic route shape (bearer auth,
no `anthropic-version` header) — useful as a pin for the route-config
work later in the refactor.
- `cargo nextest run -p fabro-llm` — 515 passed
- `cargo +nightly fmt --check` / `clippy --all-targets -- -D warnings` —
clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What
Adds wire snapshot tests pinning the exact encode/decode/stream behavior
of all four provider adapters — **109 tests / 117 insta snapshots** in
`fabro-llm/tests/it/wire/{anthropic,openai_compatible,openai_responses,gemini}.rs`,
driven by a shared canonical request corpus in `tests/it/support.rs`.
Tests only; no `src/` changes.
Each test points a real adapter at a local httpmock server,
side-channels the full received request (method, path, headers, body)
out of an `is_true` matcher closure, responds with a canned provider
body or scripted SSE transcript, and snapshots both the captured wire
request and the decoded canonical `Response` / `Vec<StreamEvent>`.
## Why
This is the behavior-pinning net for an upcoming refactor series that
separates fabro-llm's wire translation (codec) from transport/auth
concerns. The refactor must be behavior-preserving; these snapshots make
that checkable per PR instead of asserted. The anthropic and gemini
dialects have no twin coverage, so these tests are the only net for
those paths.
httpmock matcher-capture is used for all four dialects (rather than twin
request-logs for the OpenAI ones): the corpus deliberately exercises
shapes a strict twin would reject (provider_options merges,
response_format variants, bad-file-path attachment parts), one mechanism
is cheaper to maintain than two, and the twin already validates the
OpenAI dialects via `parity_matrix` and the server scenario tests.
## Coverage
Per dialect:
- **Encode** — multi-turn/system mapping, `tool_choice` ×4, tool
round-trips (incl. error results), thinking round-trips, attachments
(inline data, URL passthrough, silent bad-file-path drop, audio
fallback), `response_format` (json + json_schema), sampling params,
per-dialect `provider_options` merges (incl. the adapter-name-keyed
compat case), catalog-driven reasoning effort and prompt cache (beta
header), and the count-tokens wire route.
- **Decode** — finish-reason mappings, each dialect's distinct usage
arithmetic (anthropic direct cache reads with `reasoning_tokens: 0`;
openai-responses cached/reasoning subtraction; gemini `(prompt − cached)
+ tool_use_prompt`; compat prompt/completion only), thinking/tool/opaque
items, dual-id (`fc_…`/`call_…`) preservation.
- **Stream** — tool-call and reasoning deltas, error events (pinning
`retryable`/`failover_eligible`), and each dialect's stream-end
contract: anthropic emits no `Finish` without `message_stop`;
openai_compatible synthesizes one only if content started (both halves
of the minimax tolerance pinned); gemini synthesizes unconditionally.
Notable current behaviors pinned as-is (documented divergences, not
changed here): `ToolResult.image_data` is dropped by every encoder;
`ToolChoice::None` drops the whole `tools` array on anthropic only;
canonical `Thinking` parts are dropped by openai-responses/gemini;
`Request.metadata` is dropped by compat/gemini; gemini ignores
`reasoning_effort` and mints synthetic UUID tool-call/response ids
(normalized to `[UUID]` in snapshots).
## Test plan
- `cargo nextest run -p fabro-llm` — 498 passed (new `it` target run
twice to verify snapshot determinism incl. UUID normalization)
- `cargo +nightly fmt --check --all` / `cargo +nightly clippy -p
fabro-llm --all-targets -- -D warnings` — clean
- `fabro-llm/tests/integration.rs` and
`fabro-agent/tests/it/parity_matrix.rs` untouched
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Limit DOT templates to prompt + goal
Part of unifying config interpolation in Fabro. Per the field taxonomy,
full
MiniJinja templates (`ImportableTemplate`) should be limited to
**`prompt`
(node) and `goal` (graph)** — the content fields that legitimately need
`{{ inputs.* }}` / `{{ goal }}`. Every other graph/node/edge attribute
should
be a plain value, not a Turing-complete template.
This is a **behavior-reducing** slice and is **independent of the
InterpString
foundation PR** (it touches the MiniJinja/template-engine path, not the
`InterpString` config path), so it branches off `main` and can be
reviewed on
its own.
## What changes
- `TemplateTransform::render_attrs` still renders node `prompt`
(unchanged) and
the graph `goal` (rendered separately, as before), but **no longer
renders**
`label`, `model`, `provider`, `speed`, edge `label`, or `condition`.
Those
are left as literal text.
- When a now-demoted attribute still contains `{{ … }}` / `{% … %}`, a
`detemplated_attribute` **warning** is emitted so authors can migrate
(the
syntax is now literal, not rendered).
- `condition` keeps its dedicated routing-expression evaluator
(`evaluate_condition` / `parse_condition_expr`); only the Jinja
pre-render is
removed, so routing still works exactly as before.
- `output_schema` becomes a string-or-`@file` value, not a template:
`FileInliningTransform` still resolves an `@file` reference but loads
its
contents **verbatim**, and neither the inline value nor the loaded file
is
MiniJinja-rendered.
`prompt` and `goal` are unaffected — both inline and `@file` forms are
still
MiniJinja-rendered (the `@` only selects whether the template is in-band
or
loaded from a file).
## Behavior change
`{{ … }}` in a demoted attribute (`label`/`model`/`provider`/`speed`/
`condition`/`output_schema`) is now **literal text** instead of being
rendered.
A parse-time `detemplated_attribute` warning flags any remaining
occurrences so
they're not silently dropped. This was rarely a sensible thing to do
anyway
(e.g. `label = "{{ goal }}"` would splat the entire goal into a short
display
label).
## Verification
- `cargo build --workspace`
- `cargo nextest run -p fabro-workflow` → 1164 passed
- `cargo +nightly fmt --check --all`
- `cargo +nightly clippy --workspace --all-targets -- -D warnings` →
clean
- No pending `insta` snapshots
## Tests
- `template_transform_renders_prompt_and_leaves_other_attrs_literal` —
`prompt`
still renders; node/graph/edge `label` stay literal; one migration
warning per
demoted label.
- `file_inlining_transform_does_not_render_templates_in_output_schema`
and
`file_inlining_transform_loads_output_schema_file_verbatim` —
`output_schema`
inline and `@file` contents are used verbatim, no Jinja.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The automations page collapsed loading, no-automations, and no-search-
match into a single branch that always rendered `No automations match
"{query}"`. With an empty query that read `No automations match ""`,
which also flashed during the initial fetch and when the trigger filter
(not the search) excluded everything.
Split into loading / error / true-empty / no-match states using the
shared EmptyState/ErrorState/LoadingState components. The true-empty
state is now a "Create your first automation" panel with a primary CTA,
and the search/filter toolbar is hidden when there is nothing to filter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an "Allow local sandboxes" checkbox (checked by default) below the
Docker/Daytona choice in the web installer, and stop unconditionally
enabling all three providers when generating settings.toml. The wizard
now enables only the selected runtime plus local when allowed; the
unselected runtime is written as `enabled = false` so the config
resolver does not default it back on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emit the policy via Content-Security-Policy-Report-Only instead of the
enforcing header so browsers log violations without blocking resources
while we debug remaining CSP issues. The policy string is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Permit GitHub App manifest form posts and signed HTTPS VNC preview iframes while keeping the rest of the SPA CSP locked down. Mirror the policy in the split-web Caddy config.
Group Variables and Secrets under a new "Workflows" nav section between
General and Administration, and move Security under Administration next to
Server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Enforce Cargo lockfile use across CI and release automation so jobs fail
on stale `Cargo.lock` state instead of resolving dependencies
implicitly. This adds `--locked` to Rust CI, release builds/tests,
nightly release tagging, the TypeScript workflow's embedded Rust build,
and helper-owned Cargo calls in `fabro-dev`.
This also fixes the Linux CI flake exposed by the PR: canceling a
durably blocked in-process run could take the abort path while the
workflow was still unwinding a human-input gate, causing
`run.failed(cancelled)` to be followed by `run.unblocked`. That invalid
event order broke projection rebuilds and made `GET /runs/{id}` return
404. Cancellation now uses the durable lifecycle status when selecting
the in-process blocked-run path, so the pending interview is cancelled
before the terminal event is emitted.
The release command's intentional `cargo update --workspace` step is
unchanged, because that step updates `Cargo.lock` after bumping the
workspace version.
## Testing
- `cargo nextest run --locked -p fabro-dev --features dev -E
'test(dry_run_computes_stable_version_from_date) |
test(dry_run_prints_equivalent_build_commands)'`
- `cargo --locked dev release --dry-run --skip-tests --release-date
2026-01-01`
- `cargo --locked dev docs check`
- `cargo nextest run --locked -p fabro-server --features test-support
cancel_durably_blocked_in_process_run_cancels_pending_interview_without_abort_signal
--status-level fail --final-status-level fail --show-progress none`
- `cargo nextest run --locked -p fabro-server --features test-support
--test it scenario::lifecycle --profile ci --status-level fail
--final-status-level fail --show-progress none --no-fail-fast`
- Linux Docker stress reproduction: `cargo nextest run --locked -p
fabro-server --features test-support --test it
scenario::lifecycle::full_http_lifecycle_cancel --profile ci
--stress-count 200 --status-level fail --final-status-level fail
--show-progress none`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-dev --features dev
--all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-server --features
test-support --all-targets -- -D warnings`
- `git diff --check`
Full `cargo nextest run --locked -p fabro-dev --features dev` currently
has two unrelated policy-test failures:
`policy::catalog_builtin_references_stay_in_allowlist` and
`policy::workflow_template_rendering_call_sites_stay_in_allowlist`.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Release Repro <release-repro@example.com>
The slug cannot be changed after creation, so showing it as a read-only
row on the edit page added noise without value. Keep the editable slug
input on the create page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The search input used text-sm (20px line-height) while the filter
buttons use text-xs (16px), both with py-2, making the input 4px
taller. Trim the input to py-1.5 so it matches the buttons' 34px
height without resizing the shared filter button components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The manifest bundler collects Dockerfile path references from both the
named-environment catalog and [run.environment], but the server-side
resolver only inlined [run.environment.image]. A Dockerfile declared
under [environments.<slug>.image] therefore reached the Daytona provider
as an un-inlined Path and tripped its guard ("dockerfile path should have
been resolved to inline content before sandbox creation"), so no run
could use a catalog-defined Dockerfile environment.
Walk layer.environments alongside run.environment when resolving manifest
dockerfiles, mirroring the bundler. Add a regression test proving a
catalog dockerfile path is inlined.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runs were falling back to the built-in `default` environment (a bare
daytona-medium snapshot with no Rust toolchain), so every Rust stage in
the smoke workflow failed with exit 127 (cargo/rustc not found). The old
[run.sandbox] config that built a custom snapshot was dropped in the
move to named environments (#360) and never ported.
Add a `fabro-dev` named environment that builds the Daytona snapshot from
.fabro/Dockerfile (Rust + nightly-2026-04-14 + cargo-nextest + bun), with
8 CPU / 16GB RAM, and select it via [run.environment].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the toolbar on /runs?view=list so the runs section under
/automations/:id supports the same client-side filters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The list card linked to the singular /automation/:id, which mismatched
the rest of the new automations CRUD surface (/automations,
/automations/new, /automations/:id/edit). Switch the card link and the
slug-preview text on the create form to the plural form, and mount
/automations/:id in the router alongside the existing singular route
(kept as a back-compat alias for any older bookmarks).
Drive-by: fold two adjacent `use super::*` imports into one and reflow
a long `if let` line in the automations handler (linter cleanup; no
behavior change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace Sonner's default richColors palette with a Fabro-themed
FabroToaster: dark panel surface, accent-colored Heroicons type icons
(coral error, mint success, teal info, amber warning), and a themed
close button so persistent error toasts can be dismissed. Extract the
shared config out of the two duplicated <Toaster> mount points.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Materializing an automation run cloned the full repo fresh into a
tempdir on every click — 5–15s of git activity on the HTTP request
thread, paid in full for every run, then thrown away.
Add a per-`(owner, repo)` bare-clone cache under
`<Storage::cache_dir>/automation-repos/<owner>/<repo>.git`, and replace
the per-call clone+fetch+checkout dance with:
1. `KeyedMutex` lock on `(owner, repo)` so concurrent calls serialize
per repo and parallelize across repos.
2. If the bare clone is missing, `git clone --bare --depth 1`. Otherwise
`git worktree prune` to clean up any admin entries leaked by previous
`TempDir` drops.
3. `git fetch --depth 1 origin <ref>` against the bare clone.
4. `git rev-parse FETCH_HEAD` for the SHA.
5. `git worktree add --detach --force <temp>/repo FETCH_HEAD` into the
per-call scratch dir, then build the manifest as today.
First run for a repo still pays the clone cost. Every subsequent run
for any ref or automation against that repo pays only the fetch delta
plus a near-free worktree add (~100–500ms).
Corruption recovery: if the bare clone's `HEAD` file is missing or
zero-length after a failure, the cache wipes the directory and retries
once before surfacing `CloneFailed` as before. Auth and network errors
do not trigger a wipe.
Promote `fabro_store::KeyedMutex` and its guard to `pub` so the
server can reuse the existing primitive instead of duplicating it.
Tests:
- `bare_clone_reused_across_calls` seeds a local upstream, runs
`prepare_worktree` twice, and asserts the bare clone's `objects/`
tree is identical before and after the second call (i.e., no
re-clone).
- `bare_clone_recovers_from_corruption` truncates `HEAD` between
calls and asserts the cache rebuilds and succeeds.
- The existing plan-builder argv/timeout assertions are updated to
cover the new bare-clone, bare-fetch, worktree-add, worktree-prune,
and rev-parse FETCH_HEAD plans.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the Automations area in the web UI functional end-to-end against the
real Automation API, and fix the backend so runs created by an automation's
API trigger actually start instead of sitting in Submitted forever.
Web:
- Reveal the Automations nav tab outside demo mode; drop the now-empty
demoOnly mechanism.
- List page: render via listAutomations (was workflows mock data); wire
ellipsis menu to Edit and Delete, with ConfirmDialog + If-Match revision.
Move Create Automation into the toolbar, switch the trigger select to a
shared FilterButton, hide the redundant page-header title via a new
hideTitle handle flag.
- Play button on each card fires createAutomationRun with spinner + toast
and navigates to the new run.
- New automation form: drop the dead Goal panel and hardcoded repository
list, post to createAutomation with real triggers.
- Edit automation: new /automations/:id/edit route reusing a shared
AutomationFormFields component, PUT via replaceAutomation with If-Match.
- Show page: rebuild like a run detail page — breadcrumb, title, chips
(enabled status, repo+ref, workflow, schedule), Edit + Run actions
(Run hits createAutomationRun), and a Runs panel using RunsListView
with URL-driven search/sort/pagination/column-picker like the Children
sub-tab. Drop the obsolete Definition/Diagram/Runs child routes.
Backend (fabro-server):
- create_automation_run now calls lifecycle::queue_run_start after the
run is persisted, so the run transitions Submitted → Runnable and the
scheduler picks it up. Logs a warn and returns the created response if
start fails (no worse than the prior always-stuck behavior).
- queue_run_start in lifecycle.rs is promoted to pub(super) so sibling
handlers can reuse it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Add `claude-opus-4-8` to the built-in Anthropic model catalog with
pricing, limits, features, and fast-mode costs.
- Move the floating `opus` and `claude-opus` aliases from Opus 4.7 to
Opus 4.8 and update the public model table.
- Remove/generalize Rust tests that were pinned to specific built-in
Opus catalog data.
## Verification
- `cargo nextest run -p fabro-model`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `target/debug/fabro --json model test --model opus` (live Anthropic
smoke; resolved to `claude-opus-4-8`)
## Summary
Fixes#435.
Preserve raw non-JSON tool-call arguments for custom/freeform tools when
using the OpenAI-compatible Chat Completions adapter. This keeps
`apply_patch` receiving the raw patch text instead of `{}` when
LiteLLM/openai-compatible providers emit Codex-style freeform patch
calls.
Also extends the OpenAI twin so black-box tests can exercise the Chat
Completions path with raw tool-call arguments.
## Test Plan
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-agent --test it
openai_compatible_twin_preserves_raw_apply_patch_arguments --run-ignored
only`
- `cargo nextest run -p fabro-llm`
- `cargo nextest run -p fabro-test`