# 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`
## Summary
Follow-up to fabro-sh/fabro#447. This keeps context compaction's
effective preserve boundary consistent between summary generation,
history mutation, and emitted telemetry so tool-call/result pairs that
remain in raw history are not also summarized.
The branch also tightens the OpenAI twin support added for this
regression: scripted usage is modeled as a single `TokenUsage`, SSE
completion payloads reuse the canonical Responses JSON shape, and
request validation now treats custom tool-call outputs as tool outputs
instead of spreading raw item-type string checks.
## Verification
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-agent compaction`
- `cargo nextest run -p twin-openai`
- `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --profile e2e
--run-ignored only --test it
openai_twin_compaction_preserves_tool_call_pairs`
- `git diff --check origin/main...HEAD`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Fixes OpenAI Responses requests after context compaction by ensuring
preserved tool results are not separated from the assistant tool calls
that produced them. The previous fixed-size preserved tail could retain
a `function_call_output` while dropping the matching `function_call`,
which OpenAI rejects as an orphaned tool result.
## Changes
- Extends `History::compact` so the preserved range moves backward until
every kept tool result has its matching assistant tool call.
- Adds a unit invariant test for compacted histories that serialize tool
results.
- Adds an OpenAI twin integration regression that forces compaction
during a tool-use loop.
- Teaches the OpenAI twin to validate orphaned `function_call_output`
items and script response usage counts for deterministic compaction
tests.
## Test Plan
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --test it
openai_twin_compaction_preserves_tool_call_pairs --run-ignored all`
- `cargo nextest run -p fabro-agent`
- `cargo nextest run -p twin-openai`
- `cargo nextest run -p fabro-test`
- `cargo +nightly-2026-04-14 clippy -p fabro-agent -p fabro-test -p
twin-openai --all-targets --no-deps -- -D warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
Drop flex-1 from the columns row when the landing empty state is
showing so the row sizes to the column headers and the empty state
sits directly beneath them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Adds a standalone Docker Compose proof that runs the Fabro Rust API, a
Caddy static SPA server, and a Caddy edge proxy as separate services.
This demonstrates split web asset serving while keeping `/api/*`,
`/auth/*`, and `/health` same-origin with the API server.
## Changes
- Adds `docker-compose.split-web.yaml` with private `fabro-api` and
`fabro-web` services behind an exposed `edge` proxy on port 8080.
- Adds Caddy edge routing that sends `/api/*`, `/auth/*`, and `/health`
to Rust, while everything else goes to the static web service.
- Adds a static Caddy config for `apps/fabro-web/dist` with SPA
fallback, source-map blocking, security headers, immutable asset
caching, and `X-Fabro-PoC-Upstream` route-proof headers.
- Adds PoC server settings and a README with build, run, and validation
commands.
## Verification
- `cargo dev docker-build --tag fabro-sh/fabro:split-web-poc`
- `docker compose -f docker-compose.split-web.yaml up -d`
- `docker compose -f docker-compose.split-web.yaml ps`
- `curl` checks for `/runs`, `/assets/app.css`, `/assets/app.css.map`,
`/api/v1/health`, `/api/v1/auth/config`, `/auth/login/dev-token`,
`/api/v1/auth/me`, and `/api/v1/attach`
- Browser login flow via `browser-use`: loaded `/login`, submitted the
dev token, and landed on the authenticated Runs screen
- `docker compose -f docker-compose.split-web.yaml config`
- `caddy validate` for both Caddyfiles
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)