Commit graph

3717 commits

Author SHA1 Message Date
fabro-sh-0530[bot]
bd837fc0f0
Add POST /api/v1/providers/test endpoint (#406)
## Summary

Adds `POST /api/v1/providers/test` so API, CLI, and UI callers can check
LLM provider health without parsing `/health/diagnostics`. The endpoint
tests every configured provider once using the catalog probe model and
returns typed, per-provider results with an aggregate summary — all at
HTTP 200, with provider failures expressed in the body.

This PR also adds `scheduler_slots_used` to `SystemRunCounts` to
distinguish runs occupying concurrency slots from all "active" runs
(e.g. runs blocked waiting for human input count as active but do not
hold a scheduler slot).

### What changed and why

**Provider probe logic** (`diagnostics.rs`)

The inline probe loop inside `check_llm_providers` was extracted into
`test_llm_providers` / `probe_single_provider`, which both the new
endpoint and the existing diagnostics check now share. The extraction
preserves the diagnostics output format: `diagnostic_detail` (a
`#[serde(skip)]` field) carries the richer context string used for the
`LLM Providers` section, while `error_message` carries the redacted,
public-facing error.

Key decisions:
- `ProviderProbeStatus` is `ok | error` only — no `skip`, because v1
only iterates configured providers.
- `model_id` is nullable so auth/registration failures (where no probe
was sent) can be expressed cleanly.
- API key values appearing in upstream error responses are passed
through `redact_string` before being stored in `error_message`.

**Route** (`handler/models.rs`)

`.route("/providers/test", post(test_providers))` added alongside
`/providers`, protected by the same `RequiredUser` extractor.

**`scheduler_slots_used`** (`handler/system.rs`, `server.rs`)

The status predicate (`Starting | Running | Blocked | Paused`) was
already duplicated between the scheduler loop and `get_system_info`.
It's now a named function `counts_toward_scheduler_capacity`, used in
both places and in the new `SystemRunCounts` field. The web UI
monitoring panel was updated to display "slots used" instead of
"active."

**Generated clients**

OpenAPI spec updated; Rust and TypeScript clients regenerated. New
TypeScript types: `ProviderTestList`, `ProviderTestResult`,
`ProviderTestStatus`, `ProviderTestSummary`.

### Plan Summary

- Add `testProviders` OpenAPI operation and `ProviderTestList` /
supporting schemas to `fabro-api.yaml`.
- Extract shared `test_llm_providers` from `check_llm_providers` in
`diagnostics.rs`; keep diagnostics output identical.
- Wire `POST /providers/test` handler in `handler/models.rs`.
- Add `scheduler_slots_used` to `SystemRunCounts` and extract
`counts_toward_scheduler_capacity` predicate.
- Regenerate Rust and TypeScript API clients.
- Add integration tests covering: no providers, successful probe, auth
failure (no upstream call), registration failure, mixed catalog order,
and API key non-leakage.


### Fabro Details

<details>
<summary>Ran 8 stages in 53m 33s for $40.83</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 8s | – | 0 |
| preflight_lint | 2m 19s | – | 0 |
| implement | 23m 56s | $30.73 | 0 |
| simplify_opus | 12m 53s | $5.80 | 0 |
| simplify_gpt | 2m 40s | $4.30 | 0 |
| verify | 9m 5s | – | 0 |
| **Total** | **53m 33s** | **$40.83** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-25 21:57:26 -04:00
Bryan Helmkamp
4faa34059d
feat(workflow): add goal workflow (#407)
## Summary

Adds a reusable `goal` workflow that runs an immutable user goal through
a Work -> Completion Audit loop. The work prompt keeps the full
objective intact, while the audit prompt uses validated routing JSON to
either exit when the goal is proven complete or loop back with concrete
remaining work.

## Workflow Diagram

![Goal workflow
diagram](https://raw.githubusercontent.com/fabro-sh/fabro/2069a692e3d681c41afbfeca0ee86a604151475a/.fabro/workflows/goal/workflow.svg)

## Verification

- `cargo run -q -p fabro-cli -- validate
.fabro/workflows/goal/workflow.fabro`
- `cargo run -q -p fabro-cli -- run goal --goal "Test the reusable goal
workflow" --dry-run`
- `cargo run -q -p fabro-cli -- preflight
.fabro/workflows/goal/workflow.toml --goal "Test the reusable goal
workflow"`
- `xmllint --noout .fabro/workflows/goal/workflow.svg`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via Codex
2026-05-25 21:57:02 -04:00
Bryan Helmkamp
02a87cb650
fix(settings): show scheduler slot usage (#404)
## Summary

Fixes the Settings Resources concurrency meter so it reports scheduler
capacity usage instead of all non-terminal runs. `/api/v1/system/info`
now exposes `runs.scheduler_slots_used`, computed from the same status
predicate the scheduler uses, while `runs.active` remains unchanged for
existing lifecycle semantics.

The settings page uses only the new slot count, so pending approval runs
and runnable queued runs no longer make the concurrency meter look full.

## Verification

- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-server --features test-support
worker_started_child_run_requires_approval_before_becoming_runnable`
- `cargo nextest run -p fabro-server --features test-support
scheduler_capacity_counts_only_runs_occupying_slots`
- `cargo nextest run -p fabro-server --features test-support
get_system_info_returns_runtime_fields`
- `cargo nextest run -p fabro-server --features test-support
test_app_state_with_options_respects_max_concurrent_runs`
- `cargo nextest run -p fabro-server --features test-support
openapi_conformance`
- `bun test app/routes/settings-monitoring.test.tsx`
- `bun run typecheck`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context unknown, reasoning unknown) via
[Codex](https://openai.com/codex)
2026-05-25 18:28:18 -04:00
Bryan Helmkamp
1b8dcd41de
fix(web): hide runs list pager when pagination isn't relevant
Show the pager only when there's actually more than one page or the
user is past page 1, replacing the hardcoded total >= 25 threshold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:35:14 -04:00
Bryan Helmkamp
a11f242b3f
feat(web): remove time filter and archived toggle from run children tab
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:30:48 -04:00
fabro-sh-0530[bot]
e8f0aceee8
refactor: rationalize server secret scopes (vault-only for optional int… (#401)
## Summary

Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.

## What changed

**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.

**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.

**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).

**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.

**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.

**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.

**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.

### Plan Summary

- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.


### Fabro Details

<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-25 17:26:01 -04:00
Bryan Helmkamp
bb3f4bd62b
feat(model): add gemini-3.5-flash (#403)
## Summary

Updates the built-in Gemini catalog for the current Gemini API lineup by
adding `gemini-3.5-flash` and promoting `gemini-3.1-flash-lite` to the
canonical small default. The old `gemini-3.1-flash-lite-preview` ID
remains accepted as an alias and resolves to the stable API ID, avoiding
a breaking change for existing workflows.

The catalog test changes remove Gemini-specific data assertions and keep
only a generic small-default invariant, so future declarative catalog
updates do not require Rust test churn.

## Verification

- `cargo nextest run -p fabro-model`
- `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 (unknown context, default reasoning) via
[Codex](https://openai.com/codex)
2026-05-25 17:22:32 -04:00
Bryan Helmkamp
705079a271
feat(web): label priority provider and default models on settings (#402)
## Summary

Adds three small informational labels to `/settings/models`, all driven
from existing fields on `Provider` and `Model` (no API changes):

- **Priority** — on the configured provider with the highest catalog
`priority`
- **Default** — next to each provider's default model (`model.default`)
- **Small** — next to models flagged as the provider's small default
(`model.small_default`)

A single shared `Label` helper renders them in a subtle uppercase pill
style consistent with other section accents on the page.

## Test plan

- [ ] Visit `/settings/models` and confirm one configured provider shows
a "Priority" label next to its name
- [ ] Confirm each provider has at most one model labeled "Default" in
the Models table
- [ ] Confirm models with `small_default = true` show a "Small" label
(alongside "Default" if both)
- [ ] Confirm unconfigured providers are unaffected (filtered out before
the Models table)

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:10:37 -04:00
Bryan Helmkamp
e04979126b
test(server): remove stale routing import 2026-05-25 16:51:41 -04:00
Bryan Helmkamp
2c851d5a41
Merge remote-tracking branch 'origin/main' 2026-05-25 15:50:43 -04:00
fabro-sh-0530[bot]
b15d8b8476
feat: Add approve/deny run controls to MCP and CLI (#400)
## Summary

Exposes the existing `POST /api/v1/runs/{id}/approve` and `POST
/api/v1/runs/{id}/deny` REST endpoints through the `fabro_run_interact`
MCP tool and two new top-level CLI commands (`fabro approve`, `fabro
deny`). Workflow agents are explicitly blocked from using these actions
— approval remains a human/user operation.

## What changed

**Client & tool backend** (`fabro-client`, `fabro-tool`): Added
`approve_run` and `deny_run` to `Client` and the `FabroToolBackend`
trait, implemented in `ClientBackend`. `deny_run` passes a
`DenyRunRequest` body; absent, blank, or whitespace-only reasons are
normalised to `None`.

**`fabro_run_interact` MCP tool**: Added `Approve` and `Deny` variants
to `RunInteractAction` / `ValidatedInteractAction`, and an optional
`reason` parameter (only valid for `deny`; validated and trimmed on
input). Both actions return `{ "summary": … }` using the existing shape.
The tool description is updated to list the new actions.

**Workflow-agent guard** (`fabro-workflow`): Before dispatching
`fabro_run_interact`, the handler checks
`validated.action.requires_user()`. If the action is `approve` or
`deny`, it returns an immediate `ToolError` without ever reaching the
backend, keeping the guard explicit and independent of server auth.

**CLI** (`fabro-cli`): Extracted the archive/unarchive batch loop into a
shared `run_resolved_run_batch` helper in `commands/runs/mod.rs`, then
implemented `approval.rs` using the same helper. Both commands follow
the same batch contract as archive: attempt all runs, collect per-run
errors, exit non-zero if any fail, and emit `{ "approved"/"denied": […],
"errors": […] }` in JSON mode.

**Server auth regression** (`fabro-server`): Extended
`run_tools_worker_cannot_call_user_only_non_mcp_routes` to cover `POST
/runs/{id}/deny` alongside the existing `approve` and `timeline` checks.

**Docs** (`mcp.mdx`, `cli.mdx`): Updated the `fabro_run_interact` table
entry and added approve/deny examples, plus reference sections for the
two new CLI commands.

### Plan Summary

- Add `approve_run` / `deny_run` to `Client` and `FabroToolBackend`
- Extend `fabro_run_interact` with `approve`, `deny`, and optional
`reason`
- Block workflow-agent self-approval with an early `ToolError`
- Refactor archive batch loop into shared `run_resolved_run_batch`
helper
- Add `fabro approve` and `fabro deny` CLI commands reusing that helper
- Add integration tests for CLI commands, MCP tool, and server auth
guard


### Fabro Details

<details>
<summary>Ran 9 stages in 63m 57s for $42.33</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 28m 16s | $32.57 | 0 |
| simplify_opus | 10m 59s | $4.24 | 0 |
| simplify_gpt | 6m 13s | $3.96 | 0 |
| verify | 10m 51s | – | 0 |
| fixup | 2m 29s | $1.55 | 0 |
| **Total** | **63m 57s** | **$42.33** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-25 15:49:57 -04:00
Bryan Helmkamp
879969cf54
docs: add child runs guide
Document child-run orchestration as a first-class execution concept and link the related MCP, UI, and API surfaces back to it.
2026-05-25 15:43:53 -04:00
Bryan Helmkamp
2a2b410802
feat: remove demo-mode toggle button and endpoint
Demo mode remains available via the X-Fabro-Demo header or the
fabro-demo=1 cookie set manually in browser devtools, but the UI
button and the POST /api/v1/demo/toggle endpoint are gone. The
fixture machinery and the auth/me demoMode flag (used by the SPA to
render Automations and the /start landing) are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:35:37 -04:00
Bryan Helmkamp
b4434af951
feat(web): add models table to /settings/models
Mirrors `fabro model list` output below the existing Providers panel.
Server-side provider + query filters, debounced search, sortable
columns, and a hover/focus popover that surfaces model aliases.

Genericizes SortHeader so non-runs tables can reuse it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:21:40 -04:00
Bryan Helmkamp
3d8ca45d18
feat(web): add /settings landing page with linked overview cards
Replace the Models default with an overview landing at /settings that
shows each settings page as a card with icon, name, and one-line
description, grouped by General / Administration with a divider before
Live Events. Settings nav metadata is restructured into navSections and
exported so the sidebar and landing share a single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:04:10 -04:00
Bryan Helmkamp
a508ee3d2e
feat(web): regroup settings nav under General and Administration
Group sidebar items under General and Administration section labels;
default Settings landing page to Models; rename General page to Server
(now at /settings/server); rename Resources to Monitoring (now at
/settings/monitoring) with ChartBarSquare icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:56:01 -04:00
Bryan Helmkamp
67d8c5906e
fix(web): keep diff stats inline with elapsed on board cards
PrCard stacked stats, actions+elapsed, and diff stats as three sibling
rows, so +adds/-dels rendered below elapsed. Consolidate into a single
PrCardFooter component so future inline metadata extends one row instead
of stacking another.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:21:26 -04:00
Bryan Helmkamp
acf8caa351
feat(web): add sortable Size column to runs list
Surfaces the run t-shirt size (XS/S/M/L/XL) in both the main runs
list and the Children sub-tab, visible by default. L renders in
amber and XL in coral to flag risky and unhealthy runs at a glance.

Extracts a shared SizeChip component used by the run header and the
table cell, derives Ord on RunSize so the new sort key (server-side
ListRuns sort) orders by bucket, and reorders TOGGLEABLE_COLUMNS so
the column picker mirrors the visible table order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:07:17 -04:00
Bryan Helmkamp
d69198bc30
feat(web): hide Elapsed by default in favor of Size
Now that Size is a first-class column in the runs list, Elapsed is
redundant with it for at-a-glance scanning. Hide Elapsed by default
alongside Updated and Changes; users can still reveal it via the
column picker.

Existing users with stored prefs from the previous "updated,changes"
default keep their stored value, so they'll see both Elapsed and Size
until they toggle Elapsed off (or clear localStorage). New users get
the cleaner default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:03:59 -04:00
Bryan Helmkamp
855d73630b
refactor(web): remove unused RunRow + RUNS_LIST_GRID_TEMPLATE
The grid-based RunRow was the only Children-tab consumer of the
runs-list module's row primitive. When Children adopted the full
RunsListView (table layout) in 4dfcbc0e0, RunRow became unused — the
re-export in runs.tsx was preserved for a release as a precaution, but
nothing imports it. Same for RUNS_LIST_GRID_TEMPLATE, which only the
grid RunRow needed.

Note: automation-runs.tsx still defines its own local RunRow with the
same name; that one is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:39:17 -04:00
Bryan Helmkamp
febd5e2d2e
feat(web): hide Updated/Changes by default, reorder Elapsed after Updated
Updated and Changes are now hidden by default in both the main runs list
and the Children sub-tab — they're still toggleable via the column
picker. The column order shifts so Elapsed lives between Updated and
Changes (i.e. after Created/Updated), keeping the time-related columns
grouped on the right.

Defaults are applied in two places: fresh sessions (no stored prefs)
and existing v1 stored prefs that have no `hide` field. Users who
explicitly cleared all hides keep that choice; stored `hide: ""`
serializes round-trip as `?hide=` (empty value) so the URL distinguishes
"show every column" from "use defaults".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:31:47 -04:00
Bryan Helmkamp
1724cbadcd
fix(web): wire diff stats into list rows and format consistently
The list-view Changes column was always empty because mapRunListItem
never copied additions/deletions from the API's diff payload. Populate
them so rows actually render +/- counts.

The run overview's Changes cell was rendering raw numbers; switch it to
toLocaleString() so it matches the list view's formatting.

Also tighten tabCountBadges in the run-detail test to scope to the
tab-strip's rounded-full badges. The previous selector matched any
tabular-nums span, so the unconditional size chip caused a false
positive in "hides the Files Changed tab badge when diff stats are
absent" after the chip went unconditional in 7d4aa474f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:19:51 -04:00
Bryan Helmkamp
7d4aa474fc
fix(web): always show run size chip in run header
The size chip was gated on billing.total_usd_micros, so it only
appeared after a run reached a terminal state. summary.size is
always present, so render the chip unconditionally and only append
the billed amount to the tooltip when available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:16:11 -04:00
Bryan Helmkamp
57d7d868c1
feat(web): add per-row actions menu to run lists
Each row in the main runs list and the Children sub-tab now has a
vertical-kebab actions menu, giving a one-click path to manage a single
run without opening it. The menu mirrors the run detail page's Actions
menu but only surfaces the actions that apply to the row's current
state — Approve/Deny for runs awaiting approval, Retry for failed/dead,
Archive for terminal, Unarchive/Delete for archived, Cancel for
in-flight. Copy run ID is always available.

Delete uses the same confirmation dialog as the bulk and detail flows.
Other actions fire directly and broadcast via mutateRunListCaches so
the list refreshes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:08:23 -04:00
Bryan Helmkamp
dca4369253
feat(web): add More menu with Approve + Delete to bulk toolbar
The bulk action toolbar grows: the top-level buttons stay focused on
the common archive/unarchive flow, and an overflow "More" menu now
houses Approve (new) and Delete (moved from the top level).

Approve fans out client-side via Promise.allSettled since there's no
batch approve endpoint yet; the result is reported through the same
summarizeBatchLifecycleAction toast as the other batch actions. Only
runs whose lifecycle.approval.state === "pending" are eligible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:57:05 -04:00
Bryan Helmkamp
fa44035a24
feat(web): hide steer bar on the Children sub-tab
The Children tab's bulk action toolbar sits at the bottom of the page,
where the SteerBar was overlapping it. Add a `hideSteerBar` flag to the
Children route handle and skip rendering the fixed bottom bar in
run-detail when set. Keep the InterviewDock visible when there are
pending questions so urgent prompts aren't swallowed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:51:41 -04:00
Bryan Helmkamp
4dfcbc0e03
feat(web): upgrade Children sub-tab to use the runs list view
The /runs/:id/children tab now gets server-side pagination, sortable
columns, column picker, search/time/archived filters, and bulk
archive/unarchive/delete — same affordances as the main runs list view.
Preferences persist to localStorage under a dedicated key so they don't
collide with the /runs page.

Repo and Workflow filter buttons are intentionally omitted (children
typically share these with the parent), but those columns remain visible
for the cases where workflows fan out across repos.

- New childRunsListPreferences in components/runs-list/preferences.ts
- run-children.tsx fetches via useRunsPage({parentId, ...}) with all
  list controls wired up
- Empty state retains the existing "Learn about parent links" CTA
- useChildRuns + queryKeys.runs.children removed (replaced by the
  generalized useRunsPage)
- useRetryRun broadcasts via mutateRunListCaches now that the dedicated
  children cache key is gone
- Revert board-cache children matcher added in the previous commit
  (no longer needed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:48:18 -04:00
Bryan Helmkamp
d59f97d6a0
refactor(web): extract runs list view into reusable module
Move the inline runs list view (pagination, sorting, column picker,
selection, bulk actions) from runs.tsx into a self-contained module at
components/runs-list/ so it can be reused by the Children sub-tab and
future run-list surfaces. No behavior change to /runs.

- RunsListView now takes an emptyState slot (Runs page passes RunsLandingEmpty)
- useRunsPage accepts an optional parentId for non-page run lists
- runListCacheMatchers also matches ["runs","children",...] so bulk
  archive/delete invalidate children caches automatically

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:40:01 -04:00
Bryan Helmkamp
290a1a4168
Bump version to 0.244.0 2026-05-25 12:31:12 -04:00
Bryan Helmkamp
7cc38e2f8d
feat(web): promote Approve to dedicated buttons
Pending approval is the only action a user can take to unblock a run,
so the Approve action shouldn't be buried in the Actions dropdown.

- Run detail header: render a primary teal "Approve" button beside the
  Actions menu when approval is pending; remove the duplicate menu item.
- Board view (/runs): surface `pendingApproval` on RunItem and render
  an inline Approve button on cards in the Pending column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:57:19 -04:00
fabro-releases[bot]
3cd8a670d4 Bump version to 0.244.0-nightly.0 2026-05-25 15:32:33 +00:00
Bryan Helmkamp
47d8d6365b
docs: refresh generated CLI reference 2026-05-25 11:13:01 -04:00
Bryan Helmkamp
3ebea2413b
docs: clarify structured output fallbacks 2026-05-25 10:56:53 -04:00
Bryan Helmkamp
a306dac381
test(cli): stabilize model list snapshots
Move exact model-list table rendering coverage to a fixed mocked catalog response so real catalog metadata edits do not churn CLI snapshots.
2026-05-25 10:20:47 -04:00
Bryan Helmkamp
245052db38
feat(cli): allow rendering invalid graphs
Keep graph validation diagnostics visible, but let users opt into rendering DOT workflows that fail semantic validation with --allow-invalid.
2026-05-25 10:19:32 -04:00
Bryan Helmkamp
8c6b187cb7
fix: satisfy run state clippy lint 2026-05-25 10:00:35 -04:00
Bryan Helmkamp
05c57a5150
fix: clear stale conclusions on resume
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
Resume attempts must not inherit the prior terminal conclusion. Leaving it in the projection made attach and summaries treat a live resumed run as already failed.
2026-05-24 18:47:04 -04:00
Bryan Helmkamp
cf5c71e255
fix: allow resuming cancelled runs 2026-05-24 18:35:34 -04:00
Bryan Helmkamp
069c6baa58
Allow retrying cancelled runs
Cancelled runs are now eligible for retry alongside other failed and
dead runs. A user who cancels a run and then changes their mind no
longer has to manually re-create it from scratch.

- ensure_retryable drops the FailureReason::Cancelled rejection arm
- canRetry simplifies to failed || dead (still gated by !archived)
- OpenAPI Retry Run description no longer lists cancelled as ineligible

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:28:16 -04:00
Bryan Helmkamp
92288eded4
Use Codex-safe OpenAI context windows
Align Fabro's built-in OpenAI GPT-5 catalog defaults with Codex's 272k context window so Codex-backed runs compact before provider rejection. Direct API users can still raise the window through catalog overrides.
2026-05-24 18:21:22 -04:00
Bryan Helmkamp
97dd28b356
Add tool descriptions as tooltips in sidebar Tools list
- Loosen vertical spacing between tool rows (space-y-1 → space-y-1.5).
- Add `title={tool.description}` so hovering a tool surfaces the
  model-facing description without re-adding inline description text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:00:50 -04:00
Bryan Helmkamp
e8055a1317
Project subagent OpenAI plan when no root plan exists
PR #384 unconditionally dropped TODO events from child sessions to stop
a subagent's update_plan from overwriting the root agent's plan in
StageProjection.todos. But when the root never emits a plan and a
subagent owns the only one (e.g. delegating gpt-5.5 stages), every TODO
event was filtered, the projection stayed None, and the web sidebar
hid its Todos section.

Narrow the guard: a child OpenAI plan event is now rejected only when
it would replace an existing list with a different list_id. Empty
slots and same-list continuations pass through, so a subagent plan
projects when nothing else owns the slot, and a later root plan still
takes over via the existing list_id replacement path.

Regression test mirrors the failing run (implement@1 on
01KSDXK5DJ61CFCK9YSDR8AETQ). The PR #384 canary still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:55:29 -04:00
Bryan Helmkamp
aa1613e95a
Simplify agent sidebar: drop permission badge, slim Tools list
- Remove the "Full Access" / permission badge section; Tools now conveys
  the same surface area more directly.
- Reorder so Tools sits at the bottom (after MCPs).
- Strip each tool row to just a used/not-used indicator and the tool
  name — no descriptions, source labels, or category badges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:47:39 -04:00
Bryan Helmkamp
e68282466e
Add sandbox providers settings page
New /settings/sandboxes route surfaces the local, docker, and daytona
runtime sandbox providers using existing useServerSettings(). Mirrors
the /settings/models pattern: enabled providers shown first, disabled
hidden behind a progressive-disclosure toggle. Disabled Daytona row
links to add the DAYTONA_API_KEY secret.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:47:15 -04:00
Bryan Helmkamp
7efa38c384
Add icons to Ask Fabro preset prompts
Gives each of the four empty-state suggestions a Heroicon to aid
scannability: warning triangle for "Surface errors", bolt for
"Analyze performance", map for "Review key decisions", and lightbulb
for "Suggest improvements".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:30:14 -04:00
Bryan Helmkamp
f5e5f9c75c
Improve Slack setup and lifecycle notifications (#391)
## Summary

This improves Slack support from setup through day-to-day operator
visibility. The server now resolves Slack credentials through the same
`process env -> server.env` path as other server secrets and logs
whether Slack is enabled or which credential variables are missing.

Run lifecycle notifications now use a structured Block Kit layout
instead of one dense markdown section. Messages get a header, workflow
summary, metadata fields, optional failure details, optional PR
metadata, and an Open in Fabro context link.

The public docs now explain local `server.env` setup, Docker/process-env
setup, expected startup logs, lifecycle smoke testing, terminal
`run.failed` semantics, and the current route-based
`[run.notifications]` format.

## Validation

- `cargo test -p fabro-slack`
- `cargo check -p fabro-server`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo dev docs check`
- `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-24 16:54:25 -04:00
fabro-sh-0530[bot]
2c0416e1c5
feat: add server sandbox provider enablement policy (#389)
Operators can now disable individual sandbox providers at the server
level via `[server.sandbox.providers.<provider>]` in `settings.toml`,
without breaking existing deployments that omit the section entirely.

## What changed

**Config layer & resolution** (`fabro-config`, `fabro-types`): new
sparse `ServerSandboxLayer` / `ServerSandboxProvidersLayer` /
`ServerSandboxProviderLayer` structs with `deny_unknown_fields` parse
validation. Resolution defaults every missing level to `enabled = true`.
The resolved `ServerSandboxSettings` / `ServerSandboxProvidersSettings`
/ `ServerSandboxProviderSettings` types live in `fabro-types` and are
shared by all consumers.

**Policy enforcement** (`fabro-server`): three check points enforce the
effective provider (after dry-run Local coercion):
1. `POST /api/v1/runs` — 400 at admission.
2. `POST /api/v1/runs/preflight` — `ok: false` with a `Sandbox Provider
Policy` error check.
3. Launch (`execute_run_in_process` / `execute_run_subprocess`) —
fail-before-execution with a `LaunchFailed` reason.

The dry-run coercion logic was extracted into
`SandboxProvider::effective_for(mode)` on the type itself and reused
across `fabro-server` and `fabro-workflow`.

**Installer** (`fabro-install`): `write_sandbox_settings` now always
writes all three provider policy tables with `enabled = true`, so
generated `settings.toml` files are self-documenting.

**API schema & clients**: `ServerNamespace` gains a required `sandbox`
field in the OpenAPI spec; three new TypeScript model files were
regenerated accordingly.

### Plan Summary
- Task 1: config layer structs → resolved types → resolver helpers →
tests
- Task 2: `effective_sandbox_provider` + `sandbox_provider_policy_error`
helpers; admission, preflight, and launch checks + integration tests
- Task 3: installer writes all three provider entries; install finish
tests updated
- Task 4: OpenAPI schema, `fabro-api` build mappings, TS client
regeneration, docs


### Fabro Details

<details>
<summary>Ran 8 stages in 59m 15s for $45.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 5s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 27m 55s | $38.88 | 0 |
| simplify_opus | 14m 20s | $4.93 | 0 |
| simplify_gpt | 3m 14s | $1.88 | 0 |
| verify | 8m 49s | – | 0 |
| **Total** | **59m 15s** | **$45.70** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 16:49:05 -04:00
fabro-sh-0530[bot]
7d655d7c95
feat: expose effective agent tool list via StageProjection.agent_tools (#388)
## Summary

Adds `StageProjection.agent_tools` — a replay-authoritative list of
every tool the model can actually call — so UI and API consumers no
longer have to infer tool availability from `permission_level`. The
field is populated by a new `agent.tools.available` durable event
emitted once per stage session after provider-profile setup, MCP
integration, and access-policy filtering are complete.

### Plan Summary

- **Types (`fabro-types`)** — new `AgentToolSummary`, `AgentToolSource`,
`AgentToolCategory`, `AgentToolsAvailableProps`, and
`EventBody::AgentToolsAvailable`; `agent_tools: Vec<AgentToolSummary>`
added to `StageProjection` with skip-serializing-if-empty semantics.
- **Tool registry (`fabro-agent`)** — `ToolSource::Mcp` gains
`original_name` (no more re-parsing the qualified name downstream);
`ToolDefinitionWithSource::to_agent_tool_summary()` maps to the public
DTO; `Session::effective_tools()` / `agent_tool_summaries()` expose the
filtered list; `tool_category` split into `tool_category` (CLI gate,
defaults `Shell`) and `known_tool_category` (projection, returns `None`
→ `Other` for unknown tools).
- **Projection reducer (`fabro-store`)** — `AgentToolsAvailable`
replaces the stage's `agent_tools`; `AgentToolStarted` flips `invoked =
true` on the matching entry; legacy runs without the event get an empty
list.
- **OpenAPI + generated clients** — `AgentToolSummary`,
`AgentToolSource`, `AgentToolCategory`, `AgentToolsAvailableProps`
schemas added; `StageProjection.agent_tools` field added; `build.rs`
replacements wire them to the `fabro-types` structs.
- **Web sidebar** — new collapsible "Tools" section renders name,
description, source/category badge, and used/available state from
`stage.agent_tools`; `permission_level` is kept as secondary fallback
metadata for legacy stages.

## Key design decisions

- **`agent_tools`, not `tools`** — avoids ambiguity with MCP nested
tools and completion API tool definitions.
- **Dedicated `agent.tools.available` event** — cleaner than overloading
`agent.session.activated`; replacement semantics on replay mean
re-emission works if tool registration ever becomes mutable.
- **`original_name` carried in `ToolSource::Mcp`** — stored by the MCP
integration at registration time so the projection never needs to
re-parse qualified names like `mcp__filesystem__read_file`.
- **`invoked` is projected state, not event state** — the availability
event always emits `false`; replay of `agent.tool.started` flips
matching entries.
- **Parameter schemas omitted** — `AgentToolSummary` carries only
`name`, `description`, `source`, `category`, and `invoked` to keep
payloads small and avoid exposing implementation detail.
- **`AgentToolCategory::Other` for unknown tools** — unlike the CLI
permission gate (which defaults to `Shell` to require approval), the
projection uses `Other` to surface unrecognized MCP/skill tools
accurately.


### Fabro Details

<details>
<summary>Ran 8 stages in 56m 37s for $58.75</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 0s | – | 0 |
| preflight_lint | 2m 15s | – | 0 |
| implement | 22m 54s | $42.64 | 0 |
| simplify_opus | 16m 41s | $10.62 | 0 |
| simplify_gpt | 3m 43s | $5.49 | 0 |
| verify | 8m 33s | – | 0 |
| **Total** | **56m 37s** | **$58.75** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 16:41:55 -04:00
fabro-sh-0530[bot]
883a11ce4d
feat: MCP tool parity for fabro_tools workflow agents (#387)
## Summary

Workflow agents that opt in with `[run.agent] fabro_tools = true` now
see the full seven-tool Fabro run-management catalog — including
`fabro_run_pair` — matching what human MCP clients receive. Auth
extractors have been renamed from the ad-hoc "run tools" vocabulary to
product-level names, and all pair routes now accept run-management
actors instead of requiring a user principal.

### Plan Summary

- **Shared catalog parity** — `FABRO_RUN_PAIR_TOOL_NAME` added to
`TOOL_DEFINITIONS` in `fabro-tool`, making `register_fabro_run_tools()`
register all seven tools.
- **Workflow agent executor** — new `FABRO_RUN_PAIR_TOOL_NAME` match arm
in `execute_fabro_run_tool` parses `FabroRunPairParams`, calls
`fabro_tool::pair_run`, and renders the standard summary.
- **Auth extractor rename** — `RequiredRunToolActor` →
`RequiredRunManagementActor`; `RequireRunScopedOrRunTools` →
`RequireRunManagementTarget`. Semantics are unchanged; names now
describe the product policy.
- **Pair route migration** — all six pair handlers (`get_pair_status`,
`start_pair`, `get_pair`, `end_pair`, `send_pair_message`,
`get_transcript`) switch from `RequiredUser` to
`RequireRunManagementTarget`, removing the `Principal::User(auth.0)`
construction and the now-redundant `parse_run_id_path` calls.
- **Test coverage** — unit tests for the renamed extractors, integration
tests proving run-tools workers can read pair status/transcript
cross-run, that auth is accepted before domain logic
(worker-control-unavailable), that cross-run base workers remain
forbidden, and that run-tools workers still cannot call user-only routes
(approve, timeline).

### Key design decisions

**Forced-child behavior is preserved.** `fabro_run_create` from a
workflow agent still calls `ensure_current_run_parent`; the plan
specifically excludes relaxing this.

**Principal provenance is unchanged.** Workers keep `Principal::Worker {
run_id: … }` when acting through `fabro_tools`; no user principal is
forged.

**Pair handler run-id extraction simplified.** Because
`RequireRunManagementTarget` already extracts and validates the run ID
from the path, the pair handlers no longer repeat that parse — the
second `Path` component for pair-specific routes is bound to `_id` and
discarded.

**Twin-OpenAI gains `instructions_text` logging.** The integration test
for project-skill discovery needed to inspect the system prompt sent to
OpenAI; the twin now captures and exposes `instructions_text` in request
logs. This is a supporting change, not part of the auth model.


### Fabro Details

<details>
<summary>Ran 8 stages in 44m 10s for $39.32</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 23m 9s | $33.23 | 0 |
| simplify_opus | 4m 6s | $1.06 | 0 |
| simplify_gpt | 4m 1s | $5.03 | 0 |
| verify | 7m 56s | – | 0 |
| **Total** | **44m 10s** | **$39.32** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 15:52:19 -04:00
fabro-sh-0530[bot]
98c26d5370
Fold context-window data into agent.message, remove snapshot event (#390)
## Summary

Removes the standalone `agent.context_window.snapshot` event and instead
attaches the context-window projection directly to `agent.message`. This
eliminates the async provider token-count API calls that the old
approach required, and simplifies the event log to a single event type
carrying all post-response agent data.

## What Changed and Why

**Before:** After each LLM turn, the agent emitted a separate
`agent.context_window.snapshot` event — first a local estimate, then
potentially a second one after an async `count_input_tokens` call
resolved (or after response usage arrived). This required fingerprint
deduplication state, a `close_token` to cancel in-flight counts, and
frontend handling for the extra event type.

**After:** The `AgentEvent::AssistantMessage` variant carries an
`Option<StageContextWindowProjection>`. The projection is computed
locally at request-build time and then refined using response token
usage when available (`ResponseUsageScaledBreakdown`), or kept as a
`LocalEstimate` when response usage is absent. No provider API calls are
made.

### Plan Summary

- **Task 1:** Added `context_window:
Option<StageContextWindowProjection>` to `AgentMessageProps` (Rust types
+ OpenAPI), removed `AgentContextWindowSnapshotProps` and
`EventBody::AgentContextWindowSnapshot`.
- **Task 2:** Removed the spawned `count_input_tokens` task,
`close_token`, fingerprint sets, and both snapshot-emit methods from
`Session`. Added `context_window_from_response_usage` to
`context_window.rs`; `BuiltRequest` now holds the local projection
instead of the tool list.
- **Task 3:** Workflow conversion copies `context_window` from
`AgentEvent::AssistantMessage` into `AgentMessageProps`; store reducer
reads it from `AgentMessage` instead of the removed snapshot variant and
stamps `event_seq`.
- **Task 4:** GET endpoint tests updated to seed data via
`agent.message` with embedded context-window; endpoint behavior
unchanged.
- **Task 5:** Frontend constant and tests for
`agent.context_window.snapshot` removed; `agent.message` already
invalidates `stageContextWindow` through existing stage-activity
handling. TypeScript client regenerated with the new `AgentMessageProps`
model.

### Key Design Decisions

- **No provider token-count API calls** during normal execution —
context-window accuracy relies on local estimates scaled by response
usage, which is always available for successful turns.
- **Failed-before-response turns** emit no context-window data
(`context_window: None`), matching the old behavior where a snapshot
would have been emitted but response-usage scaling would never arrive.
- `BuiltRequest` drops the `tools` field (only needed for the
now-removed snapshot emission path); the local projection is computed at
build time and stored directly.


### Fabro Details

<details>
<summary>Ran 8 stages in 60m 3s for $55.78</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 1s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 30m 39s | $44.82 | 0 |
| simplify_opus | 10m 48s | $4.03 | 0 |
| simplify_gpt | 5m 1s | $6.93 | 0 |
| verify | 8m 47s | – | 0 |
| **Total** | **60m 3s** | **$55.78** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

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

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

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

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

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-24 15:48:21 -04:00