Commit graph

689 commits

Author SHA1 Message Date
fabro-sh-0530[bot]
f73f2a53f3
Replace queued with pending/runnable and add approval flow (web + API s… (#371)
## Summary

Replaces the single `queued` pre-execution state with explicit `pending`
and `runnable` states, and wires approve/deny actions for
parent-generated child runs that require human approval before they can
execute. This diff covers the web UI and OpenAPI spec layers of that
change.

## What changed

**Run status model**
- `queued` is removed from all TypeScript types, display maps, column
routing, and tests.
- `pending` (awaiting approval) and `runnable` (eligible for the
scheduler) replace it as distinct board columns and `RunStatus` variants
with their own labels and colors (`runnable` gets cyan; `pending` stays
muted).

**Approval actions**
- New `approveRun` / `denyRun` API calls in `run-actions.ts` invoke the
new `POST /runs/{id}/approve` and `POST /runs/{id}/deny` endpoints.
- `canApprove` predicate requires both `status.kind === "pending"` and
`lifecycle.approval?.state === "pending"` — a run whose status is
pending but has no approval record does not expose the action.
- `useApproveRun` / `useDenyRun` mutations in `mutations.ts` follow the
same pattern as `useCancelRun`.
- `ActionsMenu` in `run-detail.tsx` gains Approve (lifecycle group) and
Deny (destructive group) menu items.

**Board and event plumbing**
- `columnForStatus` now routes `pending → pending column` and `runnable
→ runnable column`; `submitted` stays in the pending column.
- `BOARD_STATUS_EVENTS` and `RUN_SUMMARY_EVENTS` replace `run.queued`
with `run.start_requested`, `run.pending`, `run.approved`, `run.denied`,
and `run.runnable`.
- The `pending` column is hidden when empty (same behaviour the old
`queued` column had).

**Waterfall phases (`run-phases.ts`)**
- `queued` phase is removed; `pending` and `runnable` phases are added
in order.
- The submitted phase closes at `run.start_requested` rather than
`run.queued`.
- Each phase derives its timestamps from its own event rather than a
single `firstTs` lookup, making multi-phase pre-execution timelines
accurate.

**OpenAPI spec**
- `POST /api/v1/runs/{id}/approve` and `POST /api/v1/runs/{id}/deny`
endpoints added with 200/404/409 responses.
- `startRun` description updated to describe the pending/runnable
branching behaviour.
- `cancelRun` description updated to reference `pending`/`runnable`
instead of `queued`.

### Plan Summary

- **Task 3** (OpenAPI schema additions for approve/deny endpoints) —
complete in this diff.
- **Task 6** (Web UI surfaces: board columns, run-detail actions,
waterfall phases, event subscriptions) — complete in this diff.
- **Task 7** (doc cleanup: references to `queued` replaced in plans,
brainstorms, and QA docs) — complete in this diff.


### Fabro Details

<details>
<summary>Ran 9 stages in 127m 37s for $104.98</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 29s | – | 0 |
| implement | 92m 10s | $91.53 | 0 |
| simplify_opus | 18m 35s | $10.65 | 0 |
| simplify_gpt | 7m 36s | $2.81 | 0 |
| verify | 3m 42s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **127m 37s** | **$104.98** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro <fabro@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 15:34:33 -04:00
Bryan Helmkamp
fd63f4b523
feat(api): expose run size buckets (#369)
## Summary

Adds a stable `Run.size` API field so clients can bucket workflow runs
by current best-effort billed usage without introducing a separate
cost-estimation system. The field uses the `RunSize` enum and serializes
as uppercase `XS`, `S`, `M`, `L`, or `XL`.

## Changes

- Derives run size from terminal billed totals when available, otherwise
from the existing projected stage usage while a run is still active.
- Exposes `size` on `Run` in the OpenAPI contract and regenerated
TypeScript client.
- Preserves existing `Run.billing` behavior so live/provisional usage
only affects `size`, not the nullable billing summary.

## Verification

- `cargo nextest run -p fabro-types run_size`
- `cargo nextest run -p fabro-store
summary_size_tracks_current_projected_usage_before_terminal_conclusion`
- `cargo nextest run -p fabro-api
run_summary_json_matches_openapi_shape`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd apps/fabro-web && bun run typecheck`
- `git diff --check`
- `cargo build --workspace`

---

[![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-23 14:41:41 -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]
7d8506cb7c
feat: add manual retry for failed and dead runs (#362)
## Summary

Adds a **Retry** action that creates a fresh run from a failed or dead
run's captured durable definition. The new run gets a new ID, records
`retried_from: <source_run_id>`, and is immediately queued. The source
run is left entirely unchanged.

### Plan Summary

- `retried_from: Option<RunId>` added to `RunCreatedProps`,
`RunProjection`, and `Run` (API surface); defaults to `null` for
backward compat with legacy events.
- `POST /api/v1/runs/{id}/retry` → `201` with the new run; eligible
source states are `failed` (except `reason=cancelled`) and `dead`.
- New `retry_run` workflow operation (modeled after `fork`) creates a
new run store, appends `run.created` with `retried_from` set, then
`run.submitted`. No checkpoints, sandbox state, PR links, questions, or
conclusions are copied.
- `start_run` handler refactored into a reusable `queue_run_start(state,
id, resume)` helper so retry can queue the new run through the same
path.
- Web: `canRetry` predicate, `useRetryRun` mutation, **Retry** menu item
in `ActionsMenu`, "Retried from" link in `RunSummaryPanelView`, and
navigation to the new run on success (deduplicated via
`lastProcessed.retry`).
- OpenAPI spec updated; generated Rust and TypeScript client types
regenerated.

## Key design decisions

**No runtime state is copied.** Only the durable definition fields
(`graph`, `settings`, `labels`, `git`, `manifest_blob`,
`definition_blob`, `fork_source_ref`, `parent_id`, `title`) are
forwarded to the new `RunCreated` event. Checkpoints, sandbox, billing,
PR links, and pending controls are left in the source run.

**`queue_run_start` extraction.** `start_run` was restructured to
extract a `queue_run_start(state, id, resume) -> Result<(), ApiError>`
helper, so the retry handler can reuse the exact same queueing path
without duplicating logic.

**`lastProcessed.retry` deduplication.** The UI effect that fires on
`retryMutation.data` checks `state.lastProcessed.retry === result`
before navigating, so React StrictMode double-invocation or re-renders
won't push duplicate navigations.

**Demo mode guard.** `canRetry` is gated by `!demoMode` at the call site
so the button is hidden in demo mode and never navigates to a missing
demo run.


### Fabro Details

<details>
<summary>Ran 9 stages in 65m 2s for $51.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 53s | – | 0 |
| preflight_lint | 2m 8s | – | 0 |
| implement | 36m 51s | $36.06 | 0 |
| simplify_opus | 16m 22s | $14.46 | 0 |
| simplify_gpt | 3m 10s | $1.18 | 0 |
| verify | 3m 39s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **65m 2s** | **$51.70** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 14:07:24 -04:00
fabro-sh-0530[bot]
0f36304d2c
Add small_default model role and async generated run titles (#368)
## Summary

Introduces a `small_default` catalog role for identifying each
provider's small/cheap utility model, and uses that model to
asynchronously generate human-readable run titles when the caller
doesn't supply one explicitly.

### Plan Summary

- **Catalog**: Add `small_default: Option<bool>` to
`ModelCatalogSettings` and `small_default: bool` to the `Model` type.
Mark built-in small defaults: `claude-haiku-4-5` (Anthropic),
`gpt-5.4-mini` (OpenAI), `gemini-3.1-flash-lite-preview` (Gemini).
Validate that each provider has at most one small default; zero is
allowed with fallback to the provider's regular default.
- **Helpers**: Add `small_default_for_provider` and
`small_default_for_configured_ids` on `Catalog`, mirroring the existing
`default_for_provider` / `default_for_configured_ids` /
`probe_for_provider` pattern.
- **Title generation**: New `run_title_generation` module in
`fabro-server` builds a prompt from workflow identity, goal, and raw run
inputs, calls `generate_object` with `max_tokens(64)` and a 10 s
timeout, normalizes output (trim, reject blank/control, truncate to 100
chars), and falls back to the deterministic title on any failure.
- **Server integration**: In the create-run handler, if no explicit
`RunManifest.title` was supplied and at least one LLM provider is ready,
spawn a detached task that generates a title and appends
`run.title.updated` — but only if the title hasn't been changed by a
concurrent user PATCH.

## What changed and why

**`small_default` vs `default`** — the existing `default` role drives
normal model selection for workflow execution and must not be disturbed.
`small_default` is a separate, additive role for lightweight metadata
work. The two roles are intentionally independent so teams can promote a
newer large model to `default` without accidentally routing title
generation there.

**Best-effort, async title enrichment** — run creation is kept
synchronous and reliable. The title task is fire-and-forget: LLM errors,
timeouts, and validation failures all silently leave the deterministic
title in place. The stale-title guard (`current.title !=
deterministic_title`) prevents the async task from clobbering a
concurrent user edit via `PATCH /runs/{id}`.

**No redaction** — per the design goal, raw input values are forwarded
to the model. This is noted explicitly in the prompt and in the module
docs.

**Prompt size bounding** — each of the three prompt sections (workflow
identity, run inputs, workflow summary) is independently capped at 4 000
characters with a `...[truncated]` marker so pathological inputs can't
produce enormous requests.

## Public interface changes

- `Model` gains `small_default: bool` in the Rust type, OpenAPI schema,
and generated TypeScript client.
- `MAX_RUN_TITLE_CHARS` is now `pub` in `fabro-types` so the
title-generation module can reuse the same limit.
- Config docs (`models.mdx`, `litellm.mdx`) document `small_default =
true` alongside `default` and `probe`.


### Fabro Details

<details>
<summary>Ran 9 stages in 61m 23s for $32.17</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 52s | – | 0 |
| preflight_lint | 2m 5s | – | 0 |
| implement | 29m 16s | $23.26 | 0 |
| simplify_opus | 17m 15s | $6.28 | 0 |
| simplify_gpt | 7m 1s | $2.63 | 0 |
| verify | 3m 7s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **61m 23s** | **$32.17** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 13:48:39 -04:00
fabro-sh-0530[bot]
caba862a33
Show live timing for in-flight runs via read-time overlay (#361)
## Summary

`Run.timing` was only populated for terminal runs, so the duration chip
and popover were hidden for every queued, running, or blocked run. This
change derives a best-effort `RunTiming` at cache read time for
started-but-not-terminal runs, making the duration chip appear and tick
for in-flight runs everywhere the UI consumes `summary.timing`.

### Plan Summary

- Add `RunProjection::live_run_timing(now)` in `fabro-types`: wall time
from `start.start_time → now`; active time summed from completed stages'
`StageTiming`.
- Add `apply_read_overlays(entry, now)` in `projection_cache.rs`: called
after mutex release on cloned entries; only fills `timing` when it is
`None` (terminal runs are unaffected).
- Thread `now: DateTime<Utc>` through `get_summary` / `list` /
`list_cached_runs` / `list_runs` / `list_runs_with_projection` in
`slate/mod.rs` and `projection_cache.rs`.
- Propagate `Utc::now()` at all HTTP handler and background-task call
sites in `fabro-server` and `fabro-workflow`.
- Add unit tests covering the three key cases: not-yet-started (returns
`None`), in-flight with completed stages, and a terminal run whose live
derivation matches `Conclusion.timing`.

### Key design decisions

**Overlay happens outside the cache mutex on a cloned copy.** The cached
`CachedRunProjection.summary.timing` is never mutated; only the cloned
value returned to callers gets the overlay. This means `get_cached_run`
(raw cache access, no `now`) still returns `None` for in-flight timing —
confirmed by the new integration test
`cached_summary_overlays_live_timing_without_mutating_cached_snapshot`.

**Known limitation — active time steps, not ticks.** `StageProjection`
records inference/tool time only at stage completion, so
`active_time_ms` reflects the sum of *completed* stages and jumps
forward when a stage finishes. `wall_time_ms` advances continuously.
This is intentional and documented in the code comment; live per-stage
inference tracking is out of scope.

**`build_summary` and `Conclusion.timing` are untouched.** All five
internal readers that want "timing as of conclusion" continue to use
`Conclusion.timing` directly; no test churn from the existing call
sites.


### Fabro Details

<details>
<summary>Ran 9 stages in 38m 28s for $10.35</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 20s | – | 0 |
| implement | 11m 27s | $6.21 | 0 |
| simplify_opus | 13m 12s | $2.48 | 0 |
| simplify_gpt | 4m 20s | $1.66 | 0 |
| verify | 4m 9s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **38m 28s** | **$10.35** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 13:31:28 -04:00
fabro-sh-0530[bot]
7e6052bec3
Surface reasoning_effort + speed in stage badge end-to-end (#363)
## Summary

The run page stage badge showed only the model name. This PR plumbs
`reasoning_effort` and `speed` from the LLM call site all the way
through the event stream, store projection, API, and UI so the badge now
renders `gpt-5.5 · high`.

### Plan Summary

- **Event props** — `AgentSessionActivatedProps` and `StagePromptProps`
gain `reasoning_effort: Option<ReasoningEffort>` and `speed:
Option<Speed>` with `serde(default, skip_serializing_if)` for
back-compat.
- **Typed projection** — `provider_used: Option<serde_json::Value>` is
replaced by `Option<StageModelUsage>`, a proper struct in `fabro-types`
with factory methods (`from_prompt_props`,
`from_agent_session_activated`). The freeform JSON bag is gone.
- **Emission sites** — `ActivationLeaseOptions` carries the new fields;
`emit_stage_prompt()` (new shared helper) resolves
`EffectiveRequestControls` via the backend and stamps them on
`Event::Prompt`. `AgentHandler` and `PromptHandler` both call this
helper instead of building the event inline.
- **ACP path** — `AgentAcpStarted` no longer writes `provider_used`; the
canonical source is the later `AgentSessionActivated` event, which is
already emitted for ACP steering sessions. Runs without a hub
legitimately leave `provider_used` unset.
- **OpenAPI** — new `StageModelUsage` and `ReasoningEffort` schemas
replace the `object | null` bag; `build.rs` maps both to the canonical
Rust types; a new `stage_model_usage_round_trip` integration test
enforces the parity requirement.
- **UI** — `extractStageModel` (event-scanning heuristic) is deleted;
replaced by `formatStageModelUsageLabel` and `stageModelUsageTitle` that
read directly off `selectedStage.providerUsed`. `parseFanInOutcome` now
sources the reducer model from `stage.prompt` instead of
`prompt.completed`.

### Key design decisions

**No type sprawl**: `fabro_model::ReasoningEffort` and `Speed` are
reused verbatim via `with_replacement` in `build.rs` — no parallel
enums.

**ACP behavior change**: previously `AgentAcpStarted` wrote a bespoke
`provider_used` blob and a later `AgentSessionActivated` would be
ignored for ACP sessions. Now `AgentSessionActivated` is the single
write path for all modes; ACP runs that never activate a steering hub
correctly leave `provider_used = null`. The integration test (`acp.rs`)
is updated to assert the new shape, and the unit test is renamed
`agent_acp_started_alone_leaves_stage_provider_used_unset` to document
intent.

**`emit_stage_prompt` helper**: both `AgentHandler` and the existing
prompt path share one function to avoid the two call sites drifting
apart again.


### Fabro Details

<details>
<summary>Ran 9 stages in 98m 44s for $65.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 30s | – | 0 |
| implement | 42m 19s | $26.85 | 0 |
| simplify_opus | 38m 3s | $35.17 | 0 |
| simplify_gpt | 9m 20s | $3.68 | 0 |
| verify | 3m 38s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **98m 44s** | **$65.70** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
2026-05-23 13:14:55 -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]
7f84ac5e3f
Replace run-scoped sandbox config with named environments (#360)
## Summary

Replaces the `[run.sandbox]` configuration surface with a named,
provider-explicit environment catalog. Runs now select an environment by
slug (`[run.environment] id = "..."`) rather than configuring a sandbox
inline. Fabro resolves the catalog through normal settings precedence,
applies sparse run-level overrides, and creates a concrete sandbox from
the resolved environment.

This is a clean break — no `[run.sandbox]` compatibility layer.

### Plan Summary

- **New config shape:** Top-level `[environments.<slug>]` catalog valid
in `settings.toml`, `.fabro/project.toml`, and `workflow.toml`. Runs
reference a slug via `[run.environment] id = "..."` with optional sparse
overrides under `[run.environment.*]`.
- **Unified environment fields:** `provider`, `image` (ref +
dockerfile), `resources` (cpu/memory/disk), `network` (mode + allow
CIDRs), `lifecycle` (preserve/stop_on_terminal/auto_stop), `labels`,
`volumes`, `env` — replacing the previous split between `[run.sandbox]`,
`[run.sandbox.docker]`, `[run.sandbox.daytona]`, and
`[run.sandbox.daytona.snapshot]`.
- **OpenAPI schema update:** `RunSandboxSettings`, `DockerSettings`,
`DaytonaSettings`, and `DaytonaNetworkLayer` replaced with
`RunEnvironmentSettings`, `EnvironmentSettings`, `EnvironmentProvider`,
`EnvironmentImageSettings`, `EnvironmentResourcesSettings`,
`EnvironmentNetworkSettings`, `EnvironmentLifecycleSettings`, and
`EnvironmentVolumeSettings`.
- **CLI flag rename:** `--sandbox <provider>` → `--environment <slug>`
on `run`, `create`, `preflight`, and `server start/restart`.
- **Provider capability model:** Hard errors for security properties a
provider cannot enforce (local with blocked/CIDR networking; docker with
CIDR allow-lists). Warnings for unsupported resource limits, volumes,
labels, auto-stop, and Docker Dockerfiles.
- **Docs and internal code updated** throughout: `.fabro/project.toml`,
workflow configs, all public docs, CLI args, manifest builders, and the
runner's GitHub credentials check.

### Provider mapping

| Environment field | Local | Docker | Daytona |
|---|---|---|---|
| `image.ref` | Ignored | Docker image | Snapshot name |
| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile
(requires `image.ref`) |
| `resources.cpu/memory/disk` | Warning; ignored | cpu_quota / memory
limit / warning | Snapshot sizing |
| `network.mode = block` | **Error** | `network_mode = none` | Daytona
block |
| `network.mode = cidr_allow_list` | **Error** | **Error** | Daytona
CIDR allow-list |
| `labels` | Warning; ignored | Warning; ignored | Daytona labels |
| `volumes` | Warning; ignored | Warning; ignored | Daytona volume
mounts |
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona
auto-stop interval |
| `env` | Process env overlay | Container env | Sandbox env |


### Fabro Details

<details>
<summary>Ran 11 stages in 217m 39s for $129.86</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 4m 7s | – | 0 |
| preflight_lint | 4m 9s | – | 0 |
| fix_lints | 3m 46s | $1.06 | 0 |
| implement | 76m 6s | $57.39 | 0 |
| simplify_opus | 71m 50s | $38.17 | 0 |
| simplify_gpt | 8m 27s | $2.24 | 0 |
| verify | 6m 10s | – | 0 |
| fixup | 42m 1s | $31.00 | 0 |
| fmt | 3s | – | 0 |
| **Total** | **217m 39s** | **$129.86** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
2026-05-23 13:03:21 -04:00
Bryan Helmkamp
d1cc47324d
fix(agent): use raw sandbox reads for edits
Separate raw file reads from the line-numbered display API so apply_patch and edit_file operate on unformatted UTF-8 content. Keep read_file/read_many_files model-facing output numbered and cover regressions for prefix corruption.
2026-05-22 21:26:36 -04: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-sh-0530[bot]
0754f1ca4a
Add fabro_run_get read-only run inspection tool (#358)
## Summary

Adds a new `fabro_run_get` MCP tool that returns a run's summary,
resolved ID, projection, and pending questions without any mutation
capability. This separates read-only inspection from operational
control, allowing Ask Fabro sessions to inspect runs safely without
access to write operations.

## What changed and why

**New tool (`fabro-tool/src/get.rs`):** `FabroRunGetParams` /
`ValidatedRunGet` / `RunGetResult` follow the same
validation-and-dispatch pattern as other run tools. The implementation
resolves a selector, then fans out to three read-only API calls
(retrieve run, get state, list questions) and assembles them into a
single structured result.

**Tool registry and dispatch:** `FABRO_RUN_GET_TOOL_NAME` is exported
from `common.rs` and `lib.rs`, added to `TOOL_DEFINITIONS`, wired into
the MCP stdio server (`fabro-mcp-server/src/server.rs`), and dispatched
in the LLM agent executor (`fabro-workflow/src/handler/llm/api.rs`).

**Ask Fabro access policy (`fabro-server/.../sessions.rs`):** The
session now registers and allows only `fabro_run_events` +
`fabro_run_get` via the new `ASK_FABRO_RUN_TOOL_NAMES` constant.
`fabro_run_interact` is explicitly moved to the denied set, closing off
mutation from that session type. The policy match arm is refactored from
a hardcoded `|`-chain to a slice `contains` check so the constant is the
single source of truth.

**Docs:** `mcp.mdx` now lists `fabro_run_get` as the inspection tool and
redescribes `fabro_run_interact` as control-oriented.

**Backward compatibility:** `fabro_run_interact` (including `get` and
`get_questions` actions) is unchanged and still fully operational for
contexts that allow it.

### Plan Summary

- New `get.rs` module in `fabro-tool` with validation, async fetch, and
unit tests
- Constants + schema registration in `common.rs` / `lib.rs`
- MCP server and LLM dispatch branches added for
`FABRO_RUN_GET_TOOL_NAME`
- Ask Fabro session swaps `FABRO_RUN_INTERACT_TOOL_NAME` →
`FABRO_RUN_GET_TOOL_NAME` in registry and policy
- MCP integration tests: tool count constant, schema assertions, two new
end-to-end tests
(`mcp_get_resolves_selector_and_returns_summary_projection_and_questions`,
`mcp_get_rejects_blank_run_id_before_auth_or_network`)
- Public MCP docs updated


### Fabro Details

<details>
<summary>Ran 9 stages in 43m 30s for $13.42</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 2s | – | 0 |
| preflight_lint | 2m 14s | – | 0 |
| implement | 19m 43s | $8.85 | 0 |
| simplify_opus | 5m 52s | $1.45 | 0 |
| simplify_gpt | 10m 16s | $3.12 | 0 |
| verify | 2m 53s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **43m 30s** | **$13.42** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-22 19:03:56 -04:00
Bryan Helmkamp
511371bd9a
feat(mcp): support goal files in run create
Allow fabro_run_create object specs to pass goal_file, reject goal and goal_file together, and preserve file-sourced goal semantics when building run manifests.
2026-05-22 14:27:42 -04:00
Bryan Helmkamp
c2d950da38
feat: orient Ask Fabro turns with run snapshots 2026-05-22 14:04:11 -04:00
Bryan Helmkamp
1930791802
feat(agent): gate tools by effective access policy
Expose only policy-allowed tool schemas to the model and block hidden tool calls before executor lookup. Give Ask Fabro a run-scoped read-only policy and prompt so it only advertises tools it can actually use.
2026-05-22 13:16:56 -04:00
Bryan Helmkamp
d52f3726fc
fix: hide Ask Fabro reasoning deltas 2026-05-22 12:57:30 -04:00
Bryan Helmkamp
4190e13a20
Collapsible run stage sidebar (#352)
## What

Makes the run-detail stage sidebar (shown on the Overview and Stages
tabs) collapsible with a slide animation.

- A toggle button slides the panel between full width (`w-56`) and an
icon-only rail (`w-12`), animating `width` over 300ms with the same
easing as the Ask Fabro panel.
- When collapsed, **stage status icons stay visible** — green check /
red X / spinning teal for running — so run progress is still scannable
at a glance. Workflow links (Graph Source, Run Logs, etc.) collapse to
icons too so they remain reachable.
- Labels and durations become `sr-only` with `title` tooltips for hover.
- The open/closed choice persists to `localStorage`
(`fabro:stage-sidebar-collapsed`), carrying across the Overview and
Stages tabs and reloads.

## Layout

- The collapse toggle is inline with the `STAGES` heading row (or
`WORKFLOW` when a run has no stages yet), so it doesn't push the stage
list down.
- The stage sidebar's top padding on the Stages tab was reduced (`pt-6`
→ `pt-3`) so the heading aligns with the adjacent content column and
sits closer to the tab nav.

## Notes

Self-contained in `StageSidebar` — `run-overview.tsx` and
`run-stages.tsx` render it inside flex layouts that already track its
width, so the slide works in both with no parent changes (aside from the
padding tweak).

Verified: `tsc` typecheck passes; `stage-sidebar` lib tests pass
(10/10).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com>
Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-22 12:20:29 -04:00
fabro-sh-0530[bot]
95b45b5960
feat: wire Ask Fabro sidebar to real session API with run-control tools (#349)
## Summary

Ships the Ask Fabro sidebar on run pages end-to-end: the agent now has
live `fabro_run_interact` and `fabro_run_events` tools scoped to its
owning run, and the web sidebar talks to real session APIs instead of a
scripted adapter. The `?ask=1` prototype gate is dropped in favour of
server-reported `run.ask_fabro.available`.

## What changed and why

### Rust — run-control tools in Ask Fabro sessions (`fabro-server`,
`fabro-workflow`, `fabro-tool`)

**Tool registration** (`fabro-workflow`): `register_fabro_run_tools` is
now `pub`; a new `register_named_fabro_run_tools` variant accepts a name
allowlist so callers can register a subset without forking the catalog
loop. Unknown names are silently ignored.

**Run-scoped backend** (`fabro-tool`): `ClientBackend` gains a
`run_scope: Option<RunId>` field set via `.with_run_scope(run_id)`.
Every method checks the scope before delegating to the HTTP client,
returning an error before a network call is made. `list_store_runs`
returns a single-element vec of the owning run when scoped;
`resolve_run` rejects non-parseable selectors rather than forwarding
them.

**Session wiring** (`fabro-server`): `build_profile` now returns
`Box<dyn AgentProfile>` (mutably accessible) instead of `Arc`;
`build_agent_session` mints a same-run worker token, builds a
`ClientBackend::with_run_scope`, constructs `FabroRunToolServices`, and
calls `register_named_fabro_run_tools` for the two tools before freezing
into an `Arc`. `AppState::self_server_target()` reads the bound address
from the runtime daemon record for the loopback HTTP call.

**Approval gate**: `build_ask_fabro_tool_approval` now fast-paths
`fabro_run_interact` and `fabro_run_events` to `Ok(())`; all other tools
remain subject to the `ReadOnly` auto-approve check. File/shell tools
are still denied.

### Web — real session adapter and sidebar wiring (`fabro-web`)

**`ask-fabro-runtime.ts`** (new): a `ChatModelAdapter` that creates a
session lazily on the first turn (`sessionsApi.createRunSession`),
caches the session id in `sessionStorage` keyed by run id, and streams
turns via `streamSessionTurn`. `applyTurnEvent` maps `run.session.*` SSE
events to assistant-ui `ThreadAssistantMessagePart[]` incrementally
(text deltas, tool-call started/completed pairs). A 404 on stream clears
the cached id so the next turn starts fresh.

**`ask-fabro-sidebar.tsx`**: drops `scriptIndexRef`, `EMPTY_CHAT`, and
the scripted adapter import; accepts `runId` and `defaultModel` props;
constructs the real adapter via `createAskFabroAdapter`.

**`run-detail.tsx`**: removes `?ask=1` / `askEnabled`; reads
`run.ask_fabro.{available, default_model}` from the summary; always
renders an `AskFabroTriggerButton` (disabled with a tooltip when
unavailable); passes `runId` and `defaultModel` to `<AskFabroSidebar>`.

### Architecture

```mermaid
graph TB
    Browser -->|SSE turn stream| SessionsHandler
    SessionsHandler -->|spawn| AskFabroAgent
    AskFabroAgent -->|fabro_run_interact\nfabro_run_events| ClientBackend
    ClientBackend -->|HTTP + same-run\nworker token| RunsAPI[Runs API\n/runs/:id]
    ClientBackend -->|run_scope check| ClientBackend
    RunsAPI -->|403 cross-run| ClientBackend
```

### Design decisions

- **Same-run scoping is double-enforced**: the `ClientBackend` scope
check fires before the HTTP call; the worker token's run scope causes a
403 at the API layer if the check were somehow bypassed.
- **`build_profile` → `Box` not `Arc`**: the profile needs mutable
access for tool registration after construction, so the `Arc` wrapping
is deferred until registration is complete.
- **`sessionStorage` per-run**: one session is reused across sidebar
open/close cycles for the same run tab; a page reload or different run
always starts clean.
- **Mutating actions included**: `interact` exposes
start/cancel/steer/archive/answer. This is intentional per the locked
decisions; the worker-token scope prevents cross-run blast radius.


### Fabro Details

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

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 11s | – | 0 |
| preflight_lint | 2m 22s | – | 0 |
| implement | 38m 37s | $32.02 | 0 |
| simplify_opus | 17m 40s | $6.55 | 0 |
| simplify_gpt | 9m 32s | $5.55 | 0 |
| verify | 3m 43s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **74m 52s** | **$44.11** | **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: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro <fabro@example.com>
Co-authored-by: fabro <fabro@fabro.sh>
2026-05-22 09:43:31 -04:00
fabro-sh-0530[bot]
f5f921aa3c
feat: add [run.agent] fabro_tools opt-in for worker run tools (#348)
## Summary

Workflow agents previously always received the `agent:run_tools` JWT
scope and had Fabro run tools registered unconditionally. This PR makes
Fabro run tool access an explicit per-run opt-in via `[run.agent]
fabro_tools = true`, defaulting to `false`.

## What changed

**Config layer** (`fabro-types`, `fabro-config`): `RunAgentSettings`
gains a `fabro_tools: bool` field (serialized with `#[serde(default)]`
for backward compatibility). The corresponding `RunAgentLayer` gets an
optional `fabro_tools: Option<bool>` that resolves to `false` when
absent. Layer merging follows the existing `Combine` macro semantics, so
a workflow-level `false` overrides a user-level `true`.

**Server** (`fabro-server`): `worker_command` gains an
`agent_fabro_tools_enabled: bool` parameter. The server reads
`run_state.spec.settings.run.agent.fabro_tools` from the stored run
before spawning `__run-worker`, then issues the worker JWT with either
`["run:worker"]` or `["run:worker", "agent:run_tools"]` accordingly.
`WorkerScopeSet::run_worker()` loses its `#[cfg(test)]` gate so it's
available in production paths.

**CLI worker** (`fabro-cli`): `FabroRunToolServices` construction is now
gated on `run_spec.settings.run.agent.fabro_tools` rather than being
unconditional. The resolved run spec already carries the setting, so no
env-var parsing is needed in the runner.

**Tests**: The single monolithic
`worker_command_always_sets_worker_token_env` test is replaced by two
focused tests — one confirming the default scope is `["run:worker"]`
only, and one confirming the opt-in scope includes `agent:run_tools`.
Shared assertion logic is extracted into
`assert_worker_command_passes_token_only_by_env` and
`worker_token_claims` helpers. Config resolver tests cover default,
explicit true/false, and layer-override behavior.

**Docs**: `[run.agent]` description and reference tables are updated; a
new `run-configuration.mdx` section explains the opt-in semantics before
the existing `[run.agent.mcps]` section.

### Plan Summary

- 1. Add `fabro_tools` to resolved and layered run config, with resolver
tests.
- 2. Gate worker JWT scope and pass setting from stored run state into
`worker_command`.
- 3. Gate CLI `FabroRunToolServices` construction on the resolved
setting.
- 4. Update docs generator sample and public reference/execution docs.
- 5. Full verification pass (nextest, fmt, clippy).


### Fabro Details

<details>
<summary>Ran 9 stages in 54m 4s for $15.21</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 28m 5s | $10.58 | 0 |
| simplify_opus | 12m 4s | $2.86 | 0 |
| simplify_gpt | 5m 19s | $1.77 | 0 |
| verify | 3m 37s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **54m 4s** | **$15.21** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-agent <agent@fabro.local>
2026-05-22 09:41:27 -04:00
fabro-sh-0530[bot]
f5ec711a2c
Stage-based pairing API and fabro_run_pair MCP tool (#344)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
## Summary

Run pairing previously required callers to supply an opaque
`agent_session_id` alongside a `stage_id` to start or target a pair
session. This leaked an internal runtime identifier across the public
HTTP API, generated TypeScript client, and would have bled into any MCP
tooling. This PR removes that coupling: the public pair API now
identifies targets by `StageId` alone, the server resolves the live
session internally, and a new `fabro_run_pair` MCP tool exposes the full
pair lifecycle without ever seeing session identifiers.

### What changed

**Public contract simplification** (`fabro-types`, OpenAPI, generated TS
client)
- `PairTarget` is now `{ stage_id, node_label }` — `node_id`, `visit`,
`agent_session_id`, `provider`, and `model` are removed.
- `PairStartRequest` accepts `{ stage_id }` instead of `{ target:
PairTargetSelector }`.
- `PairTargetSelector` and `PairTranscriptModel` types are deleted
entirely.
- `PairMessageRecord.target` (selector) replaced by
`PairMessageRecord.stage_id`.
- `PairTranscriptAssistantMessage.model` field removed.
- `MAX_PAIR_MESSAGE_BYTES` extracted as a public constant shared between
the server handler and the MCP tool.

**Internal session binding** (`SteeringHub`, server projection)
- `ActivePair` now carries `session_id: String` separately from the
public `PairRecord`. This preserves the stale-session protection that
previously relied on `target.agent_session_id`.
- Transcript matching changed from `(session_id AND stage_id)` to
`stage_id` within the already-scoped pair window sequence range —
simpler and sufficient.
- `active_api_targets` deactivation no longer does a per-target
`agent_session_id` check; it relies on the `active_steerable_stages`
lease already doing that guard.

**New `fabro_run_pair` MCP tool** (`fabro-mcp-server`)
- Actions: `status`, `start`, `get`, `message`, `end`, `transcript`.
- Validation happens before any network call; missing `run_id`, missing
`stage_id` for `start`, missing/invalid `pair_id` for other actions, and
overlong message text all return clean tool-level errors.
- `strum::IntoStaticStr` on `RunPairAction` enables the
`parse_pair_id_for_action` helper to embed the action name in error
messages without a `match`.
- MCP result schema and serialized results are covered by leakage
assertions confirming none of the removed fields surface.

**Tests**
- Negative leakage assertions added to pair DTO tests, event round-trip
tests, control-protocol tests, server handler tests, MCP validation
tests, and MCP schema test.
- Tool count updated from 5 → 6 in all CLI MCP integration tests.
- Steering hub test renamed:
`pair_start_rejects_non_selected_or_missing_target` →
`pair_start_rejects_missing_target` (session-mismatch rejection is now
an internal concern).


### Fabro Details

<details>
<summary>Ran 9 stages in 60m 29s for $31.78</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 26s | – | 0 |
| implement | 33m 14s | $25.51 | 0 |
| simplify_opus | 15m 7s | $4.18 | 0 |
| simplify_gpt | 3m 46s | $2.08 | 0 |
| verify | 3m 9s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **60m 29s** | **$31.78** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-21 21:57:55 -04:00
Bryan Helmkamp
296fbddec9
feat(api): add ask fabro session endpoints (#342)
## Summary

Adds the run-backed API surface needed for a real Ask Fabro sidebar: run
readiness metadata, detailed session projections, session-scoped event
listing/attach streaming, and turn control that exposes durable turn IDs
and machine-readable failures.

## What Changed

- Extended the OpenAPI contract and regenerated Rust/TypeScript clients
for `Run.ask_fabro`, `SessionDetail`, `SessionTurn`, paginated run
sessions, session event APIs, and optional client-supplied `turn_id`
values.
- Updated `fabro-types` and `fabro-store` so durable `run.session.*`
events project active turn state, transcript messages, and the latest
owning run event sequence.
- Implemented server routing for session details, `/events`, `/attach`,
turn conflict headers, typed turn failure codes, and cheap run readiness
decoration across run responses.
- Added browser helpers for POST turn streaming and session attach SSE
parsing, plus an exported generated `sessionsApi`.

## Verification

- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo build --workspace`
- `cargo test -p fabro-types
run_session_turn_failed_defaults_code_for_old_events`
- `cargo test -p fabro-store run_sessions::tests`
- `cargo test -p fabro-api`
- `cargo test -p fabro-server --features test-support --test it
api::sessions`
- `cargo test -p fabro-server --features test-support --test it
api::runs`
- `cd apps/fabro-web && bun test app/lib/session-stream.test.ts`
- `cd apps/fabro-web && bun run typecheck`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 Codex (context unknown, medium reasoning) via
[Codex](https://openai.com/codex/)
2026-05-21 21:26:15 -04:00
fabro-sh-0530[bot]
54bc67017e
feat: Replace duration/elapsed fields with wall_time_ms and StageTiming (#343)
## Summary

Replaces the ambiguous `runtime_secs`, `elapsed_secs`, and `duration_ms`
timing fields on run/stage public API surfaces with explicit
`wall_time_ms` (elapsed clock time) and a `StageTiming` value object
that also carries `inference_time_ms`, `tool_time_ms`, and
`active_time_ms`.

This is a greenfield breaking change — no compatibility shims are
preserved.

### What changed

**API shape**
- `RunBillingStage.runtime_secs` → `RunBillingStage.timing: StageTiming`
- `RunBillingTotals.runtime_secs` → `RunBillingTotals.timing:
StageTiming`
- `RunSummary.timestamps.duration_ms` / `elapsed_secs` removed; a
top-level `timing: StageTiming | null` field added
- Stage list item `duration_secs` → `wall_time_ms`

**Web app (`apps/fabro-web`)**
- `run-billing.tsx`: `liveRuntimeSecs` → `liveWallTimeMs`; live ticking
now returns milliseconds and the footer total sums `wallTimeMs` across
rows
- `stage-sidebar.ts`: `duration_secs` → `wall_time_ms` for the per-stage
duration display
- `runs.ts`: `elapsed_secs` lookup replaced with `timing.wall_time_ms`
- `formatElapsedSecs` / `formatDurationSecs` call sites replaced with
`formatDurationMs`

**Lockfile / tooling**
- `@openapitools/openapi-generator-cli@2.20.2` added as a dev dependency
to `@qltysh/fabro-api-client` to support regenerating the TypeScript
client after schema edits; several transitive deps pulled in alongside
it.

### Design notes

- **Units are now consistent**: every timing value on run/stage surfaces
is in milliseconds; the old API mixed seconds (`runtime_secs`,
`elapsed_secs`) with milliseconds (`duration_ms`).
- **Live ticking** still works correctly: the in-flight billing row
computes `now - startedAt` in ms and sums across rows for the footer,
avoiding a server round-trip during a running stage.
- **`StageTiming.active_time_ms = inference_time_ms + tool_time_ms`** —
parallel work is summed, so run active time can exceed wall time.
- Subsystem-internal `duration_ms` fields (sandbox setup, devcontainer
lifecycle, hooks) are intentionally left unchanged; only public
run/stage timing surfaces are affected.


### Fabro Details

<details>
<summary>Ran 9 stages in 115m 53s for $108.50</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 80m 53s | $101.97 | 0 |
| simplify_opus | 21m 35s | $4.09 | 0 |
| simplify_gpt | 5m 1s | $2.44 | 0 |
| verify | 3m 11s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **115m 53s** | **$108.50** | **0** |

</details>

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

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

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

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

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-21 21:06:40 -04:00
Bryan Helmkamp
fb2174c7d0
feat(agent): expose Fabro run tools in sessions (#339)
## Summary

API-backed Fabro agent sessions can now use the same five run-control
tools that were previously MCP-only, while the server uses a single
scoped `FABRO_WORKER_TOKEN` path for worker authorization. This lets
server-dispatched API agents create, gather, inspect, and interact with
runs without reintroducing a separate delegated run-agent token or
leaking credentials into sandboxed child environments.

## Changes

- Moved the reusable run-tool implementation into the new `fabro-tool`
crate so MCP and API agent backends share the same tool schemas and
client behavior.
- Registered the five `fabro_run_*` tools for API-mode agents, with ACP
sessions continuing to omit those tools.
- Replaced `FABRO_RUN_AGENT_TOKEN` with scoped worker-token auth: base
worker tokens keep same-run access, and `run:worker agent:run_tools`
tokens can call the run-control API across runs.
- Added server auth guards for run-tool actors and
run-scoped-or-run-tools routes, then applied them only to the routes
used by the run-tool client backend.
- Kept `FABRO_WORKER_TOKEN` scrubbed from sandbox commands, hooks, ACP
subprocesses, MCP env providers, and other child tool environments.

## Testing

- `cargo nextest run -p fabro-server worker_token principal_middleware
spawn_env --no-fail-fast`
- `cargo nextest run -p fabro-cli runner --no-fail-fast`
- `cargo nextest run -p fabro-workflow agent_run --no-fail-fast`
- `cargo nextest run -p fabro-static --no-fail-fast`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-server -p fabro-cli -p
fabro-static --all-targets -- -D warnings`
- `git diff --check`

---

[![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-21 19:48:54 -04:00
fabro-sh-0530[bot]
2b168b4588
Compute LLM cost on-read for in-flight billing stages (#345)
## Summary

The run billing page showed `—` for dollar cost on any active
(in-flight) stage because `total_usd_micros` is only computed at stage
completion. This PR prices stages whose cost is `None` at read time,
using the model and token counts already present in the projection.

## What changed

**`fabro-model/src/billing.rs`** gains two new methods on existing
types:
- `BilledTokenCounts::token_counts()` — extracts the five disjoint token
buckets, dropping the derived sum and optional cost field.
- `Catalog::price_tokens(model, tokens)` — mirrors the cost computation
from `billed_model_usage_from_llm` but callable outside the completion
path. Returns `None` for unknown models or providers with no billing
policy.

**`fabro-workflow/src/billing_rollup.rs`** —
`billing_rollup_from_projection` gains an `Option<&Catalog>` parameter.
A new private helper `stage_usage_with_cost` fills in `total_usd_micros`
on-the-fly for any stage where it is `None` and both a catalog and model
are available. All four accumulator call sites (`is_zero` check,
per-stage row, `totals`, and `by_model`) use the priced copy, keeping
the page internally consistent.

**Call sites** — the read handler (`handler/billing.rs`) passes
`Some(&catalog)` so active stages get priced. The aggregate-billing
sites in `server.rs` and the four finalization sites in `finalize.rs`
pass `None` — completed stages are already priced, and we deliberately
exclude running estimates from org-wide totals to avoid double-counting
when a run later finalizes.

## Design decisions

- **Price any stage with `total_usd_micros == None`**, not just
explicitly in-flight ones. Stages with no billing policy return `None`
again — harmless.
- **No "estimated" label** — cost-so-far is exact for tokens consumed so
far, consistent with the already-unlabeled live token count and runtime.
- In-flight **prompt** stages still show `—` because `stage.model` isn't
set until `PromptCompleted`. This is acceptable; the reported bug
concerns agent stages.


### Fabro Details

<details>
<summary>Ran 9 stages in 44m 13s for $7.17</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 4m 6s | – | 0 |
| preflight_lint | 4m 58s | – | 0 |
| implement | 13m 24s | $3.84 | 0 |
| simplify_opus | 10m 34s | $1.69 | 0 |
| simplify_gpt | 5m 58s | $1.64 | 0 |
| verify | 3m 40s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **44m 13s** | **$7.17** | **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>
2026-05-21 19:25:00 -04:00
Bryan Helmkamp
5095873ddd
feat(web): add detail popovers to the run header
Hovering the run header items now reveals a popover with extra
context:

- Run status: failure reason and error message for failed runs;
  archived timestamp for archived runs (no popover otherwise)
- Repository: full owner/repo name and the cloned branch
- Workflow: node and edge counts plus run labels
- PR: live GitHub details fetched lazily on hover — title, an
  open/draft/merged/closed badge, and the head -> base branch arrow

Workflow node/edge counts are new: WorkflowRef now carries
node_count/edge_count, computed in build_summary from the parsed
graph that is already in hand there.

Adds a HoverCard primitive alongside Tooltip (shared useHoverAnchor
hook) for rich, viewport-aware popovers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 14:10:39 -04:00
Bryan Helmkamp
37d6b3dbcd
fix(server): count whole storage tree in Fabro-managed bytes
build_disk_usage_response only summed scratch/ run dirs and logs/*.log,
omitting objects/ (SlateDB + artifacts), sessions/, and vaults/ — a ~30x
undercount of "Fabro managed" storage on the resources page.

Measure the whole storage_dir tree for total_size_bytes so it can't drift
as new subdirectories are added. Reclaimable stays a curated estimate that
matches what `fabro system prune` actually frees. A residual "other"
summary row keeps `fabro system df` totals consistent and surfaces as a
"Database & artifacts" table row.

Also add a KiB tier to formatBytesAsMemory so small storage values render
human-readably instead of raw byte counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:28:39 -04:00
Bryan Helmkamp
86b1fbef7f
feat(api): bind sessions to runs (#336)
## Summary

Ask Fabro sessions are now run-bound instead of standalone. Sessions are
created under their owning run, then accessed by flat session ID routes,
with durable state projected from the run event stream rather than a
separate session store.

## Changes

- Move session creation/listing to `POST/GET /api/v1/runs/{id}/sessions`
while keeping flat session reads, turns, interrupts, and event streams
under `/api/v1/sessions/{id}/...`.
- Add typed `run.session.*` events, ULID-backed session/turn IDs,
read-only default permissions, and a rebuildable SlateDB `session_id ->
run_id` index.
- Remove the old file-backed session store and wire the server, runtime,
Rust client, generated API crates, and TypeScript client around run
event projections.
- Replace the old top-level CLI session command with `fabro run ask` for
chatting with a run.
- Regenerate the TypeScript API client; this also catches up existing
generated models for Pair/run event detail schemas already present in
the OpenAPI spec.

## Validation

- `cargo build -p fabro-api -p fabro-client -p fabro-server -p
fabro-cli`
- `cargo nextest run -p fabro-server --features test-support -E
'test(run_bound_session_is_created_as_run_event_and_resolves_by_flat_id)
| test(sessions_are_listed_only_under_their_owning_run)'`
- `cargo nextest run -p fabro-store
projection_rebuilds_runtime_context_from_run_events`
- `cargo +nightly-2026-04-14 clippy -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-cli --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd lib/packages/fabro-api-client && bun run typecheck && cd
../../../apps/fabro-web && bun run typecheck`
- `git diff --check`

---

[![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-21 10:53:41 -04:00
Bryan Helmkamp
9837a08929
refactor(install): share persistence pipeline (#332)
## Summary

Unifies installer persistence so CLI and web install paths share the
same file/env/vault primitives, while preserving the CLI's server-API
secret persistence and auth bootstrap behavior.

## What Changed

- Added shared `fabro-install` config writers for installer-owned tagged
enum tables, replacing `server.listen` and `cli.target` atomically so
stale variant fields cannot survive.
- Added `InstallPersistencePlan` for disk-backed settings, server env,
and vault writes/removals with the existing rollback semantics for
settings and vault failures.
- Refactored `fabro install`, `fabro install github`, and
`/install/finish` to use the shared persistence plan where their disk
behavior overlaps.
- Preserved full-install ordering: settings/env first, workflow-visible
secrets through the server API second, and CLI `auth.json` only after
API secret persistence succeeds.
- Preserved web installer failure response fields for leftover and
removed env keys, plus the post-success finish hook/shutdown behavior.

## Test Plan

- `cargo nextest run -p fabro-install`
- `cargo nextest run -p fabro-cli commands::install::tests`
- `cargo nextest run -p fabro-cli --test it cmd::install`
- `cargo nextest run -p fabro-server --features test-support --test it
api::install`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`

---

[![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-21 08:04:04 -04:00
Bryan Helmkamp
bf96baa9f0
feat(server): add run pairing API (#312)
## Summary

Adds the server-side run pairing surface for joining one active API-mode
agent session, sending pair messages, reading a compact transcript, and
ending pairing explicitly before workflow release continues.

This PR wires the feature end to end:

- adds OpenAPI paths and shared `fabro-types` DTOs for pair lifecycle,
messages, transcript entries, and run event details
- adds typed `RunEvent` variants for pair lifecycle and pair-scoped
user/system messages
- extends the workflow steering hub and agent session drain path with
typed pair control items, single-target validation, pair parking, and
pair end/resume behavior
- extends worker JSONL control and server transports for pair
start/message/end while preserving existing
steer/interrupt/answer/cancel behavior
- adds Axum handlers for `/api/v1/runs/{id}/pair`, pair messages, pair
transcript, and `/api/v1/runs/{id}/events/{seq}`
- adds `fabro-client` helpers for the new endpoints

## Notes

The subprocess path does not add a bidirectional worker ack channel in
this PR. Instead, the HTTP pair handlers only return lifecycle/message
success after the corresponding durable runtime event is observed, so
mpsc enqueue success alone is not treated as API success.

The plan checklist in
`docs/superpowers/plans/2026-05-18-server-side-run-pairing-api-events.md`
is included with that distinction left visible.

## Verification

- `cargo build -p fabro-api`
- `cargo check -p fabro-api -p fabro-client -p fabro-agent -p
fabro-workflow -p fabro-interview -p fabro-server`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-api pair
run_event_round_trips_pair_lifecycle_events
run_event_round_trips_agent_pair_messages`
- `cargo nextest run -p fabro-workflow pair`
- `cargo nextest run -p fabro-interview pair`
- `cargo nextest run -p fabro-server pair
subprocess_answer_transport_pair_commands_enqueue_control_messages steer
interrupt`
2026-05-20 18:58:38 -04:00
Bryan Helmkamp
412f57f5ae
feat(web): add secrets management settings page (#327)
## What

Adds a **Settings → Secrets** page so secrets can be managed from the
browser, backed by the existing secrets HTTP API and generated TS
client.

- **`/settings/secrets`** — lists stored secrets (name, type badge,
description, last-updated) and deletes them through the shared confirm
dialog.
- **`/settings/secrets/new`** — the create form for **token** and
**file** secrets. OAuth secrets still list and delete here, but are
created by provider sign-in flows, not typed by hand (matching the CLI's
`secret set`).
- The **Secrets** entry is added to the settings sidebar nav.

## How

- `secretsApi` wired into `api-client.ts`; `useSecrets()` SWR hook +
`secrets` query key.
- New sibling routes `secrets` and `secrets/new` under `settings` (same
pattern as `runs` / `runs/:id`).
- The settings layout gains optional **handle-driven** `description` and
`headerAction`. When a page declares them, the layout renders title +
subheading + a vertically-centered header action button as one unified
header. Other settings pages are unaffected — they fall back to the
existing title-only header.

## Notes

- Values are write-only: the API never returns secret values, and the UI
never displays them.
- Reuses existing primitives throughout (`Panel`, `Badge`,
`ConfirmDialog`, `useToast`, button/input classes) — no new shared
components.
- Verified: `bun run typecheck` and `bun run build` pass; routes serve
200.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:50:09 -04:00
Bryan Helmkamp
40ed64c1c2
Add system resources settings page (#328)
## Summary

Adds server-visible resource reporting and a compact Resources settings
tab for CPU, memory, and the filesystem that contains Fabro storage.

## Changes

- Adds `GET /api/v1/system/resources` backed by `sysinfo`, including CPU
sampling, cgroup-aware memory reporting, storage filesystem matching,
and Fabro-managed disk byte totals.
- Extends the OpenAPI contract and regenerates the Rust and TypeScript
API clients.
- Adds a deterministic demo-mode resources route.
- Adds `/settings/resources` with 5 second polling and panels for
overview, CPU, memory, disk, and notes.
- Adds server integration/unit coverage and web route/render coverage.

## Screenshot

![Resources settings
page](https://raw.githubusercontent.com/fabro-sh/fabro/feature/system-resources-settings/docs/public/images/web/settings-resources.jpg)

## Verification

- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cargo nextest run -p fabro-server --features test-support --test it
api::system`
- `cargo test -p fabro-server resource_sampler::tests`
- `cd apps/fabro-web && bun test`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun run build`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound%20Engineering-Codex-6f42c1)](https://github.com/compound-engineering)

🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:22:53 -04:00
Jess Martin
5bfd115339
[codex] Add ACP steering support (#329)
## Summary
- Adds a backend-neutral live control abstraction so steering,
interrupt, and interrupt+steer no longer depend on API-only session
handles.
- Reworks ACP sessions into a live protocol loop that uses ACP
`session/prompt` for follow-up steers and ACP `session/cancel` for
interrupts without restarting the process.
- Registers ACP sessions as steerable, removes the stale non-steerable
server/UI/API path, preserves ACP projection metadata, and keeps
unsupported backends out of the steerability gate.

## Validation
- `LC_ALL=C cargo nextest run --workspace --no-fail-fast` (5,833 passed,
178 skipped)
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1
ASDF_BUN_VERSION=1.3.11 bun test` (396 passed)
- `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1
ASDF_BUN_VERSION=1.3.11 bun run typecheck`
- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && LC_ALL=C ASDF_BUN_VERSION=1.3.11
bun run typecheck`

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-20 17:21:56 -04:00
Bryan Helmkamp
32015b2226
fix(graph): support dotted Fabro graph attributes (#324)
## Summary

Graph rendering now accepts documented Fabro dotted DOT attributes end
to end while keeping raw Graphviz calls behind `fabro-graphviz`. The new
`RenderableDot` boundary applies Fabro render styling and normalization
before raw SVG rendering, and both the CLI subprocess and server path
now route through that typed boundary instead of calling `graphviz_sys`
directly outside the graphviz crate.

The branch also adds a small curated DOT compatibility corpus covering
ACP agent attributes, human default choices, and subworkflow manager
attributes. Those fixtures are exercised by both render and validation
tests, and the run overview now shows graph render errors directly
instead of falling through to the empty graph state.

## Verification

- `cargo nextest run -p fabro-graphviz`
- `cargo nextest run -p fabro-validate`
- `cargo nextest run -p fabro-cli render_graph`
- `cargo nextest run -p fabro-server
render_graph_from_manifest_accepts_fabro_dotted_attributes`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-graphviz -p fabro-cli -p
fabro-server -p fabro-validate --all-targets -- -D warnings`
- `rg -n "graphviz_sys" lib/crates/fabro-cli lib/crates/fabro-server`
- `cd apps/fabro-web && bun test`
- `cd apps/fabro-web && bun run typecheck`

---

[![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)

---------

Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com>
2026-05-20 09:31:08 -04:00
Bryan Helmkamp
d9b859d11d
chore: simplify 2026-05-20 09:13:51 -04:00
Bryan Helmkamp
d17599898e
feat(web): show creator avatar on run "Created by" cell (#319)
## Summary

The run Overview tab's "Created by" cell rendered every user as a
colored circle with the first letter of their login. Reviewers and run
owners expected the same GitHub avatar shown on `/profile` and in the
top-right nav. The cell only had `login` to work with — the
`PrincipalUser` schema carried no avatar URL.

This threads an optional `avatar_url` through `UserPrincipal`
end-to-end: schema, server auth, and frontend. The avatar is captured at
action time from the request's auth context and persisted with the run's
`created_by` principal — a point-in-time snapshot, the same pattern as
audit logs and chat apps.

## What changed

- **`fabro-types`** — `UserPrincipal` gains `avatar_url: Option<String>`
with `#[serde(default, skip_serializing_if)]`, plus a
`Principal::user_with_avatar` constructor. The existing
`Principal::user` constructor is unchanged (sets `None`), so test
fixtures and CLI/replay call sites need no edits.
- **OpenAPI** — `PrincipalUser` gains an optional nullable `avatar_url`;
Rust (progenitor) and TypeScript clients regenerated.
- **`fabro-server`** — `auth_context_from_session` (cookie auth) and
`classify_user_token` (JWT auth) populate the principal's avatar from
the session/JWT, treating an empty string as `None`.
- **`fabro-web`** — the `run-summary-panel` "Created by" cell renders an
`<img>` when `avatar_url` is present, falling back to the initial circle
otherwise.

## Compatibility

The field is optional with serde defaults, so old persisted runs and
`RunEvent.actor` payloads deserialize unchanged — they show the
initial-circle fallback. No migration or backfill.

## Known gap

CLI-initiated runs (`fabro run ...`) still show the initial circle: the
CLI auth flow hardcodes an empty `avatar_url` in the JWT subject
(`cli_flow.rs:508`). Wiring the avatar through CLI login
(`~/.fabro/auth.json`, JWT claims, refresh-token chain) is a deliberate
follow-up. Web-initiated runs get the avatar today.

## Test plan

- `cargo nextest run --workspace` — 5,832 tests pass, including new
`principal.rs` and `principal_round_trip.rs` cases covering avatar
serialization and legacy-JSON (no-field) deserialization.
- `cd apps/fabro-web && bun test run-summary-panel` — 13 tests pass,
including a new case asserting the `<img>` renders with the avatar src.
- `bun run typecheck`, `cargo +nightly-2026-04-14 fmt --check --all`,
and `clippy --workspace --all-targets -- -D warnings` all clean.
- Manual: restart `fabro server`, create a run from the web UI, confirm
the real avatar renders on the Overview tab; confirm an older run falls
back to the initial circle.

---

[![Compound Engineering
v2.60.0](https://img.shields.io/badge/Compound_Engineering-v2.60.0-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via
[Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:11:57 -04:00
Bryan Helmkamp
fbe8b50a16
feat(server): add GET /api/v1/providers and /settings/models page (#321)
## Summary

Operators had no UI surface to see which LLM providers their Fabro
server has configured — provider state was only inferable indirectly via
the per-model `configured` flag on `GET /api/v1/models`. This adds a
dedicated **Models** settings tab backed by a new providers endpoint.

- **`fabro_model::Provider`** — a public projection of the internal
`CatalogProvider` that *structurally* excludes credential-bearing fields
(`auth`, `extra_headers`, `billing_policy`, `agent_profile`). Reused by
the generated API client via progenitor `with_replacement`, mirroring
the existing `Model` pattern — no parallel API DTO.
- **`GET /api/v1/providers`** — lists catalog providers with effective
config and a `configured` status stamped per request from
`ready_llm_provider_ids()`. Sorted by the catalog's existing
`provider_order`. No write endpoints.
- **`/settings/models` web page** — new route + nav entry
(`CpuChipIcon`, between Integrations and Security) rendering each
provider with model count, default model, configured status, and a "Get
API key" link for unconfigured providers.

## Key decisions

- Provider sort: reuse catalog `provider_order` (priority desc, id asc)
— zero extra code.
- `adapter` is hidden in the UI row (noisy for first-party providers);
the OpenAPI `adapter` field is pinned to an enum matching the closed
`AdapterKind` type.
- `configured` reflects credential resolution **at the time of the
response**, not a frozen startup snapshot — doc/spec wording corrected
to match.

## Testing

- `fabro-model`: `From<&CatalogProvider>` + serde `skip_serializing_if`
unit tests.
- `fabro-api`: `Provider` type-identity + JSON-parity tests, including
the required/optional field split.
- `fabro-server`: handler tests for configured vs unconfigured
providers, exact `model_count`/`default_model` against catalog truth,
and credential-omission (asserts internal field names *and* the injected
credential value never reach the wire).
- OpenAPI route conformance test covers `GET /api/v1/providers`.
- `cargo build --workspace`, `fmt --check`, `clippy -D warnings` clean;
935 Rust tests pass; web `tsc` typecheck passes.
- Reviewed via a 10-persona `ce:review` (autofix) — no P0/P1 in shipped
code; 8 safe fixes applied.

Not done: manual UI screenshots — the `apps/fabro-web` build is blocked
in this environment by an unrelated missing `@assistant-ui/react`
dependency. Run `bun install` in `apps/fabro-web` to verify
`/settings/models` manually.

## Post-Deploy Monitoring & Validation

- **What to watch:** request logs for `GET /api/v1/providers` — expect
`200`s for authenticated users, `401` for unauthenticated. The handler
resolves LLM credentials per request via `ready_llm_provider_ids()` (the
same path the existing `list_models` handler already uses).
- **Healthy signals:** `/settings/models` renders the provider list;
`configured` matches each provider's actual credential state; no
credential strings appear in any response body or log line.
- **Failure signals / rollback trigger:** any provider object in the
response containing `auth`, `extra_headers`, or a raw key/token value →
roll back immediately (the projection type makes this structurally
impossible, but treat any occurrence as P0). 5xx spikes on the new
route.
- **Validation window / owner:** first 24h after deploy, owned by the
deploying engineer. Pre-existing note (not introduced here): credential
resolution can refresh OAuth tokens and write the vault as a side effect
of this read — shared with `list_models`; flagged for a future caching
pass.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:05:00 -04:00
Bryan Helmkamp
e7ce0f6a86
fix(server): restore legacy vault migration removed mid-PR
PR #306 added a temporary startup migration for pre-token/oauth vault
files (commit 3448a7d06) and then deleted it 28 minutes later in the
same PR (commit 437a27769) before merging. Installs that still have
`credential` or `environment` entries in secrets.json now refuse to
boot with `unknown variant 'credential'`.

Restore vault_legacy_migration.rs and wire it back into build_app_state
so old vault files are rewritten to `token`/`oauth`/`file` shape with
a timestamped backup on first boot. Keep the strict `Vault::load` path
(no empty-vault fail-open) so a genuinely corrupt vault still aborts
startup instead of silently zeroing out secrets.

The migration self-deletes after the 2026-08-18 removal deadline noted
in the module docs.
2026-05-18 16:00:18 -04:00
Bryan Helmkamp
29b7cc0de0
feat(workflow): enforce strict api/acp backends (#307)
## Summary

This PR makes agent execution a strict two-backend contract: API-backed
stages use Fabro-owned model/provider auth, while ACP-backed stages
launch a user-supplied stdio process that owns its own auth and tools.
That removes the legacy CLI backend and prevents ACP execution from
accidentally resolving or forwarding provider credentials.

## Changes

- Replaces the old `api`/`cli`/`acp` backend model with `AgentBackend {
api, acp }`, with `backend=\"cli\"` rejected and migrated toward
explicit ACP process configuration.
- Splits ACP process configuration into `acp.command` for shell command
strings and `acp.config` for JSON stdio configs, while rejecting legacy
`acp_command`.
- Restricts ACP to `agent` nodes and rejects API-only attributes such as
`model`, `provider`, `reasoning_effort`, `max_tokens`, and `speed` on
ACP nodes.
- Deletes the workflow CLI runtime, CLI credential resolver surface, CLI
live smoke tests, and `agent.cli.*` event handling.
- Updates ACP events and projections to report process identity
(`command`, optional `config_name`) rather than provider/model metadata.
- Updates import/stylesheet propagation, CLI workflow smoke coverage,
server steering tests, and web model extraction for the new
event/backend contract.

## Validation

- `cargo check -p fabro-auth -p fabro-acp -p fabro-workflow -p fabro-cli
--all-targets`
- `cargo nextest run -p fabro-auth -p fabro-acp -p fabro-validate -p
fabro-store -p fabro-workflow --lib`
- `cargo nextest run -p fabro-acp`
- `cargo nextest run -p fabro-cli --test it
workflow::acp::acp_backend_workflow`
- `cargo nextest run -p fabro-workflow --test it
codergen_without_backend_simulated`
- `cargo nextest run -p fabro-workflow --test it
import_e2e_through_engine`
- `cargo nextest run -p fabro-workflow --test it stylesheet_application`
- `cargo nextest run -p fabro-server
steer_with_active_acp_stage_returns_non_steerable_conflict`
- `cargo nextest run -p fabro-server
active_acp_stage_marker_clears_on_terminal_paths`
- `cargo nextest run -p fabro-types
agent_backend_accepts_only_api_and_acp`
- `cd apps/fabro-web && bun test app/routes/run-stages.test.ts`
- `cd apps/fabro-web && bun run typecheck`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---

[![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)

---------

Co-authored-by: Peter Bell <4843+PeterBell@users.noreply.github.com>
2026-05-18 13:20:56 -04:00
Bryan Helmkamp
cd74013d06
refactor(auth): split credential sources and vault schemas (#306)
## Summary

Compared with `origin/main`, this PR splits credential storage and
credential references into explicit types. Vault secrets now distinguish
`token`, `oauth`, and `file` payloads, while runtime/model configuration
points to credentials through explicit `env:<NAME>` and `vault:<NAME>`
source refs.

## Changes

- Replaces the old `environment`/`credential` secret schema vocabulary
with `token`/`oauth`/`file` across OpenAPI, Rust API tests, generated
TypeScript models, CLI/docs references, and the changelog.
- Updates auth resolution, refresh, provider strategies, workflow LLM
handling, server diagnostics, install flows, run manifests, and secret
handlers to consume typed vault entries and explicit credential sources.
- Updates provider catalog TOMLs and config parsing so provider auth and
extra headers use `vault` refs instead of ambiguous `credential` refs.
- Updates CLI install/login/run/secret paths and integration tests to
write and read the new credential shapes.
- Removes the temporary legacy vault migration and empty-vault fallback,
then centralizes provider vault secret-name lookup and Codex API
credential shaping.

## Verification

- `cargo +nightly-2026-04-14 fmt --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-auth -p fabro-model -p
fabro-config -p fabro-vault -p fabro-server -p fabro-cli --all-targets
-- -D warnings`
- `ulimit -n 4096 && cargo nextest run -p fabro-auth -p fabro-model -p
fabro-config -p fabro-vault -p fabro-server -p fabro-cli` (`1938`
passed, `35` skipped)
2026-05-18 11:07:42 -04:00
David Bock
6a86ced77c
fix(server): persist manifest metadata names (#302)
When Fabro creates a detached run through the server, the resulting run
metadata should still read like something a human can trust at a glance.
Before this change, those runs could persist with
`settings.project.name` and `settings.workflow.name` left `null` even
though Fabro already had enough local context to infer them. That made
`inspect` output look half-populated and made it harder to tell whether
the saved run state was complete.

This fixes that trust gap in the server-backed manifest flow.

## Summary
- backfill missing manifest-backed project and workflow names during
server run preparation
- prefer explicit `[workflow].name` from bundled `workflow.toml`, then
fall back to graph name or workflow slug
- cover both manifest preparation and persisted run-state behavior with
server tests

## Testing
- cargo test -p fabro-server
prepare_manifest_backfills_missing_project_and_workflow_names --
--nocapture
- cargo test -p fabro-server
prepare_manifest_preserves_explicit_project_and_workflow_names --
--nocapture
- cargo test -p fabro-server
create_run_persists_backfilled_project_and_workflow_names -- --nocapture

---------

Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-18 08:31:23 -04:00
Aleksi Asikainen
492aba7fff
fix: MiniJinja can't find partials (#301)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Fixes an issue where use of MiniJinja
[`include`](https://jinja.palletsprojects.com/en/stable/templates/#include)
control structure (`{% include "filename.ext" %}`) causes a render error
`template not found: tried to include non-existing template
"filename.ext"`

### Example broken diagram
``` dot
digraph ValidatePlan {
    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    test_inline_prompt [label="moo" prompt="{% include 'test.tpl.md' %}"]
                                          // ^^^^^^^^^^^^^^^^^^^^^^^^^
    start -> test_inline_prompt -> exit
}
```

### Fix
The core issue was that template rendering knew the source name for
diagnostics, but did not have a loader rooted at the prompt/goal file
location. Includes therefore failed even when the included file existed
next to the rendered file. The fix adds optional loader support to
`fabro-template`, then wires workflow rendering to the existing
`FileResolver` so includes resolve relative to the file currently being
rendered.

For `fabro validate`, there was a second manifest-specific problem:
validation runs through a bundled manifest, and the manifest builder
only bundled explicit `prompt.md` / `goal.md` files, not static
MiniJinja `include` dependencies inside those files. The manifest
builder now scans prompt/goal template text for literal `{% include
"file" %}` / `{% include 'file' %}` references and bundles those files
too. Missing or unsafe include names are left for MiniJinja/runtime
validation rather than expanding scope.

(For clarity: The fix does not support variables or arrays in
`include`.)
2026-05-17 22:15:45 -04:00
Bryan Helmkamp
302e2445b4
refactor(model): move provider facts into catalog (#298)
## Summary

Moves provider-specific facts out of `AdapterKind` metadata and into
provider catalog data, leaving adapters responsible for runtime protocol
behavior. This makes providers that share an adapter mostly TOML-driven
while still surfacing adapter construction failures during readiness
checks.

## What Changed

- Provider TOML now owns auth mode, API-key/header policy, billing
policy, agent profile, base URLs/env overrides, extra headers, and probe
markers.
- Auth, install, config, diagnostics, and server flows resolve provider
credentials from catalog auth config, including API-key, header-only,
and no-auth providers.
- LLM client registration now reports adapter construction failures,
validates final adapter requests before HTTP dispatch, and preserves
custom primary auth headers.
- Billing and docs now use provider-owned billing policy instead of
adapter metadata, and the old adapter metadata surface is removed.

## Reviewer Notes

OpenAI-compatible `base_url` validation now happens during
adapter/client registration rather than catalog build. That keeps
catalog parsing adapter-agnostic while still letting readiness and model
listing reflect providers that cannot register.

## Verification

- `cargo check -p fabro-model -p fabro-auth -p fabro-llm -p fabro-server
-p fabro-cli`
- `cargo nextest run -p fabro-llm -- adapter_registry`
- `cargo nextest run -p fabro-model -- catalog`
- `cargo nextest run -p fabro-auth -- api_key`
- `cargo nextest run -p fabro-server -- install`
- `cargo +nightly-2026-04-14 fmt --check --all`

---

[![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)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:59:08 -04:00
Bryan Helmkamp
93452001a1
feat(api): require typed PermissionLevel on session create (#300)
## Summary

- `POST /api/v1/sessions` now requires `permissions` as a typed enum
(`read-only` | `read-write` | `full`) instead of accepting an optional
plain string.
- Removes the silent fallback at `sessions.rs:906-911` where unknown
values (e.g. `"readonly"`) were coerced to `read-write` — a real
security footgun: a client trying to lock the agent down would get write
access instead.
- Invalid or missing values are now rejected by axum's `Json` extractor
with `422 Unprocessable Entity`.

## Approach

- New `PermissionLevel` OpenAPI schema (`type: string, enum: [...]`).
- Moves `PermissionLevel` from `fabro_agent::cli` to
`fabro_types::session` so `fabro-api` can `with_replacement` it without
a circular dep. `fabro_agent::cli::PermissionLevel` remains as a `pub
use` re-export so existing call sites keep working.
- `SessionRecord.permissions` becomes required and non-nullable for
coherence — every created session has a concrete level.
- `build_tool_approval` in the server takes `PermissionLevel` directly;
the string-match fallback is deleted.
- CLI's `session_permissions` returns a concrete `PermissionLevel`
(defaults to `read-write` when neither flag nor settings provide one)
and is sent explicitly on every request.

## Scope notes

Confirmed out of scope and not addressed here:
- Mid-session model/permission switching
- Interactive tool approval / HITL

## Breaking change

The `permissions` field is now required on `CreateSessionRequest` and
non-nullable on `SessionRecord`. Existing on-disk session records
persisted with `"permissions": null` will fail to deserialize.
Acceptable per project policy (no migration); local dev users may need
to clear `~/.fabro/storage/sessions/` once.

## Test plan

- [x] `cargo build --workspace`
- [x] `cargo nextest run -p fabro-api` — 125/125 (includes new
`permission_level_round_trip` parity tests)
- [x] `cargo nextest run -p fabro-server` — 554/554 (includes new 422
tests for missing + invalid permissions)
- [x] `cargo nextest run -p fabro-cli` — 892/892
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- [x] `cargo +nightly-2026-04-14 fmt --check --all`
- [x] `bun run generate` on `fabro-api-client` — emits typed
`PermissionLevel` union and required field on `CreateSessionRequest`

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:15:20 -04:00
Bryan Helmkamp
ac32963538
feat(web): add Children tab to Run detail page (#294)
## Summary

- Surfaces parent/child run relationships in the web UI as a new
**Children** tab between Files Changed and Sandbox on `/runs/:id`.
- Backend exposes a new `children_count` field on the `Run` summary,
computed on read from the existing
`RunProjectionCacheState.children_by_parent` index — accurate without an
extra query.
- Frontend reuses the compact-table `RunRow` from `/runs` (now exported)
so the children list matches the existing list-view at a glance.
- Tab always shows, with a zero state when there are no children.
Refresh button (icon-only, matching the Files Changed pattern)
re-fetches both the list and the parent detail so the count badge
updates with the list.

## Screenshots

Captured live from the running fabro server.

**Populated — `Children · 2` tab active, two succeeded child rows:**

![Children tab
populated](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/populated.png)

**Zero state — visiting a run that has no children:**

![Children tab zero
state](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/zero-state.png)

## API verification

```sh
# parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP5DJJ4EV6T7QSB081Z1N \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "parent_id": null,
  "children_count": 2
}

# child
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP7VAS2J2AG73GQSAKF4G \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP7VAS2J2AG73GQSAKF4G",
  "parent_id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "children_count": 0
}

# list-by-parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    "http://127.0.0.1:32276/api/v1/runs?parent_id=01KRTKP5DJJ4EV6T7QSB081Z1N" \
    | jq '{count: (.data | length), has_more: .meta.has_more}'
{ "count": 2, "has_more": false }
```

## What's in each commit

| Commit | What |
| --- | --- |
| `2f5f4296` | `chore(api-client)`: regenerate TS client from current
OpenAPI spec — catches up drift from #292's source-aware diagnostics and
the session/turn shape updates that hadn't been re-run yet. Pure
generator output. |
| `ba16d2b3` | `feat(web)`: the actual Children tab feature. Backend
`children_count` field + cache wiring, new `useChildRuns` SWR hook,
exported `RunRow`/`RUNS_LIST_GRID_TEMPLATE` from `runs.tsx`, new
`run-children.tsx` route, `Run.children_count` on the generated TS type.
|
| `de0c32c9` | `docs`: live UI screenshots for this PR. Safe to revert
before merge if reviewers prefer a screenshot-free repo. |

## Reproducing the screenshots

1. `cargo build -p fabro-cli && ./target/debug/fabro server start`
2. `cd apps/fabro-web && bun run build`
3. ```sh
PARENT=$(./target/debug/fabro run hello --dry-run --detach --sandbox
local --json | jq -r .run_id)
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
   ```
4. Open `http://127.0.0.1:<port>/runs/$PARENT/children` (populated) and
a child's children tab (zero state).

## Test plan

- [x] `cargo nextest run -p fabro-store -p fabro-types -p fabro-api -p
fabro-server -p fabro-mcp-server` — 900+ tests pass, including new
`run_summary_includes_children_count` in `fabro-store`
- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `cd apps/fabro-web && bun test` — 383/383 pass
- [x] OpenAPI ↔ Rust parity (the `fabro-api` `run_summary_round_trip`
test covers the new field both directions)
- [x] Manual API verification via curl (above)
- [x] Live UI verification (screenshots above)

## Out of scope (v1)

- Real-time SSE updates of the children list (refresh button covers
this).
- Multi-page pagination UI (shows first page with a "more exist" footer
when `has_more`).
- Parent breadcrumb on the child run page (separate small change).
- Tree/nesting view (flat list only).
- Empty-state CTA.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:21:40 -04:00
Bryan Helmkamp
3afc95abdd
test(server): isolate storage in streaming_session_turn_updates_runtime_context test (#293)
## Summary

- Fixes the flaky nightly release-pipeline failure observed on tag
`v0.235.0-nightly.1` ([run
25975240763](https://github.com/fabro-sh/fabro/actions/runs/25975240763/job/76354137058)).
-
`streaming_session_turn_updates_runtime_context_without_copying_prior_history_to_turn`
was using the default test server settings, which means every parallel
test shares the default session storage directory
(`$HOME/.fabro/storage`).
- `session_store::write_json` writes via `fs::write`, which truncates
the file before writing. A concurrent reader from another test's
`AppState` session lookup can observe the empty file mid-write and fail
to deserialize. The deserialization error bubbled up as a `turn.failed`
SSE event carrying `Serialization error: EOF while parsing a value at
line 1 column 0`.
- Apply the same isolation pattern used in `e9387bf62` for
`interrupt_active_session_turn_cancels_runtime_and_persists_interrupted`:
give this test its own storage root under `std::env::temp_dir()`.

## Test plan

- [x] `cargo nextest run -p fabro-server -- streaming_session_turn`
passes locally
- [x] `cargo nextest run -p fabro-server` (full suite, 552 tests) passes
locally
- [x] `cargo +nightly-2026-04-14 clippy -p fabro-server --all-targets --
-D warnings` clean
- [ ] CI green

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 05:06:57 -04:00
Bryan Helmkamp
2ba04be181
feat(template): add source-aware diagnostics (#292)
## Summary

Template failures from `fabro run` and structural warnings from `fabro
validate` now preserve source provenance through rendering, workflow
transforms, API serialization, and CLI display. Diagnostics can point at
the actual workflow, import, or prompt file with node/attribute context
instead of surfacing MiniJinja's generic `<string>` source.

## What Changed

- Added named MiniJinja render APIs plus miette-aware `TemplateError`
metadata for source names, source text, spans, and labels.
- Reworked workflow template expansion so inline attributes, imported
workflows, and `@prompt` files render with file and owner context.
- Split strict run behavior from structural validate behavior: run-start
still hard-fails on missing inputs, while validate emits source-aware
warnings and continues linting.
- Extended validation diagnostics through Rust structs, OpenAPI, server
DTO mapping, and CLI rendering with optional source path, line, column,
span, and related metadata.
- Added regression coverage across template rendering, workflow
transforms, CLI output, and the server validate endpoint.

## Verification

- `cargo nextest run -p fabro-template`
- `ulimit -n 4096 && cargo nextest run -p fabro-workflow --no-fail-fast`
- `cargo nextest run -p fabro-cli
bare_fabro_with_unbound_inputs_validates_structurally_with_warning
run_rejects_unbound_template_inputs_before_creating_remote_run`
- `cargo nextest run -p fabro-server
validate_endpoint_returns_template_source_coordinates`
- `cargo build -p fabro-api`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---

[![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)

---------

Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
2026-05-16 18:47:37 -04:00
Bryan Helmkamp
e9387bf622
test(server): isolate storage in interrupt_active_session test
The test was failing in CI because parallel tests share the default
session storage directory ($HOME/.fabro/storage). Every AppState build
runs `recover_stale_running_state`, which scans that directory and
re-marks any in-flight Running turn as Interrupted. When another test
built its AppState while this test's turn was running, the recovery
clobbered the turn before the interrupt request arrived, causing a 409
"Turn is already terminal" response.

Give the test its own storage root and wait for `turn.assistant_text_start`
so the agent is committed to the in-flight LLM call before interrupting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:11:10 -04:00
Bryan Helmkamp
2b53917759
fix(validate): treat undefined template vars in @file prompts as warnings (#290)
## Summary

`fabro validate` had inconsistent behavior for undefined template
variables depending on whether the prompt was inline or loaded via an
`@file` reference. Inline `{{ inputs.foo }}` produced a warning and
validation passed; the same expression inside a `@file`-imported prompt
produced a hard validation error.

Fixes #286.

## Root cause

Two template-rendering passes with different strictness, applied to
disjoint inputs:

1. **DOT-source pass**
(`lib/crates/fabro-workflow/src/operations/create.rs`) honored
`RenderMode::Structural` for `fabro validate` — undefined variables
downgraded to a `Severity::Warning` diagnostic, then lenient render
finished the job.
2. **Per-attribute pass**
(`lib/crates/fabro-workflow/src/transforms/variable_expansion.rs`)
inside `TemplateTransform` was always strict and had no `RenderMode`
awareness. Because `FileInliningTransform` runs *before*
`TemplateTransform`, expressions inside `@file` content only ever
encountered the strict pass.

## Fix

- Plumb `RenderMode` through `TransformOptions` into
`TemplateTransform`.
- In `RenderMode::Structural`, the transform catches
`TemplateError::UndefinedVariable` per attribute, emits a warning
diagnostic, and falls back to `render_lenient`.
- Diagnostics flow through a new `Transformed.diagnostics` field into
`Validated` alongside lint output.
- Diagnostics now include `node_id` when the undefined variable was
found inside a node attribute, which is more useful than the previous
"at line 1" location.
- `RenderMode` and the shared `template_undefined_variable_diagnostic`
helper moved to `pipeline/types.rs` so the transform layer can reach
them without a circular dep.

Strict mode (`fabro run`, preflight) is unchanged — undefined inputs
still hard-fail before a run is created.

## Behavior

Illustrative output shapes (variable names and line numbers depend on
the fixture):

Inline prompt (unchanged):
```
warning: undefined template variable `inputs.<name>` at line <n> (template_undefined_variable)
Validation: OK
```

`@file`-imported prompt (previously a hard error, now matches inline —
node-attributed instead of line-attributed):
```
warning [node: <id>]: undefined template variable `inputs.<name>` in node `<id>` (template_undefined_variable)
Validation: OK
```

## Test plan

- [x] `cargo nextest run --workspace` — 5773/5773 passing
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings` clean
- [x] `cargo +nightly-2026-04-14 fmt --check --all` clean
- [x] New regression test
`bare_fabro_with_unbound_inputs_in_imported_prompt_validates_structurally_with_warning`
in `lib/crates/fabro-cli/tests/it/cmd/validate.rs` against new fixture
`test/templated_unbound_imported/`
- [x] Existing
`bare_fabro_with_unbound_inputs_validates_structurally_with_warning` and
`strict_render_hard_fails_on_unbound_inputs` still pass — verifies
inline structural and run-start strict behavior are both preserved
- [x] Manual reproduction of the exact inputs from the issue now
succeeds with a warning

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

---------

Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:31:22 -04:00
Bryan Helmkamp
56c7f627c4
feat(session): add server-backed agent sessions (#278)
## Summary

Adds the first server-backed Fabro agent session slice: persistent
session records, durable turn/event storage, HTTP session APIs, SSE turn
streaming, generated clients, and a new `fabro session -p <prompt>` CLI
path.

## What Changed

- Adds shared session IDs, records, statuses, event envelopes, and
message DTOs in `fabro-types`, with OpenAPI replacements in `fabro-api`.
- Renames the agent runtime transcript item from `Turn` to `Message` and
adds conversion between runtime history and persisted `SessionMessage`
records.
- Introduces a file-backed `SessionStore` for session metadata, turns,
full transcripts, and append-only events under local storage.
- Wires server session routes for create/list/read/update/delete, turn
submission, event replay, interrupt requests, and session-scoped tools.
- Implements streamed turn execution with durable events persisted
before SSE broadcast, active-turn conflict handling, local same-machine
`working_dir` validation, and noninteractive permission denials.
- Adds `fabro-client` helpers and the `fabro session -p` command, plus
regenerated TypeScript API client files.

## Notes

V1 intentionally keeps session execution local to same-machine server
targets. Remote clone-backed session sandboxes, interactive REPL/TUI
behavior, warm session pooling, and real tool discovery for
`/sessions/{id}/tools` remain follow-up work.

## Verification

- `cargo build --workspace`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-store session_store_contract_tests --lib`
- `cargo test -p fabro-agent
history::tests::session_message_roundtrip_preserves_runtime_history
--lib`
- `cargo test -p fabro-server 'session_' --lib`
- `cargo test -p fabro-server --features test-support --test it
openapi_conformance -- --nocapture`
- `cargo test -p fabro-cli --test it cmd::session:: -- --nocapture`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `git diff --check`

---

[![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-16 17:25:05 -04:00
Bryan Helmkamp
4f5e3b78f8
refactor: remove compatibility shims (#281)
## Summary
Simplifies the greenfield PR/run schema surface by collapsing alias-only
type shims and removing legacy compatibility paths that kept old wire
shapes and workflow names alive.

## Changes
- Use canonical `Run`, `PullRequestLink`, `PullRequestResponse`,
`BoardColumn`, `WorkflowSettings`, SWR `Key`, and `SteerRunRequest`
names directly across Rust and web code.
- Remove legacy PR/event deserialization compatibility for old PR
records and command output fields, with tests updated to reject stale
wire shapes.
- Drop obsolete workflow aliases for `agent_loop`, `one_shot`,
`codergen_mode`, and `stack.child_dotfile`, then update docs and tests
to the current names.

## Verification
- `git diff --check`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-types -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-workflow -p fabro-cli`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`

---

[![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)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:29:25 -04:00