diff --git a/.ai/prompts/code-review-deep-1.md b/.ai/prompts/code-review-deep-1.md
index 1d837cfe7..c15a499b6 100644
--- a/.ai/prompts/code-review-deep-1.md
+++ b/.ai/prompts/code-review-deep-1.md
@@ -17,7 +17,7 @@ Output a report with all the bugs using this format:
title of bugbrief description of bug
- lib/crates/fabro-cli/src/commands/resume.rs
+ lib/apps/fabro-cli/src/commands/resume.rs115115
diff --git a/.ai/prompts/code-review-deep-2.md b/.ai/prompts/code-review-deep-2.md
index 6bc81586b..aa7000436 100644
--- a/.ai/prompts/code-review-deep-2.md
+++ b/.ai/prompts/code-review-deep-2.md
@@ -19,7 +19,7 @@ Output a report with all the bugs using this format:
important OR nityes OR no
- lib/crates/fabro-cli/src/commands/resume.rs
+ lib/apps/fabro-cli/src/commands/resume.rs115115
@@ -51,7 +51,7 @@ Here is a real-world example:
importantno
- lib/crates/fabro-cli/src/commands/resume.rs
+ lib/apps/fabro-cli/src/commands/resume.rs208208
diff --git a/.claude/skills/docs/references/mapping.md b/.claude/skills/docs/references/mapping.md
index 0a7c9e329..0d6a0d3cf 100644
--- a/.claude/skills/docs/references/mapping.md
+++ b/.claude/skills/docs/references/mapping.md
@@ -4,32 +4,32 @@ Which source files affect which doc pages. Use this as guidance — also apply j
| Source | Docs |
|--------|------|
-| `lib/crates/fabro-cli/src/main.rs`, `lib/crates/fabro-workflow/src/cli/mod.rs`, `lib/crates/fabro-workflow/src/cli/run.rs` | `docs/public/reference/cli.mdx` |
-| `lib/crates/fabro-cli/src/cli_config.rs` | `docs/public/reference/cli-configuration.mdx` |
-| `lib/crates/fabro-llm/src/cli.rs` | `docs/public/reference/cli.mdx` |
-| `lib/crates/fabro-api/src/serve.rs` | `docs/public/reference/cli.mdx` |
-| `lib/crates/fabro-workflow/src/parser/*.rs` | `docs/public/reference/dot-language.mdx` |
-| `lib/crates/fabro-workflow/src/condition.rs` | `docs/public/reference/dot-language.mdx` |
-| `lib/crates/fabro-workflow/src/cli/validate.rs` | `docs/public/reference/dot-language.mdx` |
-| `lib/crates/fabro-workflow/src/stylesheet.rs` | `docs/public/workflows/stylesheets.mdx` |
-| `lib/crates/fabro-workflow/src/transform.rs` | `docs/public/workflows/variables.mdx` |
-| `lib/crates/fabro-workflow/src/handler/*.rs` | `docs/public/workflows/stages-and-nodes.mdx`, `docs/public/reference/dot-language.mdx` |
-| `lib/crates/fabro-workflow/src/handler/human.rs` | `docs/public/workflows/human-in-the-loop.mdx` |
-| `lib/crates/fabro-workflow/src/cli/run_config.rs` | `docs/public/execution/run-configuration.mdx` |
-| `lib/crates/fabro-workflow/src/engine.rs` | `docs/public/core-concepts/how-arc-works.mdx` |
-| `lib/crates/fabro-workflow/src/context/*.rs` | `docs/public/execution/context.mdx` |
-| `lib/crates/fabro-workflow/src/checkpoint.rs` | `docs/public/execution/checkpoints.mdx` |
-| `lib/crates/fabro-workflow/src/retro.rs`, `lib/crates/fabro-workflow/src/retro_agent.rs` | `docs/public/execution/retros.mdx` |
-| `lib/crates/fabro-workflow/src/interviewer/*.rs` | `docs/public/execution/interviews.mdx` |
-| `lib/crates/fabro-workflow/src/hook/*.rs` | `docs/public/agents/hooks.mdx` |
-| `lib/crates/fabro-workflow/src/daytona_sandbox.rs` | `docs/public/integrations/daytona.mdx`, `docs/public/execution/environments.mdx` |
-| `lib/crates/fabro-agent/src/tools.rs`, `lib/crates/fabro-agent/src/tool_registry.rs`, `lib/crates/fabro-agent/src/tool_execution.rs` | `docs/public/agents/tools.mdx` |
-| `lib/crates/fabro-agent/src/v4a_patch.rs` | `docs/public/agents/tools.mdx` |
-| `lib/crates/fabro-agent/src/cli.rs` | `docs/public/agents/permissions.mdx` |
-| `lib/crates/fabro-agent/src/subagent.rs` | `docs/public/agents/subagents.mdx` |
-| `lib/crates/fabro-agent/src/mcp_integration.rs` | `docs/public/agents/mcp.mdx` |
-| `lib/crates/fabro-llm/src/catalog.rs`, `lib/crates/fabro-llm/src/providers/*.rs` | `docs/public/core-concepts/models.mdx` |
-| `lib/crates/fabro-slack/src/*.rs` | `docs/public/integrations/slack.mdx` |
-| `lib/crates/fabro-mcp/src/*.rs` | `docs/public/agents/mcp.mdx` |
-| `lib/crates/fabro-api/src/*.rs` | `docs/public/api-reference/overview.mdx`, `docs/public/api-reference/demo-mode.mdx` |
-| `lib/crates/fabro-api/src/server_config.rs` | `docs/public/administration/server-configuration.mdx` |
+| `lib/apps/fabro-cli/src/main.rs`, `lib/components/fabro-workflow/src/cli/mod.rs`, `lib/components/fabro-workflow/src/cli/run.rs` | `docs/public/reference/cli.mdx` |
+| `lib/apps/fabro-cli/src/cli_config.rs` | `docs/public/reference/cli-configuration.mdx` |
+| `lib/components/fabro-llm/src/cli.rs` | `docs/public/reference/cli.mdx` |
+| `lib/foundation/fabro-api/src/serve.rs` | `docs/public/reference/cli.mdx` |
+| `lib/components/fabro-workflow/src/parser/*.rs` | `docs/public/reference/dot-language.mdx` |
+| `lib/components/fabro-workflow/src/condition.rs` | `docs/public/reference/dot-language.mdx` |
+| `lib/components/fabro-workflow/src/cli/validate.rs` | `docs/public/reference/dot-language.mdx` |
+| `lib/components/fabro-workflow/src/stylesheet.rs` | `docs/public/workflows/stylesheets.mdx` |
+| `lib/components/fabro-workflow/src/transform.rs` | `docs/public/workflows/variables.mdx` |
+| `lib/components/fabro-workflow/src/handler/*.rs` | `docs/public/workflows/stages-and-nodes.mdx`, `docs/public/reference/dot-language.mdx` |
+| `lib/components/fabro-workflow/src/handler/human.rs` | `docs/public/workflows/human-in-the-loop.mdx` |
+| `lib/components/fabro-workflow/src/cli/run_config.rs` | `docs/public/execution/run-configuration.mdx` |
+| `lib/components/fabro-workflow/src/engine.rs` | `docs/public/core-concepts/how-arc-works.mdx` |
+| `lib/components/fabro-workflow/src/context/*.rs` | `docs/public/execution/context.mdx` |
+| `lib/components/fabro-workflow/src/checkpoint.rs` | `docs/public/execution/checkpoints.mdx` |
+| `lib/components/fabro-workflow/src/retro.rs`, `lib/components/fabro-workflow/src/retro_agent.rs` | `docs/public/execution/retros.mdx` |
+| `lib/components/fabro-workflow/src/interviewer/*.rs` | `docs/public/execution/interviews.mdx` |
+| `lib/components/fabro-workflow/src/hook/*.rs` | `docs/public/agents/hooks.mdx` |
+| `lib/components/fabro-workflow/src/daytona_sandbox.rs` | `docs/public/integrations/daytona.mdx`, `docs/public/execution/environments.mdx` |
+| `lib/components/fabro-agent/src/tools.rs`, `lib/components/fabro-agent/src/tool_registry.rs`, `lib/components/fabro-agent/src/tool_execution.rs` | `docs/public/agents/tools.mdx` |
+| `lib/components/fabro-agent/src/v4a_patch.rs` | `docs/public/agents/tools.mdx` |
+| `lib/components/fabro-agent/src/cli.rs` | `docs/public/agents/permissions.mdx` |
+| `lib/components/fabro-agent/src/subagent.rs` | `docs/public/agents/subagents.mdx` |
+| `lib/components/fabro-agent/src/mcp_integration.rs` | `docs/public/agents/mcp.mdx` |
+| `lib/components/fabro-llm/src/catalog.rs`, `lib/components/fabro-llm/src/providers/*.rs` | `docs/public/core-concepts/models.mdx` |
+| `lib/components/fabro-slack/src/*.rs` | `docs/public/integrations/slack.mdx` |
+| `lib/components/fabro-mcp/src/*.rs` | `docs/public/agents/mcp.mdx` |
+| `lib/foundation/fabro-api/src/*.rs` | `docs/public/api-reference/overview.mdx`, `docs/public/api-reference/demo-mode.mdx` |
+| `lib/foundation/fabro-api/src/server_config.rs` | `docs/public/administration/server-configuration.mdx` |
diff --git a/.fabro/workflows/implement-plan/workflow.fabro b/.fabro/workflows/implement-plan/workflow.fabro
index 8c7ad4256..e2a59dcae 100644
--- a/.fabro/workflows/implement-plan/workflow.fabro
+++ b/.fabro/workflows/implement-plan/workflow.fabro
@@ -12,7 +12,7 @@ digraph ImplementPlan {
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="openai/gpt-5.6-sol", provider="openrouter", reasoning_effort="xhigh"]
simplify_fable [label="Simplify (Claude Fable 5)", prompt="@prompts/simplify.md", model="anthropic/claude-fable-5", provider="openrouter", reasoning_effort="xhigh"]
simplify_sol [label="Simplify (GPT-5.6 Sol)", prompt="@prompts/simplify.md", model="openai/gpt-5.6-sol", provider="openrouter", reasoning_effort="max"]
- 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"]
+ 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", timeout="20m", 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.", model="anthropic/claude-fable-5", provider="openrouter", reasoning_effort="xhigh", max_visits=3]
start -> toolchain
diff --git a/.gitattributes b/.gitattributes
index 068cf7f02..96454cdd1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,2 @@
-lib/crates/fabro-spa/assets/** linguist-generated=true -diff
+lib/apps/fabro-spa/assets/** linguist-generated=true -diff
lib/packages/fabro-api-client/src/** linguist-generated=true
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 1580d81df..12ba1b9cc 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -4,7 +4,9 @@ on:
push:
branches: [main]
paths:
- - "lib/crates/**"
+ - "lib/apps/**"
+ - "lib/components/**"
+ - "lib/foundation/**"
- "test/**"
- "Cargo.toml"
- "Cargo.lock"
@@ -18,7 +20,9 @@ on:
pull_request:
branches: [main]
paths:
- - "lib/crates/**"
+ - "lib/apps/**"
+ - "lib/components/**"
+ - "lib/foundation/**"
- "test/**"
- "Cargo.toml"
- "Cargo.lock"
@@ -76,7 +80,7 @@ jobs:
- name: Verify legacy auth identity removal
run: |
if git grep -nE '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; then
+ -- lib/apps lib/components lib/foundation apps lib/packages docs/public/api-reference/fabro-api.yaml; then
echo "::error::Legacy auth identities remain in the repository"
exit 1
else
diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml
index e2f63a047..064c38c3d 100644
--- a/.github/workflows/typescript.yml
+++ b/.github/workflows/typescript.yml
@@ -5,7 +5,7 @@ on:
branches: [main]
paths:
- "apps/**"
- - "lib/crates/fabro-spa/**"
+ - "lib/apps/fabro-spa/**"
- "lib/packages/**"
- "package.json"
- "bun.lock"
@@ -17,7 +17,7 @@ on:
branches: [main]
paths:
- "apps/**"
- - "lib/crates/fabro-spa/**"
+ - "lib/apps/fabro-spa/**"
- "lib/packages/**"
- "package.json"
- "bun.lock"
diff --git a/.gitignore b/.gitignore
index 5c49b62a9..d9b235fc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,8 @@ target
node_modules
apps/fabro-web/dist
apps/fabro-web/.dist-builds/
-lib/crates/fabro-spa/assets/*
-!lib/crates/fabro-spa/assets/.gitkeep
+lib/apps/fabro-spa/assets/*
+!lib/apps/fabro-spa/assets/.gitkeep
tmp
evals/swe-bench/repos/
evals/swe-bench/results/
diff --git a/AGENTS.md b/AGENTS.md
index d77efe095..bdc9ab53f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -56,7 +56,7 @@ The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source of
1. Edit `docs/public/api-reference/fabro-api.yaml`
2. `cargo build -p fabro-api` — build.rs regenerates Rust types and client via progenitor
-3. Write/update handler in `lib/crates/fabro-server/src/server.rs`, add route to `build_router()`
+3. Write/update handler in `lib/apps/fabro-server/src/server.rs`, add route to `build_router()`
4. `cargo nextest run -p fabro-server` — conformance test catches spec/router drift
5. `cd lib/packages/fabro-api-client && bun run generate` — regenerates TypeScript Axios client
@@ -64,7 +64,7 @@ The OpenAPI spec at `docs/public/api-reference/fabro-api.yaml` is the source of
- Treat OpenAPI as the source of truth for the wire contract, not as the automatic owner of Rust types.
- Before adding or keeping a generated schema type, search the workspace for an existing hand-written Rust type with the same product meaning.
-- If the schema and an existing Rust type have the same semantics and serde shape, reuse the existing type via `lib/crates/fabro-api/build.rs` `with_replacement(...)` instead of generating a parallel API type.
+- If the schema and an existing Rust type have the same semantics and serde shape, reuse the existing type via `lib/foundation/fabro-api/build.rs` `with_replacement(...)` instead of generating a parallel API type.
- If two types are close but not identical, prefer proposing changes that align them into one canonical type rather than accepting small drift. It is usually better to iterate the API now than to create permanently split Rust/API types.
- Keep a separate API DTO only when the API is intentionally a projection, summary, or presentation-specific view of internal state. In that case, give it a distinct API-facing name instead of reusing the internal concept name.
- Treat `ApiFoo` aliases and `foo_to_api` / `foo_from_api` adapters as a smell unless they represent a real semantic boundary. They should not exist only to bridge accidental duplicate types.
@@ -107,7 +107,7 @@ Before merging changes that add or move shared test helpers, verify:
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
-### Rust crates (`lib/crates/`)
+### Rust crates (`lib/apps/`, `lib/components/`, and `lib/foundation/`)
- **fabro-cli** — CLI entry point. Commands: `run`, `exec`, `serve`, `validate`, `parse`, `cp`, `model`, `doctor`, `install`, `ps`, `system prune`
- **fabro-workflow** — Core workflow engine. Parses Graphviz graphs, runs stages, manages checkpoints/resume, hooks, and human-in-the-loop interactions
- **fabro-agent** — AI coding agent with tool use (Bash, Read, Write, Edit, Glob, Grep, WebFetch). `Sandbox` trait abstracts execution environments
diff --git a/Cargo.lock b/Cargo.lock
index 098d71cbf..5e3fb490a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2239,7 +2239,7 @@ dependencies = [
[[package]]
name = "fabro-acp"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"agent-client-protocol",
"agent-client-protocol-tokio",
@@ -2258,7 +2258,7 @@ dependencies = [
[[package]]
name = "fabro-agent"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-trait",
@@ -2300,7 +2300,7 @@ dependencies = [
[[package]]
name = "fabro-api"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"chrono",
"fabro-automation",
@@ -2323,7 +2323,7 @@ dependencies = [
[[package]]
name = "fabro-auth"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-trait",
@@ -2348,7 +2348,7 @@ dependencies = [
[[package]]
name = "fabro-automation"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"chrono",
@@ -2367,11 +2367,11 @@ dependencies = [
[[package]]
name = "fabro-build-support"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
[[package]]
name = "fabro-checkpoint"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"chrono",
"fabro-config",
@@ -2387,7 +2387,7 @@ dependencies = [
[[package]]
name = "fabro-cli"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2489,7 +2489,7 @@ dependencies = [
[[package]]
name = "fabro-client"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"bytes",
@@ -2518,7 +2518,7 @@ dependencies = [
[[package]]
name = "fabro-config"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"chrono",
@@ -2547,7 +2547,7 @@ dependencies = [
[[package]]
name = "fabro-core"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"async-trait",
"fabro-types",
@@ -2562,7 +2562,7 @@ dependencies = [
[[package]]
name = "fabro-db"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"chrono",
@@ -2574,7 +2574,7 @@ dependencies = [
[[package]]
name = "fabro-dev"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2593,7 +2593,7 @@ dependencies = [
[[package]]
name = "fabro-dump"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"bytes",
@@ -2607,7 +2607,7 @@ dependencies = [
[[package]]
name = "fabro-environment"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"chrono",
@@ -2629,7 +2629,7 @@ dependencies = [
[[package]]
name = "fabro-github"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"base64",
@@ -2651,7 +2651,7 @@ dependencies = [
[[package]]
name = "fabro-graphviz"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"fabro-types",
@@ -2659,13 +2659,14 @@ dependencies = [
"nom",
"regex",
"serde",
+ "serde_json",
"strum 0.28.0",
"thiserror 2.0.18",
]
[[package]]
name = "fabro-hooks"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"async-trait",
"fabro-agent",
@@ -2688,7 +2689,7 @@ dependencies = [
[[package]]
name = "fabro-http"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"fabro-static",
"http 1.4.0",
@@ -2698,7 +2699,7 @@ dependencies = [
[[package]]
name = "fabro-install"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"base64",
@@ -2717,7 +2718,7 @@ dependencies = [
[[package]]
name = "fabro-interview"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"async-trait",
"dialoguer",
@@ -2732,7 +2733,7 @@ dependencies = [
[[package]]
name = "fabro-llm"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-trait",
@@ -2773,7 +2774,7 @@ dependencies = [
[[package]]
name = "fabro-macros"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"clap",
"fabro-options-metadata",
@@ -2784,7 +2785,7 @@ dependencies = [
[[package]]
name = "fabro-manifest"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"fabro-api",
@@ -2802,7 +2803,7 @@ dependencies = [
[[package]]
name = "fabro-mcp"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"axum",
@@ -2822,7 +2823,7 @@ dependencies = [
[[package]]
name = "fabro-mcp-server"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"chrono",
@@ -2849,7 +2850,7 @@ dependencies = [
[[package]]
name = "fabro-mcp-store"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"chrono",
"fabro-db",
@@ -2867,7 +2868,7 @@ dependencies = [
[[package]]
name = "fabro-model"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"fabro-static",
"http 1.4.0",
@@ -2883,7 +2884,7 @@ dependencies = [
[[package]]
name = "fabro-oauth"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"axum",
@@ -2905,7 +2906,7 @@ dependencies = [
[[package]]
name = "fabro-options-metadata"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"serde",
"serde_json",
@@ -2913,7 +2914,7 @@ dependencies = [
[[package]]
name = "fabro-proc"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"cc",
"libc",
@@ -2922,7 +2923,7 @@ dependencies = [
[[package]]
name = "fabro-redact"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"aho-corasick",
"ref-cast",
@@ -2938,7 +2939,7 @@ dependencies = [
[[package]]
name = "fabro-sandbox"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-trait",
@@ -2983,7 +2984,7 @@ dependencies = [
[[package]]
name = "fabro-server"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-trait",
@@ -3075,7 +3076,7 @@ dependencies = [
[[package]]
name = "fabro-slack"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"fabro-http",
"fabro-interview",
@@ -3097,18 +3098,18 @@ dependencies = [
[[package]]
name = "fabro-spa"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"rust-embed",
]
[[package]]
name = "fabro-static"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
[[package]]
name = "fabro-store"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"async-trait",
"bytes",
@@ -3138,7 +3139,7 @@ dependencies = [
[[package]]
name = "fabro-telemetry"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"base64",
@@ -3164,7 +3165,7 @@ dependencies = [
[[package]]
name = "fabro-template"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"fabro-types",
@@ -3178,7 +3179,7 @@ dependencies = [
[[package]]
name = "fabro-test"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"assert_cmd",
@@ -3203,7 +3204,7 @@ dependencies = [
[[package]]
name = "fabro-tool"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-trait",
@@ -3224,7 +3225,7 @@ dependencies = [
[[package]]
name = "fabro-tracker"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-trait",
@@ -3238,7 +3239,7 @@ dependencies = [
[[package]]
name = "fabro-types"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"chrono",
"clap",
@@ -3260,7 +3261,7 @@ dependencies = [
[[package]]
name = "fabro-util"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"console 0.15.11",
@@ -3281,7 +3282,7 @@ dependencies = [
[[package]]
name = "fabro-validate"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"fabro-acp",
"fabro-graphviz",
@@ -3294,7 +3295,7 @@ dependencies = [
[[package]]
name = "fabro-variable"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"chrono",
@@ -3311,7 +3312,7 @@ dependencies = [
[[package]]
name = "fabro-vault"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"chrono",
@@ -3330,7 +3331,7 @@ dependencies = [
[[package]]
name = "fabro-workflow"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"assert_cmd",
@@ -3368,6 +3369,7 @@ dependencies = [
"fabro-util",
"fabro-validate",
"fabro-vault",
+ "fabro-workflow",
"futures",
"git2",
"hex",
@@ -8493,7 +8495,7 @@ dependencies = [
[[package]]
name = "twin-github"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"axum",
"base64",
@@ -8512,7 +8514,7 @@ dependencies = [
[[package]]
name = "twin-openai"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
dependencies = [
"anyhow",
"async-stream",
diff --git a/Cargo.toml b/Cargo.toml
index 5bd216ed8..e9f63c9d7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,11 +1,17 @@
[workspace]
-members = ["lib/crates/*", "test/twin/openai", "test/twin/github"]
-default-members = ["lib/crates/fabro-cli"]
+members = [
+ "lib/apps/*",
+ "lib/components/*",
+ "lib/foundation/*",
+ "test/twin/openai",
+ "test/twin/github",
+]
+default-members = ["lib/apps/fabro-cli"]
resolver = "2"
[workspace.package]
edition = "2021"
-version = "0.303.0-nightly.2"
+version = "0.303.0-nightly.4"
license = "MIT"
[workspace.dependencies]
@@ -84,7 +90,7 @@ hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
insta = "1"
-fabro-test = { path = "lib/crates/fabro-test" }
+fabro-test = { path = "lib/foundation/fabro-test" }
twin-openai = { path = "test/twin/openai" }
twin-github = { path = "test/twin/github" }
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] }
@@ -100,11 +106,11 @@ rust-embed = "8"
percent-encoding = "2"
minijinja = "=2.19.0"
miette = { version = "7.6", features = ["fancy"] }
-fabro-http = { path = "lib/crates/fabro-http" }
-fabro-environment = { path = "lib/crates/fabro-environment" }
-fabro-options-metadata = { path = "lib/crates/fabro-options-metadata" }
-fabro-redact = { path = "lib/crates/fabro-redact" }
-fabro-static = { path = "lib/crates/fabro-static" }
+fabro-http = { path = "lib/foundation/fabro-http" }
+fabro-environment = { path = "lib/components/fabro-environment" }
+fabro-options-metadata = { path = "lib/foundation/fabro-options-metadata" }
+fabro-redact = { path = "lib/foundation/fabro-redact" }
+fabro-static = { path = "lib/foundation/fabro-static" }
graphviz-sys = { git = "https://github.com/fabro-sh/graphviz-sys" }
ref-cast = "1"
strum = { version = "0.28", features = ["derive"] }
diff --git a/apps/fabro-web/app/lib/model-offerings.test.ts b/apps/fabro-web/app/lib/model-offerings.test.ts
new file mode 100644
index 000000000..a54a1b0e8
--- /dev/null
+++ b/apps/fabro-web/app/lib/model-offerings.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, test } from "bun:test";
+import {
+ modelOfferingKey,
+ modelOfferingTestArgs,
+} from "./model-offerings";
+
+describe("model offering identity", () => {
+ const openai = { id: "portable-model", provider: "openai" };
+ const openrouter = { id: "portable-model", provider: "openrouter" };
+
+ test("keeps duplicate model IDs in independent row state", () => {
+ expect(modelOfferingKey(openai)).not.toBe(modelOfferingKey(openrouter));
+
+ const state = new Map([
+ [modelOfferingKey(openai), "ok"],
+ [modelOfferingKey(openrouter), "error"],
+ ]);
+ expect(state.get(modelOfferingKey(openai))).toBe("ok");
+ expect(state.get(modelOfferingKey(openrouter))).toBe("error");
+ });
+
+ test("includes the row provider in model-test request arguments", () => {
+ expect(modelOfferingTestArgs(openai)).toEqual([
+ "portable-model",
+ "openai",
+ ]);
+ expect(modelOfferingTestArgs(openrouter)).toEqual([
+ "portable-model",
+ "openrouter",
+ ]);
+ });
+});
diff --git a/apps/fabro-web/app/lib/model-offerings.ts b/apps/fabro-web/app/lib/model-offerings.ts
new file mode 100644
index 000000000..e2bc94d2d
--- /dev/null
+++ b/apps/fabro-web/app/lib/model-offerings.ts
@@ -0,0 +1,13 @@
+import type { Model } from "@qltysh/fabro-api-client";
+
+type ModelOfferingIdentity = Pick;
+
+export function modelOfferingKey(model: ModelOfferingIdentity): string {
+ return `${model.provider}\u0000${model.id}`;
+}
+
+export function modelOfferingTestArgs(
+ model: ModelOfferingIdentity,
+): [id: string, provider: string] {
+ return [model.id, model.provider];
+}
diff --git a/apps/fabro-web/app/routes/run-overview.tsx b/apps/fabro-web/app/routes/run-overview.tsx
index 25262b748..0150d19ea 100644
--- a/apps/fabro-web/app/routes/run-overview.tsx
+++ b/apps/fabro-web/app/routes/run-overview.tsx
@@ -23,7 +23,7 @@ export const handle = { wide: true, fullHeight: true };
type Direction = "LR" | "TB";
-// Mirrors fabro-graphviz's RANKDIR_RE (lib/crates/fabro-graphviz/src/render.rs) —
+// Mirrors fabro-graphviz's RANKDIR_RE (lib/components/fabro-graphviz/src/render.rs) —
// keep the accepted `rankdir=` syntax in sync with that regex.
const RANKDIR_RE = /rankdir\s*=\s*(\w+)/;
diff --git a/apps/fabro-web/app/routes/settings-models.tsx b/apps/fabro-web/app/routes/settings-models.tsx
index 57b7429ed..f6e3905df 100644
--- a/apps/fabro-web/app/routes/settings-models.tsx
+++ b/apps/fabro-web/app/routes/settings-models.tsx
@@ -28,6 +28,10 @@ import {
} from "../components/runs-list/sort-header";
import { Tooltip } from "../components/ui";
import { formatContextWindow, formatTokensPerSecond } from "../lib/format";
+import {
+ modelOfferingKey,
+ modelOfferingTestArgs,
+} from "../lib/model-offerings";
import { useDebouncedValue } from "../hooks/effects";
export function meta() {
@@ -277,30 +281,46 @@ function ModelsSection({ providers }: { providers: Provider[] }) {
const runSweep = useCallback(async () => {
if (running) return;
- const ids = rows.map((r) => r.id);
- if (ids.length === 0) return;
+ const offerings = rows.map((model) => ({
+ id: model.id,
+ provider: model.provider,
+ key: modelOfferingKey(model),
+ }));
+ if (offerings.length === 0) return;
const seed = new Map();
- for (const id of ids) seed.set(id, { phase: "queued" });
+ for (const offering of offerings) {
+ seed.set(offering.key, { phase: "queued" });
+ }
setResults(seed);
- setSweep({ done: 0, total: ids.length, ok: 0, failed: 0 });
+ setSweep({ done: 0, total: offerings.length, ok: 0, failed: 0 });
let cursor = 0;
const worker = async () => {
- while (cursor < ids.length) {
+ while (cursor < offerings.length) {
const i = cursor;
cursor += 1;
- const id = ids[i];
+ const offering = offerings[i];
setResults((prev) => {
const next = new Map(prev);
- next.set(id, { phase: "running" });
+ next.set(offering.key, { phase: "running" });
return next;
});
let outcome: RowState;
try {
- const result = await apiData(() => modelsApi.testModel(id));
- if (result.status === "ok") {
+ const result = await apiData(() =>
+ modelsApi.testModel(...modelOfferingTestArgs(offering)),
+ );
+ if (
+ result.provider !== offering.provider ||
+ result.model_id !== offering.id
+ ) {
+ outcome = {
+ phase: "error",
+ message: `Server tested unexpected offering ${result.provider}/${result.model_id}`,
+ };
+ } else if (result.status === "ok") {
outcome = { phase: "ok" };
} else if (result.status === "error") {
outcome = {
@@ -319,7 +339,7 @@ function ModelsSection({ providers }: { providers: Provider[] }) {
setResults((prev) => {
const next = new Map(prev);
- next.set(id, outcome);
+ next.set(offering.key, outcome);
return next;
});
setSweep((prev) =>
@@ -336,7 +356,7 @@ function ModelsSection({ providers }: { providers: Provider[] }) {
};
await Promise.all(
- Array.from({ length: Math.min(TEST_CONCURRENCY, ids.length) }, () =>
+ Array.from({ length: Math.min(TEST_CONCURRENCY, offerings.length) }, () =>
worker(),
),
);
@@ -435,12 +455,12 @@ function ModelsSection({ providers }: { providers: Provider[] }) {
{rows.map((model) => (
))}
@@ -468,7 +488,7 @@ function ModelTableRow({
state: RowState | undefined;
}) {
return (
-
+
{providerLabel}
diff --git a/docs/internal/cli-workflow-coupling-audit.md b/docs/internal/cli-workflow-coupling-audit.md
index 7fdf953ba..198e658f3 100644
--- a/docs/internal/cli-workflow-coupling-audit.md
+++ b/docs/internal/cli-workflow-coupling-audit.md
@@ -2,7 +2,7 @@
## Scope
-- Production imports under `lib/crates/fabro-cli/src/**` that still reference `fabro_workflow::*` after the server-owned selector/export refactor.
+- Production imports under `lib/apps/fabro-cli/src/**` that still reference `fabro_workflow::*` after the server-owned selector/export refactor.
- Test-only imports are listed separately so the remaining architectural debt is explicit.
## Completed In This Change
@@ -16,30 +16,30 @@
| Path | Direct dependency | Why it still exists | Required remediation track |
| --- | --- | --- | --- |
-| `lib/crates/fabro-cli/src/commands/pr/create.rs` | `StageOutcome`, `pull_request::maybe_open_pull_request` | CLI still reconstructs store state and runs PR creation logic from the workflow pipeline directly. | Replace with a server API, or extract PR orchestration into a non-engine shared service crate plus API. |
-| `lib/crates/fabro-cli/src/commands/run/runner.rs` | `artifact_snapshot::CapturedArtifactInfo`, `artifact_upload::{ArtifactSink, StageArtifactUploader}`, `event::{Emitter, RunEventSink}`, `operations::{self, StartServices}`, `run_control::RunControlState`, `runtime_store::{RunStoreBackend, RunStoreHandle}` | Hidden worker subprocess path still lives inside the CLI crate and embeds the workflow engine directly. | Re-home worker/runtime code outside the user CLI surface, ideally into a dedicated worker crate or binary. |
-| `lib/crates/fabro-cli/src/manifest_builder.rs` | `git::{GitSyncStatus, head_sha, sync_status}` | Manifest submission still relies on git helper logic that happens to live in `fabro_workflow`. | Extract git-sync inspection helpers into a non-workflow shared crate/module. |
-| `lib/crates/fabro-cli/src/server_client.rs` | `artifact_snapshot::CapturedArtifactInfo` | The upload client reuses a workflow-owned artifact snapshot DTO. | Extract shared artifact snapshot DTOs into `fabro-store`, `fabro-types`, or a dedicated shared crate. |
-| `lib/crates/fabro-cli/src/commands/runs/inspect.rs` | `run_status::RunStatus` | CLI output types still depend on engine-owned run status enums. | Extract shared status types into `fabro-types` or switch to API-generated/public store types. |
-| `lib/crates/fabro-cli/src/commands/runs/list.rs` | `run_status::RunStatus` | List rendering still depends on engine-owned run status enums. | Extract shared status types into `fabro-types` or switch to API-generated/public store types. |
-| `lib/crates/fabro-cli/src/commands/run/attach.rs` | `StageOutcome`, `run_status::RunStatus` | Attach/replay logic still formats engine-owned terminal status types directly. | Extract shared run/conclusion status types into `fabro-types`. |
-| `lib/crates/fabro-cli/src/commands/run/output.rs` | `StageOutcome`, `records::Conclusion` | Human-readable completion output still consumes workflow-owned conclusion/status records. | Extract shared conclusion/status DTOs into `fabro-types` or `fabro-store`. |
-| `lib/crates/fabro-cli/src/commands/run/wait.rs` | `records::Conclusion`, `run_status::RunStatus` | Wait output still depends on workflow-owned status/conclusion records. | Extract shared conclusion/status DTOs into `fabro-types` or `fabro-store`. |
-| `lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs` | `StageOutcome`, `format_cost` | Progress UI still depends on shared stage outcome and workflow-owned cost-formatting helper code. | Move formatting helpers into `fabro-util`. |
-| `lib/crates/fabro-cli/src/commands/run/run_progress/info_display.rs` | `event::RunNoticeLevel` | Progress UI still formats workflow-owned notice levels directly. | Extract shared notice/event enums into `fabro-types`. |
-| `lib/crates/fabro-cli/src/commands/run/run_progress/event.rs` | `event::RunNoticeLevel` | Progress event translation still depends on workflow-owned notice levels. | Extract shared notice/event enums into `fabro-types`. |
+| `lib/apps/fabro-cli/src/commands/pr/create.rs` | `StageOutcome`, `pull_request::maybe_open_pull_request` | CLI still reconstructs store state and runs PR creation logic from the workflow pipeline directly. | Replace with a server API, or extract PR orchestration into a non-engine shared service crate plus API. |
+| `lib/apps/fabro-cli/src/commands/run/runner.rs` | `artifact_snapshot::CapturedArtifactInfo`, `artifact_upload::{ArtifactSink, StageArtifactUploader}`, `event::{Emitter, RunEventSink}`, `operations::{self, StartServices}`, `run_control::RunControlState`, `runtime_store::{RunStoreBackend, RunStoreHandle}` | Hidden worker subprocess path still lives inside the CLI crate and embeds the workflow engine directly. | Re-home worker/runtime code outside the user CLI surface, ideally into a dedicated worker crate or binary. |
+| `lib/apps/fabro-cli/src/manifest_builder.rs` | `git::{GitSyncStatus, head_sha, sync_status}` | Manifest submission still relies on git helper logic that happens to live in `fabro_workflow`. | Extract git-sync inspection helpers into a non-workflow shared crate/module. |
+| `lib/apps/fabro-cli/src/server_client.rs` | `artifact_snapshot::CapturedArtifactInfo` | The upload client reuses a workflow-owned artifact snapshot DTO. | Extract shared artifact snapshot DTOs into `fabro-store`, `fabro-types`, or a dedicated shared crate. |
+| `lib/apps/fabro-cli/src/commands/runs/inspect.rs` | `run_status::RunStatus` | CLI output types still depend on engine-owned run status enums. | Extract shared status types into `fabro-types` or switch to API-generated/public store types. |
+| `lib/apps/fabro-cli/src/commands/runs/list.rs` | `run_status::RunStatus` | List rendering still depends on engine-owned run status enums. | Extract shared status types into `fabro-types` or switch to API-generated/public store types. |
+| `lib/apps/fabro-cli/src/commands/run/attach.rs` | `StageOutcome`, `run_status::RunStatus` | Attach/replay logic still formats engine-owned terminal status types directly. | Extract shared run/conclusion status types into `fabro-types`. |
+| `lib/apps/fabro-cli/src/commands/run/output.rs` | `StageOutcome`, `records::Conclusion` | Human-readable completion output still consumes workflow-owned conclusion/status records. | Extract shared conclusion/status DTOs into `fabro-types` or `fabro-store`. |
+| `lib/apps/fabro-cli/src/commands/run/wait.rs` | `records::Conclusion`, `run_status::RunStatus` | Wait output still depends on workflow-owned status/conclusion records. | Extract shared conclusion/status DTOs into `fabro-types` or `fabro-store`. |
+| `lib/apps/fabro-cli/src/commands/run/run_progress/stage_display.rs` | `StageOutcome`, `format_cost` | Progress UI still depends on shared stage outcome and workflow-owned cost-formatting helper code. | Move formatting helpers into `fabro-util`. |
+| `lib/apps/fabro-cli/src/commands/run/run_progress/info_display.rs` | `event::RunNoticeLevel` | Progress UI still formats workflow-owned notice levels directly. | Extract shared notice/event enums into `fabro-types`. |
+| `lib/apps/fabro-cli/src/commands/run/run_progress/event.rs` | `event::RunNoticeLevel` | Progress event translation still depends on workflow-owned notice levels. | Extract shared notice/event enums into `fabro-types`. |
## Test-Only Couplings
| Path | Direct dependency | Why it still exists | Suggested handling |
| --- | --- | --- | --- |
-| `lib/crates/fabro-cli/src/commands/dump.rs` test module | `event::{Event, append_event}` | Unit tests synthesize workflow events directly. | Low priority; keep until a lighter-weight event fixture helper exists. |
-| `lib/crates/fabro-cli/src/commands/run/wait.rs` test module | `StageOutcome`, `records::Conclusion`, `run_status::RunStatusRecord` | Output tests construct workflow-owned records directly. | Replace with shared fixture builders once conclusion DTOs move out. |
-| `lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs` test module | `event::{Event, RunNoticeLevel, to_run_event, to_run_event_at}`, `outcome::billed_model_usage_from_llm` | Progress tests build engine events directly. | Replace with shared event fixture helpers after event DTO extraction. |
-| `lib/crates/fabro-cli/src/commands/run/run_progress/event.rs` test module | `event::{Event, to_run_event}` | Event rendering tests depend on engine event constructors. | Replace with shared event fixture helpers after event DTO extraction. |
-| `lib/crates/fabro-cli/src/commands/run/runner.rs` test module | `artifact_upload::StageArtifactUploader` | Worker tests still reach into workflow upload internals. | Keep with worker re-home work; not worth separating first. |
-| `lib/crates/fabro-cli/tests/it/workflow/real_cli.rs` | `context::Context`, `event::Emitter`, `handler::agent::{CodergenBackend, CodergenResult}`, `handler::llm::cli::AgentCliBackend` | Integration test exercises the real workflow engine directly through CLI harnesses. | Accept as engine integration coverage or move under workflow-owned test support later. |
-| `lib/crates/fabro-cli/tests/it/scenario/recovery.rs` | `operations::{RunTimeline, build_timeline}` | Scenario test inspects rewind timeline internals directly. | Replace after server-owned rewind/timeline APIs exist. |
+| `lib/apps/fabro-cli/src/commands/dump.rs` test module | `event::{Event, append_event}` | Unit tests synthesize workflow events directly. | Low priority; keep until a lighter-weight event fixture helper exists. |
+| `lib/apps/fabro-cli/src/commands/run/wait.rs` test module | `StageOutcome`, `records::Conclusion`, `run_status::RunStatusRecord` | Output tests construct workflow-owned records directly. | Replace with shared fixture builders once conclusion DTOs move out. |
+| `lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs` test module | `event::{Event, RunNoticeLevel, to_run_event, to_run_event_at}`, `outcome::billed_model_usage_from_llm` | Progress tests build engine events directly. | Replace with shared event fixture helpers after event DTO extraction. |
+| `lib/apps/fabro-cli/src/commands/run/run_progress/event.rs` test module | `event::{Event, to_run_event}` | Event rendering tests depend on engine event constructors. | Replace with shared event fixture helpers after event DTO extraction. |
+| `lib/apps/fabro-cli/src/commands/run/runner.rs` test module | `artifact_upload::StageArtifactUploader` | Worker tests still reach into workflow upload internals. | Keep with worker re-home work; not worth separating first. |
+| `lib/apps/fabro-cli/tests/it/workflow/real_cli.rs` | `context::Context`, `event::Emitter`, `handler::agent::{CodergenBackend, CodergenResult}`, `handler::llm::cli::AgentCliBackend` | Integration test exercises the real workflow engine directly through CLI harnesses. | Accept as engine integration coverage or move under workflow-owned test support later. |
+| `lib/apps/fabro-cli/tests/it/scenario/recovery.rs` | `operations::{RunTimeline, build_timeline}` | Scenario test inspects rewind timeline internals directly. | Replace after server-owned rewind/timeline APIs exist. |
## Follow-Up Order
diff --git a/docs/internal/event-schema-competitive-analysis.md b/docs/internal/event-schema-competitive-analysis.md
index 9763fdf30..60a1c02ab 100644
--- a/docs/internal/event-schema-competitive-analysis.md
+++ b/docs/internal/event-schema-competitive-analysis.md
@@ -48,9 +48,9 @@ That envelope is stronger than most comparator systems. It gives Fabro stable to
Relevant current Fabro sources:
- `docs-internal/events-strategy.md`
-- `lib/crates/fabro-workflow/src/event.rs`
-- `lib/crates/fabro-types/src/run_event/mod.rs`
-- `lib/crates/fabro-agent/src/types.rs`
+- `lib/components/fabro-workflow/src/event.rs`
+- `lib/foundation/fabro-types/src/run_event/mod.rs`
+- `lib/components/fabro-agent/src/types.rs`
## Comparison Matrix
diff --git a/docs/internal/events.md b/docs/internal/events.md
index 53d10e1e4..eea2d6878 100644
--- a/docs/internal/events.md
+++ b/docs/internal/events.md
@@ -1215,26 +1215,6 @@ Emitted when the agent detects a tool-use loop.
No properties.
-### `agent.turn.limit`
-
-Emitted when the agent reaches its maximum turn count.
-
-```json
-{
- "id": "...", "ts": "...", "run_id": "...",
- "event": "agent.turn.limit",
- "node_id": "code", "node_label": "code",
- "session_id": "ses_abc",
- "properties": {
- "max_turns": 25
- }
-}
-```
-
-| Property | Type | Description |
-|----------|------|-------------|
-| `max_turns` | number | Maximum turns allowed |
-
### `agent.skill.expanded`
```json
diff --git a/docs/internal/fabro-event-schema-v2-concrete-shape.md b/docs/internal/fabro-event-schema-v2-concrete-shape.md
index 4d8541b90..78134f619 100644
--- a/docs/internal/fabro-event-schema-v2-concrete-shape.md
+++ b/docs/internal/fabro-event-schema-v2-concrete-shape.md
@@ -269,21 +269,21 @@ The durable model remains simple: replay ordered events, no duplicate truth laye
An engineer implementing this proposal should make only these structural changes unless a later section explicitly says otherwise.
-1. Update [`RunEvent`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/run_event/mod.rs) to add:
+1. Update [`RunEvent`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/foundation/fabro-types/src/run_event/mod.rs) to add:
- `stage_id`
- `parallel_group_id`
- `parallel_branch_id`
- `tool_call_id`
- `actor`
-2. Update `RunEvent::to_value()` and `RunEvent` parsing in [`run_event/mod.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/run_event/mod.rs) so the new envelope fields serialize and deserialize.
-3. Extend `StoredEventFields` and `stored_event_fields()` in [`event.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-workflow/src/event.rs) to populate:
+2. Update `RunEvent::to_value()` and `RunEvent` parsing in [`run_event/mod.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/foundation/fabro-types/src/run_event/mod.rs) so the new envelope fields serialize and deserialize.
+3. Extend `StoredEventFields` and `stored_event_fields()` in [`event.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/components/fabro-workflow/src/event.rs) to populate:
- `stage_id`
- `parallel_group_id`
- `parallel_branch_id`
- `tool_call_id` on tool-lifecycle events
- `actor` when there is a clear primary actor
These values should come from the emitter's current execution context for stage and parallel scope, and from event-specific payloads for `tool_call_id`.
-4. Leave [`EventEnvelope`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-store/src/types.rs) structurally unchanged:
+4. Leave [`EventEnvelope`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/components/fabro-store/src/types.rs) structurally unchanged:
- `seq: u32`
- `payload: EventPayload`
5. Update API/SSE envelope serialization so wire JSON is flattened:
@@ -304,11 +304,11 @@ An engineer implementing this proposal should make only these structural changes
V2 should keep the current hand-coded domain split for prop structs:
-- run props in [`run.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/run_event/run.rs)
-- stage and checkpoint props in [`stage.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/run_event/stage.rs)
-- agent props in [`agent.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/run_event/agent.rs)
-- infra/setup props in [`infra.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/run_event/infra.rs)
-- parallel/interview/git/misc props in [`misc.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/run_event/misc.rs)
+- run props in [`run.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/foundation/fabro-types/src/run_event/run.rs)
+- stage and checkpoint props in [`stage.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/foundation/fabro-types/src/run_event/stage.rs)
+- agent props in [`agent.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/foundation/fabro-types/src/run_event/agent.rs)
+- infra/setup props in [`infra.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/foundation/fabro-types/src/run_event/infra.rs)
+- parallel/interview/git/misc props in [`misc.rs`](/Users/bhelmkamp/p/fabro-sh/fabro/lib/foundation/fabro-types/src/run_event/misc.rs)
That split is part of the design quality. V2 should keep adding hand-coded prop structs, not collapse everything into generic maps.
@@ -374,7 +374,6 @@ V2 keeps the current durable family surface broadly intact.
- `agent.error`
- `agent.warning`
- `agent.loop.detected`
-- `agent.turn.limit`
- `agent.steering.injected`
- `agent.compaction.started`
- `agent.compaction.completed`
diff --git a/docs/internal/mcp-server-qa-test-plan.md b/docs/internal/mcp-server-qa-test-plan.md
index de2fcfc75..c5d7b8e74 100644
--- a/docs/internal/mcp-server-qa-test-plan.md
+++ b/docs/internal/mcp-server-qa-test-plan.md
@@ -1,6 +1,6 @@
# Fabro MCP Server — QA Test Plan
-One-time manual QA pass for the 5 tools exposed by `fabro-mcp-server`. Source of truth: `lib/crates/fabro-mcp-server/src/run_tools/`.
+One-time manual QA pass for the 5 tools exposed by `fabro-mcp-server`. Source of truth: `lib/apps/fabro-mcp-server/src/run_tools/`.
This plan is **not** a template for adding automated test coverage — it exists to drive a single hands-on sweep against a real running server. Tick boxes as scenarios pass; add notes inline for failures or surprising behavior. Open bugs/PRs for issues found; do not port these scenarios into the Rust test suite.
diff --git a/docs/internal/migrations-strategy.md b/docs/internal/migrations-strategy.md
index 1aa73370f..318463a58 100644
--- a/docs/internal/migrations-strategy.md
+++ b/docs/internal/migrations-strategy.md
@@ -9,7 +9,7 @@ Migrations are product-facing compatibility code. Treat them like startup and st
Each crate owns the migrations for the data it owns.
```text
-lib/crates//
+lib///
migrations/
YYYYMMDDSS_descriptive_name.rs
src/migrations.rs
diff --git a/docs/internal/plan-events-as-source-of-truth-follow-ups.md b/docs/internal/plan-events-as-source-of-truth-follow-ups.md
index 650eca25d..1ca2b9fc2 100644
--- a/docs/internal/plan-events-as-source-of-truth-follow-ups.md
+++ b/docs/internal/plan-events-as-source-of-truth-follow-ups.md
@@ -170,14 +170,14 @@ Required payload fields:
Emit from the same places that currently call `put_status`:
-- `lib/crates/fabro-workflow/src/operations/create.rs`
-- `lib/crates/fabro-workflow/src/operations/start.rs`
-- `lib/crates/fabro-workflow/src/operations/resume.rs`
-- `lib/crates/fabro-workflow/src/pipeline/finalize.rs`
-- `lib/crates/fabro-workflow/src/lifecycle/disk.rs`
+- `lib/components/fabro-workflow/src/operations/create.rs`
+- `lib/components/fabro-workflow/src/operations/start.rs`
+- `lib/components/fabro-workflow/src/operations/resume.rs`
+- `lib/components/fabro-workflow/src/pipeline/finalize.rs`
+- `lib/components/fabro-workflow/src/lifecycle/disk.rs`
- CLI administrative flows that directly mutate status:
- - `lib/crates/fabro-cli/src/commands/runs/rm.rs`
- - `lib/crates/fabro-cli/src/commands/run/rewind.rs`
+ - `lib/apps/fabro-cli/src/commands/runs/rm.rs`
+ - `lib/apps/fabro-cli/src/commands/run/rewind.rs`
### 2. Enrich `checkpoint.completed` to carry a full checkpoint snapshot
@@ -200,11 +200,11 @@ Add fields covering:
Emitter seam:
-- `lib/crates/fabro-workflow/src/lifecycle/event.rs`
+- `lib/components/fabro-workflow/src/lifecycle/event.rs`
Producer seam for the source checkpoint object:
-- `lib/crates/fabro-workflow/src/lifecycle/disk.rs`
+- `lib/components/fabro-workflow/src/lifecycle/disk.rs`
Design rule:
@@ -231,7 +231,7 @@ Add:
Producer seam:
-- `lib/crates/fabro-workflow/src/pipeline/pull_request.rs`
+- `lib/components/fabro-workflow/src/pipeline/pull_request.rs`
After this lands, `put_pull_request` should become removable during the later memoized-state cutover.
@@ -245,7 +245,7 @@ Do not add a separate storage-shaped event. The final patch is run-level termina
Enrich `run.completed`, using the patch already computed from:
-- `lib/crates/fabro-workflow/src/lifecycle/git.rs`
+- `lib/components/fabro-workflow/src/lifecycle/git.rs`
Required payload:
@@ -287,9 +287,9 @@ Required projected output:
Likely seams:
-- `lib/crates/fabro-workflow/src/handler/agent.rs`
-- `lib/crates/fabro-workflow/src/handler/llm/api.rs`
-- `lib/crates/fabro-workflow/src/pipeline/retro.rs` if retro uses the same forwarded agent session path
+- `lib/components/fabro-workflow/src/handler/agent.rs`
+- `lib/components/fabro-workflow/src/handler/llm/api.rs`
+- `lib/components/fabro-workflow/src/pipeline/retro.rs` if retro uses the same forwarded agent session path
- any CLI-backed LLM path if it still produces `provider_used.json`
Do not keep the current “read JSON sidecar, then `put_node_provider_used`” pattern once this event exists.
@@ -304,7 +304,7 @@ Use the existing terminal parallel event rather than adding a storage-shaped eve
Add to `parallel.completed`, emitted from:
-- `lib/crates/fabro-workflow/src/handler/parallel.rs`
+- `lib/components/fabro-workflow/src/handler/parallel.rs`
Required new payload:
@@ -346,19 +346,19 @@ Likely files to touch:
- `docs-internal/events.md`
- `docs-internal/run-directory-keys.md`
- `docs-internal/events-strategy.md`
-- `lib/crates/fabro-workflow/src/event.rs`
-- `lib/crates/fabro-workflow/src/lifecycle/event.rs`
-- `lib/crates/fabro-workflow/src/lifecycle/disk.rs`
-- `lib/crates/fabro-workflow/src/lifecycle/git.rs`
-- `lib/crates/fabro-workflow/src/operations/create.rs`
-- `lib/crates/fabro-workflow/src/operations/start.rs`
-- `lib/crates/fabro-workflow/src/operations/resume.rs`
-- `lib/crates/fabro-workflow/src/pipeline/finalize.rs`
-- `lib/crates/fabro-workflow/src/pipeline/pull_request.rs`
-- `lib/crates/fabro-workflow/src/handler/agent.rs`
-- `lib/crates/fabro-workflow/src/handler/parallel.rs`
-- `lib/crates/fabro-cli/src/commands/runs/rm.rs`
-- `lib/crates/fabro-cli/src/commands/run/rewind.rs`
+- `lib/components/fabro-workflow/src/event.rs`
+- `lib/components/fabro-workflow/src/lifecycle/event.rs`
+- `lib/components/fabro-workflow/src/lifecycle/disk.rs`
+- `lib/components/fabro-workflow/src/lifecycle/git.rs`
+- `lib/components/fabro-workflow/src/operations/create.rs`
+- `lib/components/fabro-workflow/src/operations/start.rs`
+- `lib/components/fabro-workflow/src/operations/resume.rs`
+- `lib/components/fabro-workflow/src/pipeline/finalize.rs`
+- `lib/components/fabro-workflow/src/pipeline/pull_request.rs`
+- `lib/components/fabro-workflow/src/handler/agent.rs`
+- `lib/components/fabro-workflow/src/handler/parallel.rs`
+- `lib/apps/fabro-cli/src/commands/runs/rm.rs`
+- `lib/apps/fabro-cli/src/commands/run/rewind.rs`
- tests in `fabro-workflow`, `fabro-cli`, and `fabro-store`
## Phases
diff --git a/docs/internal/run-directory-keys.md b/docs/internal/run-directory-keys.md
index 91ceeb2a8..1b78fd560 100644
--- a/docs/internal/run-directory-keys.md
+++ b/docs/internal/run-directory-keys.md
@@ -5,8 +5,8 @@ This document maps the files that still live under a run scratch directory. Dura
Scope:
- Scratch root: `~/.fabro/scratch/YYYYMMDD-{run_id}/`
- This covers local run files only
-- Persistent store keys live in `lib/crates/fabro-store/src/keys.rs`
-- Artifact object-store keys live in `lib/crates/fabro-store/src/artifact_store.rs`
+- Persistent store keys live in `lib/components/fabro-store/src/keys.rs`
+- Artifact object-store keys live in `lib/components/fabro-store/src/artifact_store.rs`
There is no `_init.json` anymore. Run existence in the database is determined by stored run events, and local scratch directories are managed separately under `scratch/`.
diff --git a/docs/internal/slow-test-opportunities-2026-04-07.md b/docs/internal/slow-test-opportunities-2026-04-07.md
index 2fe511ff4..790a80c20 100644
--- a/docs/internal/slow-test-opportunities-2026-04-07.md
+++ b/docs/internal/slow-test-opportunities-2026-04-07.md
@@ -22,7 +22,7 @@ Method:
### [x] 1. Change two slow `exec` mock responses from retriable `500` to non-retriable `400`
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/exec.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/exec.rs`
Measured evidence:
- `fabro-cli::it::cmd::exec::exec_cli_server_target_overrides_configured_server_target`: `6.858s` median
@@ -32,7 +32,7 @@ Measured evidence:
- mocked `400`: `0.053s` median
Implementation status:
-- Implemented in `lib/crates/fabro-cli/tests/it/cmd/exec.rs`
+- Implemented in `lib/apps/fabro-cli/tests/it/cmd/exec.rs`
- Verified with `ulimit -n 4096` via 5 targeted nextest runs per test
- Post-change nextest exec-time medians:
- `exec_server_target_uses_remote_transport_instead_of_local_api_key_resolution`: `1.580s`
@@ -57,7 +57,7 @@ Cons:
### 2. Short-circuit delete-path worker grace for already-terminal runs
Files:
-- `lib/crates/fabro-server/src/server.rs`
+- `lib/apps/fabro-server/src/server.rs`
Measured evidence:
- `fabro-cli::it::cmd::system_prune::system_prune_yes_deletes_matching_runs`: `10.477s`
@@ -87,11 +87,11 @@ Cons:
### [x] 3. Collapse the five abnormally slow `help` integration tests into one smoke test or a lighter harness
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/artifact.rs`
-- `lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs`
-- `lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs`
-- `lib/crates/fabro-cli/tests/it/cmd/config.rs`
-- `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/artifact.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/artifact_list.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/artifact_cp.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/config.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/attach.rs`
Measured evidence:
- Slow `help` tests:
@@ -131,8 +131,8 @@ Cons:
### [x] 4. Replace `doctor_no_color_when_no_color_set` with a render-path assertion
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/doctor.rs`
-- `lib/crates/fabro-util/src/check_report.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/doctor.rs`
+- `lib/foundation/fabro-util/src/check_report.rs`
Measured evidence:
- `fabro-cli::it::cmd::doctor::doctor_no_color_when_no_color_set`: `5.131s`
@@ -144,7 +144,7 @@ Estimated impact:
Implementation status:
- Implemented by deleting `fabro-cli::it::cmd::doctor::doctor_no_color_when_no_color_set`
-- Added a unit-level render assertion in `lib/crates/fabro-cli/src/commands/doctor.rs`:
+- Added a unit-level render assertion in `lib/apps/fabro-cli/src/commands/doctor.rs`:
- `render_report_text_without_color_has_no_ansi`
- Verified with `ulimit -n 4096; cargo nextest run -p fabro-cli render_report_text_without_color_has_no_ansi --status-level fail --final-status-level fail --show-progress none`
- Verification result: `1 passed`
@@ -166,8 +166,8 @@ Cons:
### [x] 5. Fix local Unix-socket autostart so it doesn't burn the full 5s readiness wait
Files:
-- `lib/crates/fabro-cli/src/server_client.rs`
-- `lib/crates/fabro-cli/tests/it/cmd/server_start.rs`
+- `lib/apps/fabro-cli/src/server_client.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/server_start.rs`
Measured evidence:
- Pre-fix 5-run timing for `fabro-cli::it::cmd::server_start::concurrent_autostart_converges_on_one_shared_daemon_and_cleans_up`:
@@ -197,7 +197,7 @@ Implementation status:
- Implemented by splitting the Unix-socket connection path into:
- a single immediate health probe before autostart
- the existing retrying readiness wait after autostart
-- Kept the original integration test coverage in `lib/crates/fabro-cli/tests/it/cmd/server_start.rs`
+- Kept the original integration test coverage in `lib/apps/fabro-cli/tests/it/cmd/server_start.rs`
- Verified with `ulimit -n 4096; cargo nextest run -p fabro-cli concurrent_autostart_converges_on_one_shared_daemon_and_cleans_up --status-level fail --final-status-level fail --show-progress none`
- Verification result: `1 passed`
- Post-change 5-run timing for `fabro-cli::it::cmd::server_start::concurrent_autostart_converges_on_one_shared_daemon_and_cleans_up`:
@@ -217,7 +217,7 @@ Implementation status:
### [x] 6. Collapse three lightweight `attach` smoke tests into one scenario-style test
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/attach.rs`
Measured evidence:
- `attach_requires_run_arg`: `1.595s`
@@ -230,7 +230,7 @@ Estimated impact:
- Conservative recoverable time: about `3.15s`
Implementation status:
-- Implemented by removing the 3 command-owned smoke tests from `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
+- Implemented by removing the 3 command-owned smoke tests from `lib/apps/fabro-cli/tests/it/cmd/attach.rs`
- Added `fabro-cli::it::scenario::smoke::attach_smoke_covers_arg_validation_and_remote_server_behaviors`
- Verified with `ulimit -n 4096; cargo nextest run -p fabro-cli attach_smoke_covers_arg_validation_and_remote_server_behaviors --status-level fail --final-status-level fail --show-progress none`
- Verification result: `1 passed`
@@ -256,7 +256,7 @@ Cons:
### [x] 7. Collapse the three `completion` tests
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/completion.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/completion.rs`
Measured evidence:
- `completion::generates_zsh_completions`: `1.567s`
@@ -295,7 +295,7 @@ Cons:
### [x] 8. Remove or merge the duplicate attach replay test
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/attach.rs`
Measured evidence:
- `attach_replays_completed_detached_run`: `2.696s`
@@ -303,7 +303,7 @@ Measured evidence:
- The two tests are currently identical in code and assertions
Implementation status:
-- Implemented by removing the duplicate test from `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
+- Implemented by removing the duplicate test from `lib/apps/fabro-cli/tests/it/cmd/attach.rs`
- Verified with `ulimit -n 4096; cargo nextest run -p fabro-cli attach_replays_completed_detached_run --status-level fail --final-status-level fail --show-progress none`
- Verification result: `1 passed`
@@ -325,7 +325,7 @@ Cons:
### [x] 9. Make `attach_before_completion_streams_to_finished_state` event-driven instead of sleep-driven
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/attach.rs`
Measured evidence:
- `attach_before_completion_streams_to_finished_state`: `3.043s`
@@ -333,7 +333,7 @@ Measured evidence:
- `write_gated_workflow()` adds another fixed `sleep 0.2`
Implementation status:
-- Implemented by replacing the fixed 1-second gate-release sleep with a real attach-output signal in `lib/crates/fabro-cli/tests/it/cmd/attach.rs`
+- Implemented by replacing the fixed 1-second gate-release sleep with a real attach-output signal in `lib/apps/fabro-cli/tests/it/cmd/attach.rs`
- The test now spawns `fabro attach`, waits for replayed stderr output (`✓ start`), then releases the workflow gate
- Verified with `ulimit -n 4096; cargo nextest run -p fabro-cli attach_before_completion_streams_to_finished_state --status-level fail --final-status-level fail --show-progress none`
- Verification result: `1 passed`
@@ -361,14 +361,14 @@ Cons:
### [x] 10. Remove or parameterize the fixed `sleep 0.2` in `write_gated_workflow()`
Files:
-- `lib/crates/fabro-cli/tests/it/cmd/support.rs`
+- `lib/apps/fabro-cli/tests/it/cmd/support.rs`
Measured evidence:
- `write_gated_workflow()` hardcodes `sleep 0.2`
- The helper is used in 6 cmd tests
Implementation status:
-- Implemented by deleting the fixed `sleep 0.2` from `write_gated_workflow()` in `lib/crates/fabro-cli/tests/it/cmd/support.rs`
+- Implemented by deleting the fixed `sleep 0.2` from `write_gated_workflow()` in `lib/apps/fabro-cli/tests/it/cmd/support.rs`
- Verified with `ulimit -n 4096; cargo nextest run -p fabro-cli -E 'test(attach_before_completion_streams_to_finished_state) | test(ctrl_c_cancels_active_run_via_server) | test(rm_force_terminates_active_run_worker) | test(start_rejects_already_active_or_completed_run) | test(start_runs_under_server_ownership_without_launcher_record)' --status-level fail --final-status-level fail --show-progress none`
- Verification result: `5 passed`
- Targeted 5-pass benchmark with `ulimit -n 4096` over the 5 tests that currently use the helper:
diff --git a/docs/internal/testing-strategy.md b/docs/internal/testing-strategy.md
index 5ec1534f5..b5fa9488a 100644
--- a/docs/internal/testing-strategy.md
+++ b/docs/internal/testing-strategy.md
@@ -37,7 +37,7 @@ This is the right place for:
If the setup requires direct writes to internal run files or runtime directories, prefer this layer over `fabro-cli/tests/it`.
-### `lib/crates/fabro-cli/tests/it/cmd/*.rs`
+### `lib/apps/fabro-cli/tests/it/cmd/*.rs`
`cmd/*` tests are command-owned tests.
@@ -64,7 +64,7 @@ Bad command-test assertions:
- behavior primarily owned by another command
- runtime internals that only exist because the test planted them by hand
-### `lib/crates/fabro-cli/tests/it/workflow/*.rs`
+### `lib/apps/fabro-cli/tests/it/workflow/*.rs`
`workflow/*` tests are black-box workflow-behavior tests.
@@ -79,7 +79,7 @@ Examples:
These tests should focus on the workflow's observed behavior, not on CLI help text or command argument validation.
-### `lib/crates/fabro-cli/tests/it/scenario/*.rs`
+### `lib/apps/fabro-cli/tests/it/scenario/*.rs`
`scenario/*` tests are cross-command lifecycle tests.
diff --git a/docs/public/agents/outputs.mdx b/docs/public/agents/outputs.mdx
index e03b4e08f..b726a9ffd 100644
--- a/docs/public/agents/outputs.mdx
+++ b/docs/public/agents/outputs.mdx
@@ -66,7 +66,7 @@ JSON objects without recognized fields are ignored.
### Validated routing output
-Set `output_schema="routing"` on an agent or prompt node to require Fabro's built-in routing directive schema:
+Set `output_schema="routing"` on an agent, prompt, or command node to require Fabro's built-in routing directive schema:
```dot
review [
@@ -81,7 +81,9 @@ With `output_schema="routing"`, the routing JSON must be an object with at least
Validated routing uses the same reverse scan as normal routing extraction: Fabro validates the last parsable JSON object that contains a recognized routing field. If a routing object is present but malformed or has invalid field types, Fabro repairs that response instead of falling through to a file fallback.
-Fabro repairs invalid structured output inside the same LLM context before failing the node. For prompt nodes, Fabro appends the invalid assistant response and a corrective user message to the same message list. For agent nodes using the API backend, Fabro sends the corrective message to the same live agent session. `output_retries` controls these repair turns and defaults to `2`; `output_retries=0` validates once and fails without a repair turn. Negative values are treated as `0`. These repair turns are separate from workflow `max_retries` and do not consume node retry attempts.
+Fabro repairs invalid structured output inside the same LLM context before failing an agent or prompt node. For prompt nodes, Fabro appends the invalid assistant response and a corrective user message to the same message list. For agent nodes using the API backend, Fabro sends the corrective message to the same live agent session. `output_retries` controls these repair turns and defaults to `2`; `output_retries=0` validates once and fails without a repair turn. Negative values are treated as `0`. These repair turns are separate from workflow `max_retries` and do not consume node retry attempts.
+
+Command nodes instead validate only after an exit-code-`0` script, applying the same reverse scan to merged stdout and stderr. Print the intended JSON object last. Invalid output fails deterministically without a repair turn, command retry, or `status.json` fallback; `output_retries`, `retry_policy`, and `max_retries` do not retry the validation failure. Nonzero exits keep their normal command failure behavior without schema validation.
### Routing fallback sources
@@ -95,7 +97,9 @@ Agent nodes can provide routing directives through fallback files. Fabro checks
This fallback chain applies to normal routing extraction and to `output_schema="routing"`. For validated routing, Fabro only advances to the next source when the current source has no JSON object or no object with recognized routing fields. If the current source contains malformed routing JSON or valid JSON with wrong routing field types, validation fails and Fabro starts the repair loop instead.
-Prompt nodes do not use file fallbacks; they validate or extract routing directives from the response text only.
+The last-file fallback only reads `.json` and `.md` files (case-insensitive), and the routing JSON must be the final JSON object in the file, with only whitespace after it. Fabro ignores other file types and routing JSON followed by any other content. These restrictions do not apply to the dedicated `status.json` fallback.
+
+Prompt nodes do not use file fallbacks; they validate or extract routing directives from the response text only. Command nodes likewise have no file fallback and validate only their merged stdout and stderr.
If no source provides routing directives, the transition falls through to condition matching, unconditional edges, or weight-based tiebreaking as described in [Transitions](/workflows/transitions).
@@ -119,7 +123,7 @@ review -> approve [label="Approve"]
## Custom structured outputs
-Agent and prompt nodes can also validate their final JSON object against a JSON Schema file:
+Agent, prompt, and command nodes can also validate their final JSON object against a JSON Schema file:
```dot
audit [
@@ -130,9 +134,9 @@ audit [
]
```
-`output_schema="@path/to/schema.json"` uses the same workflow file-reference rules as prompt files: the schema is loaded relative to the workflow file and inlined before execution. The final JSON object in the LLM response is validated with `jsonschema`.
+`output_schema="@path/to/schema.json"` uses the same workflow file-reference rules as prompt files: the schema is loaded relative to the workflow file and inlined before execution. The final JSON object in the LLM response, or in a successful command's merged stdout and stderr, is validated with `jsonschema`.
-Custom schema validation only reads the response text. It does not fall back to `status.json` or the last file touched by the agent.
+Custom schema validation only reads response or command output text. It does not fall back to `status.json` or the last file touched by the agent.
When custom schema validation succeeds, Fabro stores the parsed JSON value in context at:
@@ -140,12 +144,12 @@ When custom schema validation succeeds, Fabro stores the parsed JSON value in co
|---|---|
| `output.{node_id}` | The parsed JSON object that matched the custom schema |
-For example, node `audit` writes its parsed custom output to `output.audit`. Fabro still stores the raw response text at `response.audit`.
+For example, node `audit` writes its parsed custom output to `output.audit`. Fabro still stores raw LLM response text at `response.audit`; command output remains available through `command.output`.
-If custom schema validation fails, Fabro sends concise validation feedback to the same prompt conversation or agent session and asks for corrected JSON. After `output_retries` repair turns are exhausted, the node fails terminally with `output schema validation failed after N repair attempt(s)`.
+If custom schema validation fails for an agent or prompt node, Fabro sends concise validation feedback to the same prompt conversation or agent session and asks for corrected JSON. After `output_retries` repair turns are exhausted, the node fails terminally with `output schema validation failed after N repair attempt(s)`. A command node instead fails deterministically after its first validation, without a repair turn or command retry.
-Structured output validation currently applies to agent and prompt nodes. `backend="acp"` does not support `output_schema` in this release. Custom schemas update `output.{node_id}`; routing schemas update routing fields and `context_updates` instead.
+Structured output validation applies to agent, prompt, and command nodes. `backend="acp"` does not support `output_schema` in this release. Custom schemas update `output.{node_id}`; routing schemas update routing fields and merge `context_updates` as flat context keys that edge conditions can read. Edge conditions cannot traverse the custom `output.{node_id}` object.
## Output logging
diff --git a/docs/public/agents/subagents.mdx b/docs/public/agents/subagents.mdx
index 11cf30c38..597bd96e3 100644
--- a/docs/public/agents/subagents.mdx
+++ b/docs/public/agents/subagents.mdx
@@ -44,7 +44,6 @@ Sub-agent failures do not automatically fail the parent stage. The parent receiv
Common cases:
-- **Hits `max_turns`** -- returns normally with its last output
- **Panics or errors** -- returned as a failed `wait` result
- **`spawn_agent` fails** -- returned immediately as a tool result
@@ -97,5 +96,5 @@ Sub-agents are most useful for:
Use [child runs](/execution/child-runs) instead when the delegated work should be a separate Fabro run with its own workflow, lifecycle, sandbox, checkpoints, and outputs.
-Sub-agents run with no turn limit by default. Pass `max_turns` when you want predictable cost or time bounds. All active sub-agents are cleaned up automatically when the parent session closes.
+Sub-agents run until they complete, fail, are cancelled, or hit the session's wall-clock timeout. All active sub-agents are cleaned up automatically when the parent session closes.
diff --git a/docs/public/api-reference/client-sdks.mdx b/docs/public/api-reference/client-sdks.mdx
index e0fcb4a94..b46291669 100644
--- a/docs/public/api-reference/client-sdks.mdx
+++ b/docs/public/api-reference/client-sdks.mdx
@@ -43,7 +43,7 @@ The `fabro-api` crate generates Rust structs, enums, and a `reqwest`-based HTTP
A `build.rs` script reads `docs/public/api-reference/fabro-api.yaml`, patches it from OpenAPI 3.1 to 3.0 for progenitor compatibility, and generates both types and a client. The generated code is written to `OUT_DIR` and included via:
```rust
-// lib/crates/fabro-api/src/lib.rs
+// lib/foundation/fabro-api/src/lib.rs
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
```
diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml
index a39a4c414..ec20f9a6b 100644
--- a/docs/public/api-reference/fabro-api.yaml
+++ b/docs/public/api-reference/fabro-api.yaml
@@ -5452,7 +5452,9 @@ paths:
operationId: listModels
tags: [Models]
summary: List Models
- description: Returns a paginated list of available LLM models from the built-in catalog.
+ description: |
+ Returns one row per provider/model offering from the catalog. Model IDs
+ are unique within a provider; `(provider, id)` is the resource identity.
parameters:
- $ref: "#/components/parameters/ModelProviderFilter"
- $ref: "#/components/parameters/ModelQueryFilter"
@@ -5487,7 +5489,8 @@ paths:
required: true
schema:
type: string
- description: The model identifier.
+ description: The canonical model ID or an alias.
+ - $ref: "#/components/parameters/ModelTestProviderParam"
- $ref: "#/components/parameters/ModelTestModeParam"
responses:
"200":
@@ -5996,6 +5999,17 @@ components:
$ref: "#/components/schemas/ModelTestMode"
example: basic
+ ModelTestProviderParam:
+ name: provider
+ in: query
+ required: false
+ description: |
+ Pin the test to this provider's offering. When omitted, the server
+ selects among ready providers by catalog priority.
+ schema:
+ $ref: "#/components/schemas/ProviderId"
+ example: openrouter
+
headers:
XRequestId:
description: >
@@ -7766,6 +7780,12 @@ components:
$ref: "#/components/schemas/SessionStatus"
model:
type: ["string", "null"]
+ description: Canonical model ID selected when the session was created.
+ provider:
+ oneOf:
+ - $ref: "#/components/schemas/ProviderId"
+ - type: "null"
+ description: Provider selected when the session was created.
active_turn:
oneOf:
- $ref: "#/components/schemas/SessionTurn"
@@ -7798,6 +7818,12 @@ components:
$ref: "#/components/schemas/SessionStatus"
model:
type: ["string", "null"]
+ description: Canonical model ID selected when the session was created.
+ provider:
+ oneOf:
+ - $ref: "#/components/schemas/ProviderId"
+ - type: "null"
+ description: Provider selected when the session was created.
active_turn:
oneOf:
- $ref: "#/components/schemas/SessionTurn"
@@ -7832,6 +7858,12 @@ components:
$ref: "#/components/schemas/SessionStatus"
model:
type: ["string", "null"]
+ description: Canonical model ID selected when the session was created.
+ provider:
+ oneOf:
+ - $ref: "#/components/schemas/ProviderId"
+ - type: "null"
+ description: Provider selected when the session was created.
active_turn:
oneOf:
- $ref: "#/components/schemas/SessionTurn"
@@ -7857,7 +7889,12 @@ components:
type: string
model:
type: string
- description: Catalog model ID or alias, or provider-qualified provider/model reference. Stored as the canonical catalog model ID.
+ description: |
+ Catalog model ID or alias. The server selects among ready
+ providers and stores the canonical model ID.
+ provider:
+ $ref: "#/components/schemas/ProviderId"
+ description: Optional provider pin. Provider-qualified model references remain accepted for compatibility.
SubmitTurnRequest:
type: object
@@ -8190,6 +8227,7 @@ components:
- reasoning
- reasoning_effort
- prompt_cache
+ - cache_control_breakpoints
- sampling_params
properties:
tools:
@@ -8206,6 +8244,12 @@ components:
prompt_cache:
type: boolean
description: Whether the model endpoint supports prompt caching.
+ cache_control_breakpoints:
+ type: boolean
+ description: >-
+ Whether the endpoint only caches when the request marks the
+ cacheable prefix with Anthropic-style cache_control breakpoints
+ (e.g. Claude via OpenRouter).
sampling_params:
type: boolean
description: Whether the model accepts classic sampling parameters (temperature, top_p).
@@ -8235,7 +8279,9 @@ components:
example: 1.50
Model:
- description: An available LLM model from the built-in catalog.
+ description: |
+ One provider's offering of an LLM model. The `id` is unique within
+ `provider`; `(provider, id)` is the stable resource identity.
type: object
required:
- id
@@ -8255,7 +8301,7 @@ components:
properties:
id:
type: string
- description: Unique model identifier.
+ description: Canonical human-facing model ID, unique within the provider.
example: "claude-opus-4-6"
provider:
$ref: "#/components/schemas/ProviderId"
@@ -8310,12 +8356,15 @@ components:
type: object
required:
- model_id
+ - provider
- status
properties:
model_id:
type: string
- description: The model identifier that was tested.
+ description: The canonical model ID that was tested.
example: "claude-opus-4-6"
+ provider:
+ $ref: "#/components/schemas/ProviderId"
status:
type: string
enum:
@@ -8406,7 +8455,7 @@ components:
$ref: "#/components/schemas/CompletionMessage"
model:
type: string
- description: Model ID or alias. Server picks default if omitted.
+ description: Model ID or alias. Server picks a ready-provider default if omitted.
system:
type: string
description: System prompt (convenience; prepended as a system message).
@@ -8442,7 +8491,7 @@ components:
description: Reasoning effort level.
provider:
type: string
- description: Provider to route to.
+ description: Optional provider pin.
provider_options:
description: Provider-specific options.
@@ -8459,12 +8508,15 @@ components:
CompletionResponse:
type: object
- required: [id, model, message, stop_reason, usage]
+ required: [id, model, provider, message, stop_reason, usage]
properties:
id:
type: string
model:
type: string
+ description: Canonical model ID selected for the request.
+ provider:
+ $ref: "#/components/schemas/ProviderId"
message:
$ref: "#/components/schemas/CompletionMessage"
stop_reason:
@@ -8518,7 +8570,10 @@ components:
same format the model writes back via `write_workflow_file`.
model:
type: string
- description: Model id or alias. Server picks the default if omitted.
+ description: Model ID or alias. Server picks a ready-provider default if omitted.
+ provider:
+ $ref: "#/components/schemas/ProviderId"
+ description: Optional provider pin.
PaginatedSavedQueryList:
description: Paginated list of saved queries.
@@ -10497,7 +10552,11 @@ components:
oneOf:
- $ref: "#/components/schemas/TodoListProjection"
- type: "null"
- description: Projected todo / task list for this stage.
+ description: |
+ Todo / task list owned by this stage's root agent session. OpenAI
+ child sessions have separate per-session plans that do not appear
+ here. Anthropic task lists are root-scoped and shared with child
+ sessions, so child mutations of that shared list do appear here.
subagents:
type: array
description: Subagents spawned by this stage, in replay/insertion order.
diff --git a/docs/public/changelog/2026-03-23.mdx b/docs/public/changelog/2026-03-23.mdx
index e3d47ecda..359f5bbc1 100644
--- a/docs/public/changelog/2026-03-23.mdx
+++ b/docs/public/changelog/2026-03-23.mdx
@@ -5,7 +5,7 @@ date: "2026-03-23"
## Unlimited agent tool rounds
-Agent stages no longer have an artificial cap on tool rounds per input or subagent turns. Previously, the default limits could cause agents to stop mid-task on complex operations. Both `max_tool_rounds_per_input` and subagent `max_turns` now default to unlimited, so agents run until they complete their work or hit the context window.
+Agent stages no longer have an artificial cap on tool rounds per input or subagent turns, so agents run until they complete their work or are interrupted.
## More
diff --git a/docs/public/changelog/2026-05-12.mdx b/docs/public/changelog/2026-05-12.mdx
index c7d47f285..96387b4e7 100644
--- a/docs/public/changelog/2026-05-12.mdx
+++ b/docs/public/changelog/2026-05-12.mdx
@@ -24,7 +24,7 @@ This is useful for workflows that prepare their own workspace, run against alrea
- Added `[run.clone]`, `[run.run_branch]`, and `[run.meta_branch]` settings for clone, branch, and metadata branch behavior
-- Added `[llm.providers]` and `[llm.models]` settings foundations for configurable provider and model catalog data
+- Added provider-scoped `[llm.providers..models.]` settings foundations for configurable provider and model catalog data
- Added typed provider `extra_headers` settings for gateway-backed LLM providers
diff --git a/docs/public/changelog/2026-05-13.mdx b/docs/public/changelog/2026-05-13.mdx
index 3cb981f80..8659dafaa 100644
--- a/docs/public/changelog/2026-05-13.mdx
+++ b/docs/public/changelog/2026-05-13.mdx
@@ -28,8 +28,7 @@ credentials = ["env:ACME_GATEWAY_API_KEY"]
x-portkey-api-key = { env = "PORTKEY_API_KEY" }
x-portkey-config = { literal = "@bedrock-prod" }
-[llm.models."team-code-large"]
-provider = "proxy"
+[llm.providers.proxy.models."team-code-large"]
api_id = "provider-wire-model-name"
default = true
```
diff --git a/docs/public/core-concepts/models.mdx b/docs/public/core-concepts/models.mdx
index 7bda6217b..ab88318dd 100644
--- a/docs/public/core-concepts/models.mdx
+++ b/docs/public/core-concepts/models.mdx
@@ -11,6 +11,29 @@ No single model is best at everything. Fabro lets you assign the right model to
## Model catalog
+Fabro keeps five related concepts separate:
+
+- A **provider** serves requests, such as `openai` or `openrouter`.
+- A **model slug** is Fabro's canonical, human-facing model ID, such as `gpt-5.6-sol`.
+- An **alias** is another user-facing selector, such as `gpt-56-sol`.
+- A **family** is display and matching metadata. It does not affect routing identity.
+- An **API ID** is the opaque model string sent on the provider wire. Workflows should never reference it.
+
+One provider's route to one model slug is an **offering**, identified by `(provider, model slug)`. The same slug and alias may appear on several providers. Within one provider, however, every slug or alias must identify exactly one offering.
+
+For an unqualified selector, Fabro checks canonical slugs before aliases, filters the candidates to providers whose adapters are ready, and then chooses the highest provider priority. Equal priorities use canonical provider ID order. An explicit provider restricts selection to that provider and is a pin: if it is unavailable, Fabro reports the error instead of silently switching.
+
+For example, the shared `gpt-56-sol` alias can be portable across direct OpenAI and OpenRouter offerings:
+
+| Ready providers | Selector | Selected offering |
+|---|---|---|
+| OpenAI only | `gpt-56-sol` | `openai/gpt-5.6-sol` |
+| OpenRouter only | `gpt-56-sol` | `openrouter/gpt-5.6-sol` |
+| OpenAI and OpenRouter | `gpt-56-sol` | OpenAI, because it has higher priority |
+| Both, with `provider = "openrouter"` | `gpt-56-sol` | OpenRouter, because the provider is pinned |
+
+Fabro performs this selection once when creating a run and persists the chosen provider and canonical slug in the run settings and graph. Resuming that run does not reconsider provider priority when credentials change. Runtime [model fallbacks](/execution/failures#model-fallbacks) are the separate mechanism for handling a later provider failure.
+
| Model | Provider | Aliases | Context | Cost (in/out per Mtok) | Speed |
|---|---|---|---|---|---|
| `claude-fable-5` | anthropic | `fable`, `claude-fable` | 1M | $10.00 / $50.00 | n/a |
@@ -46,7 +69,7 @@ Claude Fable 5 is available as an explicit model but is not the default Anthropi
## Configuring providers and models
-Fabro's catalog starts with the built-in providers and models, then merges any `[llm]` entries from settings. Provider and model IDs are strings, so a server or project can add an OpenAI-compatible provider without a Fabro release.
+Fabro's catalog starts with the built-in providers and models, then merges any `[llm]` entries from settings. Models are nested under their provider, so two providers can expose the same model slug without overwriting each other.
```toml title="settings.toml"
[llm.providers.proxy]
@@ -62,8 +85,7 @@ credentials = ["env:ACME_GATEWAY_API_KEY", "vault:ACME_GATEWAY_API_KEY"]
x-portkey-api-key = "{{ env.PORTKEY_API_KEY }}"
x-portkey-config = "@bedrock-prod"
-[llm.models."team-code-large"]
-provider = "proxy"
+[llm.providers.proxy.models."team-code-large"]
api_id = "provider-wire-model-name"
agent_profile = "anthropic"
display_name = "Team Code Large"
@@ -73,27 +95,26 @@ small_default = true
aliases = ["team-code"]
estimated_output_tps = 80
-[llm.models."team-code-large".limits]
+[llm.providers.proxy.models."team-code-large".limits]
context_window = 200000
max_output = 32000
-[llm.models."team-code-large".features]
+[llm.providers.proxy.models."team-code-large".features]
tools = true
reasoning = true
reasoning_effort = "levels"
prompt_cache = true
-effort = true
-[llm.models."team-code-large".controls]
+[llm.providers.proxy.models."team-code-large".controls]
reasoning_effort = ["low", "medium", "high"]
speed = ["fast"]
-[llm.models."team-code-large".costs]
+[llm.providers.proxy.models."team-code-large".costs]
input_cost_per_mtok = 1.50
output_cost_per_mtok = 8.00
cache_input_cost_per_mtok = 0.30
-[llm.models."team-code-large".costs.speed.fast]
+[llm.providers.proxy.models."team-code-large".costs.speed.fast]
input_cost_per_mtok = 3.00
output_cost_per_mtok = 16.00
cache_input_cost_per_mtok = 0.60
@@ -106,24 +127,27 @@ For [LiteLLM](/integrations/litellm), Fabro ships a disabled provider entry. Ena
enabled = true
base_url = "http://localhost:4000/v1"
-[llm.models."litellm-gpt-5"]
-provider = "litellm"
+[llm.providers.litellm.models."litellm-gpt-5"]
api_id = "gpt-5"
display_name = "LiteLLM GPT-5"
family = "litellm"
default = true
-[llm.models."litellm-gpt-5".limits]
+[llm.providers.litellm.models."litellm-gpt-5".limits]
context_window = 128000
max_output = 8192
-[llm.models."litellm-gpt-5".features]
+[llm.providers.litellm.models."litellm-gpt-5".features]
tools = true
vision = false
reasoning = false
```
-`api_id` is the model name sent to the provider API. Omit it when the Fabro model ID and provider model ID are the same.
+`api_id` is the opaque model name sent to that provider's API. It defaults to the exact model slug, so omit it when the two strings match. Fabro does not infer vendor prefixes or rewrite the value.
+
+
+Historical built-in catalog keys that exposed provider API IDs remain accepted as compatibility selectors. Fabro normalizes a primary or node selector such as `openai/gpt-5.6-sol` to the canonical `gpt-5.6-sol` slug before normal provider-aware selection. With no provider pin, the highest-priority ready offering wins; a separate `provider = "openrouter"` pin selects the OpenRouter offering. Fabro also normalizes these keys in legacy top-level `[llm.models]` rows without rewriting the settings file.
+
Model roles are separate: `default = true` controls normal model selection for workflow execution, while `small_default = true` marks the provider's small/cheap utility model for metadata tasks such as generated run titles. If a provider has no small default, Fabro falls back to that provider's normal default.
@@ -139,7 +163,7 @@ Provider fields in configuration, APIs, and model routing are provider ID string
### Poolside
-Fabro ships a built-in [Poolside](/integrations/poolside) provider for Laguna S 2.1 and Laguna XS 2.1 over Poolside's OpenAI-compatible API. Store a direct API key with `fabro provider login --provider poolside`. The same models are also available through the opt-in OpenRouter provider under vendor-namespaced IDs.
+Fabro ships a built-in [Poolside](/integrations/poolside) provider for Laguna S 2.1 and Laguna XS 2.1 over Poolside's OpenAI-compatible API. Store a direct API key with `fabro provider login --provider poolside`. The same model slugs are also available through the opt-in OpenRouter provider; its vendor-namespaced strings remain provider-only `api_id` values.
### OpenRouter
@@ -169,11 +193,11 @@ Fabro ships an Ollama provider definition that is disabled by default. Enable it
enabled = true
```
-Enabling the provider alone does not expose any models — until #267 adds auto-discovery, add explicit `[llm.models.]` blocks for each Ollama model you have pulled locally. Ollama's OpenAI-compatible endpoint accepts any bearer token, so local users can set `OLLAMA_API_KEY=ollama`.
+Enabling the provider alone does not expose any models — until #267 adds auto-discovery, add explicit `[llm.providers.ollama.models.""]` blocks for each Ollama model you have pulled locally. Ollama's OpenAI-compatible endpoint accepts any bearer token, so local users can set `OLLAMA_API_KEY=ollama`.
## Default models
-When no model or provider is specified, Fabro checks configured provider credentials and chooses the first configured provider by catalog priority. If no provider credentials are configured, it uses the catalog's global default model. Each provider has a default model:
+When no model or provider is specified, Fabro chooses the default offering on the highest-priority ready provider. If no provider adapter is ready, run creation reports that no eligible offering is available. Each provider has its own default model:
| Provider | Default model |
|---|---|
@@ -184,7 +208,7 @@ When no model or provider is specified, Fabro checks configured provider credent
| `poolside` | `laguna-s-2.1` |
| `zai` | `glm-4.7` |
| `minimax` | `minimax-m2.5` |
-| `inception` | `mercury` |
+| `inception` | `mercury-2` |
## Using models in workflows
@@ -221,7 +245,7 @@ fabro run docs/internal/demo/01-hello.fabro --model claude-opus-4-6
fabro run docs/internal/demo/04-pipeline.fabro --model gemini-3.1-pro-preview
```
-These flags set the default model for all nodes that don't have an explicit model assigned via a stylesheet. The provider is automatically inferred from the model catalog — you only need `--provider` for models not in the catalog or to force a specific provider.
+These flags set the default model for all nodes that don't have an explicit model assigned via a stylesheet. Without `--provider`, Fabro selects among ready offerings by priority. Add `--provider` to pin an exact provider, including for an uncatalogued provider model string.
### Run config TOML
@@ -247,7 +271,7 @@ Then launch with:
fabro run run.toml
```
-The `fallbacks` array is optional. Each entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. Fabro tries them in order when the primary provider is unavailable.
+The `fallbacks` array is optional. Each entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. Fabro tries them in order when the primary provider is unavailable. In this field, qualified references keep their established provider-pin meaning: `"openai/gpt-5.6-sol"` selects the direct OpenAI offering.
The precedence order is: node-level stylesheet > run config TOML > CLI flags > server defaults. More specific settings always win.
diff --git a/docs/public/examples/repl-handoff.mdx b/docs/public/examples/repl-handoff.mdx
index a89942013..bf05c425e 100644
--- a/docs/public/examples/repl-handoff.mdx
+++ b/docs/public/examples/repl-handoff.mdx
@@ -73,7 +73,7 @@ Use Claude Code's plan mode to collaborate on an implementation plan. Go back an
> /plan Add retry logic to the webhook delivery system
Planning...
-1. Add RetryPolicy struct to lib/crates/fabro-webhooks/src/policy.rs
+1. Add RetryPolicy struct to lib/components/fabro-webhooks/src/policy.rs
2. Implement exponential backoff with jitter
3. Add max_retries field to WebhookConfig
4. Write tests for retry timing, max attempts, and jitter bounds
diff --git a/docs/public/execution/context.mdx b/docs/public/execution/context.mdx
index 25f6e536d..6f25c39c4 100644
--- a/docs/public/execution/context.mdx
+++ b/docs/public/execution/context.mdx
@@ -140,6 +140,16 @@ Fidelity can be set at three levels. The first match wins:
If none of these are set, fidelity defaults to `compact`.
+### Parallel branch fidelity
+
+The first node in each parallel branch uses this precedence:
+
+1. `fidelity` on the fork-to-branch edge
+2. `fidelity` on the branch node
+3. Otherwise, inherit the fork's preamble unchanged
+
+Fabro renders any branch-specific preambles before fan-out from the fork's context snapshot, then places them into the isolated branch contexts. An explicit branch-level `full` degrades to `summary:high` because concurrent branches cannot share conversation sessions. `thread_id` on a branch node or fork-to-branch edge is inert.
+
### Full fidelity and threads
`full` fidelity is typically used with `thread_id` to create a shared conversation across multiple nodes. Nodes with the same `thread_id` share a single LLM session, preserving full context continuity:
diff --git a/docs/public/execution/failures.mdx b/docs/public/execution/failures.mdx
index b76ae98e1..a74eefa9f 100644
--- a/docs/public/execution/failures.mdx
+++ b/docs/public/execution/failures.mdx
@@ -121,7 +121,15 @@ provider = "anthropic"
fallbacks = ["gemini", "openai"]
```
-When Anthropic is unavailable, Fabro tries Gemini first, then OpenAI. Each fallback entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. For each fallback provider, Fabro selects the closest model by matching required capabilities (tool use, vision, reasoning) and minimizing cost difference.
+When Anthropic fails, Fabro tries Gemini first, then OpenAI. Fallback resolution is provider-aware:
+
+- A bare provider token such as `"gemini"` selects that provider's closest compatible model.
+- A qualified selector such as `"openrouter/gpt-56-sol"` resolves only within that provider.
+- A bare model slug or alias considers ready providers and uses provider priority.
+
+Qualified fallback references always remain provider pins, including strings that were historical built-in API IDs. For example, `"openai/gpt-5.6-sol"` pins the direct OpenAI offering.
+
+The primary provider and model were already resolved and persisted when the run was created; resuming does not re-run primary selection. Fallbacks are only considered after an eligible runtime failure.
### What triggers failover
diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx
index 1b51a9161..3cb544906 100644
--- a/docs/public/execution/run-configuration.mdx
+++ b/docs/public/execution/run-configuration.mdx
@@ -138,12 +138,16 @@ name = "claude-sonnet-4-5"
| Field | Description |
|---|---|
-| `name` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`). See [Models](/core-concepts/models). |
-| `provider` | Provider name (optional — auto-inferred from the model catalog). Only needed for models not in the catalog or to force a specific provider. |
+| `name` | Canonical model slug or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`). See [Models](/core-concepts/models). |
+| `provider` | Optional provider pin. When omitted, Fabro selects among ready offerings by provider priority. When present, an unavailable provider is an error rather than permission to switch. |
| `fallbacks` | Ordered list of model references to try when the primary is unavailable. Entries can be bare provider tokens (`"openai"`), bare model aliases, or qualified `"provider/model"` references. |
Provider values are catalog provider ID strings. Built-in IDs like `anthropic` and `openai` work, and settings-defined IDs like `proxy` work after they are added under `[llm.providers.]`.
+At run creation, Fabro resolves the primary selector and every node selector against the ready-provider snapshot. It persists the selected canonical model slug and provider, so resuming the run does not choose a different provider just because credentials or priorities changed. The configured fallback chain remains available for failures that occur while the materialized run is executing.
+
+Historical built-in provider API IDs are accepted for compatibility and normalize before this selection. For example, `name = "openai/gpt-5.6-sol"` is treated as the canonical `gpt-5.6-sol` selector; omit `provider` to use readiness and priority, or set `provider` separately to pin an offering.
+
#### `[run.model.controls]`
Set default model controls for all nodes that do not override them in the workflow stylesheet:
diff --git a/docs/public/integrations/bedrock.mdx b/docs/public/integrations/bedrock.mdx
index 66a03c91c..34781fe59 100644
--- a/docs/public/integrations/bedrock.mdx
+++ b/docs/public/integrations/bedrock.mdx
@@ -157,6 +157,6 @@ Bedrock-specific request fields pass through verbatim via `provider_options.bedr
How Fabro routes model IDs, providers, and fallbacks.
- Full reference for `[llm.providers.]` and `[llm.models.]`.
+ Full reference for provider settings and provider-scoped model offerings.
diff --git a/docs/public/integrations/litellm.mdx b/docs/public/integrations/litellm.mdx
index 7f1891f8d..5fb1b6955 100644
--- a/docs/public/integrations/litellm.mdx
+++ b/docs/public/integrations/litellm.mdx
@@ -24,18 +24,17 @@ _version = 1
enabled = true
base_url = "http://localhost:4000/v1"
-[llm.models."litellm-gpt-5"]
-provider = "litellm"
+[llm.providers.litellm.models."litellm-gpt-5"]
api_id = "gpt-5"
display_name = "LiteLLM GPT-5"
family = "litellm"
default = true
-[llm.models."litellm-gpt-5".limits]
+[llm.providers.litellm.models."litellm-gpt-5".limits]
context_window = 128000
max_output = 8192
-[llm.models."litellm-gpt-5".features]
+[llm.providers.litellm.models."litellm-gpt-5".features]
tools = true
vision = false
reasoning = false
@@ -94,18 +93,17 @@ digraph Example {
Declare each LiteLLM-routed model explicitly so Fabro knows its provider, context window, tool support, and routing defaults:
```toml title="settings.toml"
-[llm.models."litellm-fast"]
-provider = "litellm"
+[llm.providers.litellm.models."litellm-fast"]
api_id = "fast-model"
display_name = "LiteLLM Fast"
family = "litellm"
aliases = ["fast"]
-[llm.models."litellm-fast".limits]
+[llm.providers.litellm.models."litellm-fast".limits]
context_window = 64000
max_output = 4096
-[llm.models."litellm-fast".features]
+[llm.providers.litellm.models."litellm-fast".features]
tools = true
vision = false
reasoning = false
@@ -128,6 +126,6 @@ Only one model for a provider should set `default = true`. You may also mark one
How Fabro routes model IDs, providers, and fallbacks.
- Full reference for `[llm.providers.]` and `[llm.models.]`.
+ Full reference for provider settings and provider-scoped model offerings.
diff --git a/docs/public/integrations/openrouter.mdx b/docs/public/integrations/openrouter.mdx
index b1b6f2534..6f1f046b5 100644
--- a/docs/public/integrations/openrouter.mdx
+++ b/docs/public/integrations/openrouter.mdx
@@ -44,34 +44,33 @@ export OPENROUTER_API_KEY=sk-or-v1-...
## Included models
-The built-in catalog curates frontier and open-weights models under vendor-namespaced IDs:
+The built-in catalog gives OpenRouter offerings the same human-facing model slugs used by direct providers. Vendor-namespaced OpenRouter IDs remain opaque `api_id` values:
-| Fabro model ID | Notes |
+| Fabro model slug | OpenRouter API ID / notes |
| --- | --- |
-| `anthropic/claude-opus-4-7` | Claude via OpenRouter, Anthropic-style cache billing |
-| `anthropic/claude-sonnet-4-6` | Provider default |
-| `anthropic/claude-haiku-4-5` | Provider small default |
-| `openai/gpt-5.4`, `openai/gpt-5.5` | |
-| `google/gemini-3.1-pro-preview`, `google/gemini-3.5-flash` | |
-| `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | |
-| `moonshotai/kimi-k2.6`, `qwen/qwen3-coder`, `qwen/qwen3.6-flash` | |
-| `poolside/laguna-s-2.1`, `poolside/laguna-xs-2.1` | Poolside Laguna coding models with native reasoning and tool use |
-| `z-ai/glm-4.6`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro` | |
-| `nvidia/nemotron-3-super-120b-a12b`, `mistralai/devstral-2512` | |
+| `claude-opus-4-7` | `anthropic/claude-opus-4.7`; Anthropic-style cache billing |
+| `claude-sonnet-4-6` | `anthropic/claude-sonnet-4.6`; provider default |
+| `claude-haiku-4-5` | `anthropic/claude-haiku-4.5`; provider small default |
+| `gpt-5.4`, `gpt-5.5` | `openai/gpt-5.4`, `openai/gpt-5.5` |
+| `gemini-3.1-pro-preview`, `gemini-3.5-flash` | `google/...` API IDs |
+| `deepseek-v4-pro`, `deepseek-v4-flash` | `deepseek/...` API IDs |
+| `kimi-k2.6`, `qwen3-coder`, `qwen3.6-flash` | Vendor-prefixed API IDs |
+| `laguna-s-2.1`, `laguna-xs-2.1` | `poolside/...`; native reasoning and tool use |
+| `glm-4.6`, `minimax-m2.7`, `mimo-v2.5-pro` | Vendor-prefixed API IDs |
+| `nemotron-3-super-120b-a12b`, `devstral-2512` | Vendor-prefixed API IDs |
-Any other OpenRouter model can be added as a settings model entry with `provider = "openrouter"` and the OpenRouter slug as `api_id`:
+Any other OpenRouter model can be added under the provider. Choose a stable Fabro model slug as the table key and put OpenRouter's exact vendor/model string in `api_id`:
```toml title="settings.toml"
-[llm.models."meta-llama/llama-4-maverick"]
-provider = "openrouter"
+[llm.providers.openrouter.models."llama-4-maverick"]
api_id = "meta-llama/llama-4-maverick"
display_name = "Llama 4 Maverick"
family = "llama-4"
-[llm.models."meta-llama/llama-4-maverick".limits]
+[llm.providers.openrouter.models."llama-4-maverick".limits]
context_window = 1000000
-[llm.models."meta-llama/llama-4-maverick".features]
+[llm.providers.openrouter.models."llama-4-maverick".features]
tools = true
vision = false
reasoning = false
@@ -81,8 +80,8 @@ reasoning = false
```bash
fabro model list --provider openrouter
-fabro model test --model anthropic/claude-sonnet-4-6
-fabro run workflow.fabro --model deepseek/deepseek-v4-flash
+fabro model test --provider openrouter --model claude-sonnet-4-6
+fabro run workflow.fabro --provider openrouter --model deepseek-v4-flash
```
When targeting a non-default remote server, pass the same `--server` value to verification commands:
@@ -159,6 +158,6 @@ Fabro does not send OpenRouter's optional attribution headers (`HTTP-Referer`, `
How Fabro routes model IDs, providers, and fallbacks.
- Full reference for `[llm.providers.]` and `[llm.models.]`.
+ Full reference for provider settings and provider-scoped model offerings.
diff --git a/docs/public/reference/dot-language.mdx b/docs/public/reference/dot-language.mdx
index 678d5ac23..8c350d1ba 100644
--- a/docs/public/reference/dot-language.mdx
+++ b/docs/public/reference/dot-language.mdx
@@ -201,12 +201,12 @@ Start nodes can also be identified by ID (`start` or `Start`). Exit nodes can be
| `prompt` | String | Task instructions for the LLM. Supports file references with `@path/to/file.md` |
| `reasoning_effort` | String | `low`, `medium`, or `high` (default: `high`) |
| `max_tokens` | Integer | Maximum output tokens |
-| `fidelity` | String | How much prior context is passed: `compact`, `full`, `summary:high`, `summary:medium`, `summary:low`, `truncate` |
-| `thread_id` | String | Groups nodes into a shared conversation thread |
+| `fidelity` | String | How much prior context is passed: `compact`, `full`, `summary:high`, `summary:medium`, `summary:low`, `truncate`. On a node entered directly from a parallel fork, this is overridden by the fork-to-branch edge; explicit `full` degrades to `summary:high`. |
+| `thread_id` | String | Groups nodes into a shared conversation thread. Inert when the node is entered directly from a parallel fork. |
| `model` | String | Explicit model ID (overrides stylesheet) |
| `provider` | String | Explicit provider name (overrides stylesheet). Auto-inferred from the model catalog when omitted. |
| `project_memory` | Boolean | When `true` (default), prompt nodes discover and include project docs (`AGENTS.md`, `CLAUDE.md`, etc.) as a system prompt. Set to `false` to disable. |
-| `output_schema` | String | Optional structured output validation. Use `routing` for Fabro's built-in routing directive schema, `@path/to/schema.json` for a JSON Schema file, or an inline JSON Schema object string. Supported on agent and prompt nodes. |
+| `output_schema` | String | Optional structured output validation. Use `routing` for Fabro's built-in routing directive schema, `@path/to/schema.json` for a JSON Schema file, or an inline JSON Schema object string. Supported on agent, prompt, and command nodes. |
| `output_retries` | Integer | Corrective structured-output turns inside the same prompt conversation or agent session. Default `2`; `0` validates once and fails without repair; negative values are treated as `0`. Separate from `max_retries`. |
| `backend` | String | Agent execution backend: `api` (default) or `acp`. `api` runs Fabro's tool loop through provider APIs; `acp` runs an Agent Client Protocol stdio agent inside the active sandbox. Prompt nodes are API-only. See [Agents — Backends](/core-concepts/agents#backends). |
| `acp.command` | String | Shell command for nodes with `backend="acp"`. Mutually exclusive with `acp.config`. The value is always parsed as a command string, not JSON. |
@@ -214,7 +214,7 @@ Start nodes can also be identified by ID (`start` or `Start`). Exit nodes can be
#### Structured output validation
-`output_schema` opts an agent or prompt node into strict JSON validation:
+`output_schema` opts an agent, prompt, or command node into strict JSON validation:
```dot
review [
@@ -235,7 +235,9 @@ audit [
- On validation failure, Fabro sends validation feedback to the same active context before failing: prompt nodes keep the prior assistant response in the message list, and API-backed agent nodes repair in the same live session.
- `output_retries` defaults to `2` and controls only these corrective structured-output turns. Negative values are treated as `0`. It is not the same as `max_retries` and does not consume workflow retry attempts.
- Custom schema output is stored in context at `output.{node_id}`. Routing schema output updates routing fields and any `context_updates`.
-- Agent routing fallbacks still apply to `output_schema="routing"`: response text first, then `status.json`, then the last file touched by the agent. Custom schemas and prompt nodes validate response text only.
+- Agent routing fallbacks still apply to `output_schema="routing"`: response text first, then `status.json`, then the last file touched by the agent. The last-file fallback only accepts `.json` and `.md` files (case-insensitive) whose final JSON object contains the routing directive; only whitespace may follow it. Custom schemas and prompt nodes validate response text only.
+- Command nodes validate merged stdout and stderr only after the script exits with code `0`, using the same object selection as agents and prompts: custom schemas validate the last JSON object, and `routing` validates the last JSON object containing a recognized routing field. Print the intended JSON object last. A validation error is a deterministic, non-retryable failure with no repair turn or `status.json` fallback. `output_retries`, `retry_policy`, and `max_retries` do not retry it. Nonzero exits retain normal command failure behavior without schema validation.
+- For commands, custom schema output is stored at `output.{node_id}`; edge conditions cannot traverse into its fields. The `routing` schema applies routing fields and merges `context_updates` into flat context keys, which conditions can read (for example, `context.kept_count`).
- `backend="acp"` with `output_schema` is unsupported in this release.
### Command nodes
@@ -244,6 +246,7 @@ audit [
|---|---|---|
| `script` | String | Shell command to execute |
| `language` | String | `"shell"` (default) or `"python"` |
+| `output_schema` | String | Optional structured output validation. Accepts `routing`, `@path/to/schema.json`, or an inline JSON Schema object string. See [Structured output validation](#structured-output-validation). |
### Parallel (fan-out) nodes
@@ -251,6 +254,8 @@ audit [
|---|---|---|
| `max_parallel` | Integer | Maximum concurrent branches (default: 4). The node always waits for every branch. |
+For the first node in each branch, `fidelity` resolves from the fork-to-branch edge, then the branch node; without either, the fork preamble is inherited unchanged. Branch-specific preambles are rendered before fan-out from the fork's context snapshot. Concurrent branches cannot share sessions, so explicit branch `full` becomes `summary:high`, and branch-level `thread_id` is inert.
+
### Wait nodes
| Attribute | Type | Description |
@@ -281,8 +286,8 @@ audit [
| `label` | String | Display text; also used for human gate option matching |
| `condition` | String | Boolean expression for conditional routing (see below) |
| `weight` | Integer | Priority for tiebreaking (higher wins, default: 0) |
-| `fidelity` | String | Override fidelity level for this transition |
-| `thread_id` | String | Override thread ID for this transition |
+| `fidelity` | String | Override fidelity level for this transition. On a fork-to-branch edge, takes precedence over the branch node; explicit `full` degrades to `summary:high`. |
+| `thread_id` | String | Override thread ID for this transition. Inert on fork-to-branch edges. |
| `loop_restart` | Boolean | Restart the workflow from this edge's target when taken: stage history and retry counts clear and the context resets to empty (visit counts are kept). Failed outcomes may only take it for `transient_infra` failures — see [Failures](/execution/failures#loop-restart-edges) |
| `freeform` | Boolean | When `true` on a human-gate edge, accept free-text input instead of fixed choices |
diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx
index 4332f3adc..9f88c7696 100644
--- a/docs/public/reference/sdk.mdx
+++ b/docs/public/reference/sdk.mdx
@@ -84,7 +84,7 @@ pub fn new(
| Method | Description |
|---|---|
| `initialize().await` | Discovers project docs, skills, and MCP servers. Call before `process_input`. |
-| `process_input(input).await` | Sends user input and runs the agent loop until the model stops or a limit is hit. |
+| `process_input(input).await` | Sends user input and runs the agent loop until the model stops, the session is interrupted, or an error occurs. |
| `close()` | Ends the session and emits `SessionEnded`. |
| `interrupt()` | Cancels the current `process_input` call. |
| `cancel_token()` | Returns a `CancellationToken` for external cancellation. |
@@ -110,8 +110,6 @@ All fields are public. Key settings with their defaults:
| Field | Default | Description |
|---|---|---|
-| `max_turns` | `0` (unlimited) | Maximum conversation turns before stopping. |
-| `max_tool_rounds_per_input` | `200` | Maximum tool execution rounds per `process_input` call. |
| `default_command_timeout_ms` | `10,000` | Default timeout for Bash tool commands. |
| `max_command_timeout_ms` | `600,000` | Maximum allowed timeout for Bash tool commands. |
| `enable_loop_detection` | `true` | Detect and break out of repetitive tool call patterns. |
@@ -221,7 +219,6 @@ Key `AgentEvent` variants:
| `ToolCallCompleted { tool_name, tool_call_id, output, is_error }` | A tool call finished. |
| `Error { error }` | An `AgentError` occurred. |
| `LoopDetected` | The agent is repeating itself. |
-| `TurnLimitReached { max_turns }` | Turn limit hit. |
| `CompactionStarted` / `CompactionCompleted` | Context window compaction. |
| `SubAgentSpawned` / `SubAgentCompleted` | Sub-agent lifecycle. |
| `McpServerReady` / `McpServerFailed` | MCP server connection status. |
@@ -296,7 +293,7 @@ All fallible `Session` methods return `Result`:
| `SessionClosed` | `process_input` was called on a closed session. |
| `InvalidState(String)` | The session is in an unexpected state. |
| `ToolExecution(String)` | A tool execution failed. |
-| `Interrupted(InterruptReason)` | The session was cancelled (`Cancelled`) or timed out (`WallClockTimeout`). |
+| `Interrupted(InterruptReason)` | The session was cancelled or timed out. |
---
diff --git a/docs/public/reference/user-configuration.mdx b/docs/public/reference/user-configuration.mdx
index fe62e4ee7..dd6afd83f 100644
--- a/docs/public/reference/user-configuration.mdx
+++ b/docs/public/reference/user-configuration.mdx
@@ -35,7 +35,7 @@ Files that omit `_version` are treated as version `1`. The legacy top-level `ver
|---|---|
| CLI-only | `[cli.target]`, `[cli.auth]`, `[cli.exec]`, `[cli.output]`, `[cli.updates]`, `[cli.logging]` |
| Shared run defaults | `[run.model]`, `[run.environment]`, `[environments.]`, `[run.checkpoint]`, `[run.inputs]`, `[run.prepare]`, `[run.pull_request]`, `[run.integrations.github.permissions]`, `[run.hooks]`, `[run.agent.mcps]` |
-| Shared LLM catalog | `[llm.providers.]`, `[llm.models.]`, model limits, features, controls, and costs |
+| Shared LLM catalog | `[llm.providers.]`, provider-scoped `[llm.providers..models.]` offerings, limits, features, controls, and costs |
| Server-only | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
`[cli.*]` and `[server.*]` stanzas are owner-specific: they are only consumed from `~/.fabro/settings.toml` (plus process-local flags and env overrides). The same stanzas in `.fabro/project.toml` or `workflow.toml` remain schema-valid but runtime-inert.
@@ -97,23 +97,22 @@ credentials = ["env:ACME_GATEWAY_API_KEY", "vault:ACME_GATEWAY_API_KEY"]
x-portkey-api-key = "{{ env.PORTKEY_API_KEY }}"
x-portkey-config = "@bedrock-prod"
-[llm.models."team-code-large"]
-provider = "proxy"
+[llm.providers.proxy.models."team-code-large"]
api_id = "provider-wire-model-name"
agent_profile = "anthropic"
display_name = "Team Code Large"
default = true
aliases = ["team-code"]
-[llm.models."team-code-large".controls]
+[llm.providers.proxy.models."team-code-large".controls]
reasoning_effort = ["low", "medium", "high"]
speed = ["fast"]
-[llm.models."team-code-large".costs]
+[llm.providers.proxy.models."team-code-large".costs]
input_cost_per_mtok = 1.50
output_cost_per_mtok = 8.00
-[llm.models."team-code-large".costs.speed.fast]
+[llm.providers.proxy.models."team-code-large".costs.speed.fast]
input_cost_per_mtok = 3.00
output_cost_per_mtok = 16.00
@@ -201,19 +200,20 @@ x-team-secret = "{{ secrets.gateway_team_secret }}"
| `auth.credentials` | array | required when `auth` present | Ordered credential refs. Accepted forms are `vault:`, `env:`, and `aws_sigv4` (sign requests from the AWS default credential chain — Bedrock). Literal secret strings are rejected. |
| `auth.header` | `"bearer"` or `{ custom = "Header-Name" }` | `"bearer"` | Primary API-key header policy. Omit when the provider uses a standard bearer token. |
| `extra_headers` | table | `{}` | Additional headers attached to provider requests. Values are interpolation strings: literal text, an `{{ env.NAME }}` token, or a `{{ secrets.NAME }}` token. Put credentials in a secret and reference them with a `{{ secrets.NAME }}` token, not a bare literal. |
-| `priority` | integer | `0` | Higher-priority configured providers win default selection; ties use canonical provider ID. |
+| `priority` | integer | `0` | Higher-priority ready providers win unqualified model and default selection; ties use canonical provider ID. |
| `enabled` | boolean | `true` | Set `false` to disable a provider after lower-precedence layers define it. |
| `aliases` | array | `[]` | Additional provider names accepted by model routing and fallback config. |
-## `[llm.models.]`
+## `[llm.providers..models.]`
-Define or override a model in the catalog. The table key is the canonical
-model ID Fabro users reference; `api_id` is the model string sent to the
-provider API.
+Define or override one provider's offering of a model. The table key is the
+canonical model slug Fabro users reference. An offering's identity is the
+pair `(provider, model slug)`, so different providers may use the same slug
+and aliases. `api_id` is the opaque model string sent to this provider's API
+and defaults to the exact model slug.
```toml title="settings.toml"
-[llm.models."team-code-large"]
-provider = "proxy"
+[llm.providers.proxy.models."team-code-large"]
api_id = "provider-wire-model-name"
agent_profile = "anthropic"
display_name = "Team Code Large"
@@ -224,27 +224,27 @@ enabled = true
aliases = ["team-code"]
estimated_output_tps = 80
-[llm.models."team-code-large".limits]
+[llm.providers.proxy.models."team-code-large".limits]
context_window = 200000
max_output = 32000
-[llm.models."team-code-large".features]
+[llm.providers.proxy.models."team-code-large".features]
tools = true
vision = false
reasoning = true
reasoning_effort = "levels"
prompt_cache = true
-[llm.models."team-code-large".controls]
+[llm.providers.proxy.models."team-code-large".controls]
reasoning_effort = ["low", "medium", "high"]
speed = ["fast"]
-[llm.models."team-code-large".costs]
+[llm.providers.proxy.models."team-code-large".costs]
input_cost_per_mtok = 1.50
output_cost_per_mtok = 8.00
cache_input_cost_per_mtok = 0.30
-[llm.models."team-code-large".costs.speed.fast]
+[llm.providers.proxy.models."team-code-large".costs.speed.fast]
input_cost_per_mtok = 3.00
output_cost_per_mtok = 16.00
cache_input_cost_per_mtok = 0.60
@@ -252,8 +252,7 @@ cache_input_cost_per_mtok = 0.60
| Key | Type / values | Default | Description |
|---|---|---|---|
-| `provider` | string | None | Provider ID this model belongs to. |
-| `api_id` | string | model ID | Identifier sent to the provider API. |
+| `api_id` | string | model slug | Opaque identifier sent to this provider's API. An explicitly empty value is invalid. |
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` | provider profile | Agent profile override for this model. Model overrides take precedence over provider overrides. |
| `billing_policy` | `"openai"` \| `"anthropic"` \| `"gemini"` \| `"none"` | provider policy | Billing algorithm override for this model — for models whose billing family differs from their provider's (e.g. Claude served through OpenRouter bills Anthropic-style cache reads/writes). |
| `display_name` | string | model ID | Human-readable model name. |
@@ -263,17 +262,17 @@ cache_input_cost_per_mtok = 0.60
| `default` | boolean | `false` | Whether this is the provider default model. |
| `probe` | boolean | `false` | Whether this model should be preferred for provider connectivity probes. Set `false` in a higher-precedence layer to clear an inherited probe marker. |
| `enabled` | boolean | `true` | Set `false` to disable a model after lower-precedence layers define it. |
-| `aliases` | array | `[]` | Additional model names accepted by routing and fallback config. |
+| `aliases` | array | `[]` | Additional model selectors accepted by routing and fallback config. Aliases may repeat across providers, but one selector cannot identify two models within the same provider. |
| `estimated_output_tps` | number | None | Estimated output tokens per second for catalog display and planning. |
-## `[llm.models..limits]`
+## `[llm.providers..models..limits]`
| Key | Type / values | Default | Description |
|---|---|---|---|
| `context_window` | integer | None | Maximum context window size in tokens. |
| `max_output` | integer | None | Maximum output tokens, if known. |
-## `[llm.models..features]`
+## `[llm.providers..models..features]`
| Key | Type / values | Default | Description |
|---|---|---|---|
@@ -284,14 +283,14 @@ cache_input_cost_per_mtok = 0.60
| `prompt_cache` | boolean | `false` | Whether prompt cache pricing/usage applies. |
| `sampling_params` | boolean | `true` | Whether the model accepts classic sampling parameters (`temperature`, `top_p`). |
-## `[llm.models..controls]`
+## `[llm.providers..models..controls]`
| Key | Type / values | Default | Description |
|---|---|---|---|
| `reasoning_effort` | array | all standard levels when feature is `"levels"` or `"always_adaptive"` | User-facing reasoning effort values Fabro may send for this model. Can be set explicitly for reasoning models whose provider adapter maps effort to a non-native API shape. |
| `speed` | array | `[]` | Additional speeds beyond implicit `standard`; do not list `standard`. |
-## `[llm.models..costs]`
+## `[llm.providers..models..costs]`
| Key | Type / values | Default | Description |
|---|---|---|---|
@@ -299,10 +298,12 @@ cache_input_cost_per_mtok = 0.60
| `output_cost_per_mtok` | number | None | Output cost in USD per million tokens. |
| `cache_input_cost_per_mtok` | number | None | Cached input/read cost in USD per million tokens. |
-## `[llm.models..costs.speed.]`
+## `[llm.providers..models..costs.speed.]`
-Per-speed cost overrides use the same keys as `[llm.models..costs]`.
-Each `` key must be declared in `[llm.models..controls].speed`.
+Per-speed cost overrides use the same keys as
+`[llm.providers..models..costs]`. Each `` key
+must be declared in
+`[llm.providers..models..controls].speed`.
The `standard` speed is implicit and always uses the base cost table.
## `[cli.updates]`
diff --git a/docs/public/workflows/stages-and-nodes.mdx b/docs/public/workflows/stages-and-nodes.mdx
index 9263d1ea7..819befe7e 100644
--- a/docs/public/workflows/stages-and-nodes.mdx
+++ b/docs/public/workflows/stages-and-nodes.mdx
@@ -171,6 +171,8 @@ fork -> quality
Because the checkout is shared, file changes from one branch are immediately visible to the others. Concurrent writes can race or overwrite each other. Fabro does not isolate branch files, lock paths, detect conflicts, or warn about overlapping writes. Design branches to be read-only or assign each branch disjoint files and directories when deterministic workspace changes matter.
+For each branch's first node, fidelity resolves from the fork-to-branch edge, then the branch node; otherwise it inherits the fork preamble unchanged. Fabro renders branch-specific preambles before fan-out from the fork snapshot. Branch-level `full` degrades to `summary:high` because concurrent branches cannot share sessions, and `thread_id` on a branch node or fork-to-branch edge is inert.
+
### Merge (fan-in)
**Shape:** `tripleoctagon`
diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/apps/fabro-cli/Cargo.toml
similarity index 55%
rename from lib/crates/fabro-cli/Cargo.toml
rename to lib/apps/fabro-cli/Cargo.toml
index 6f14e385e..44c9982bc 100644
--- a/lib/crates/fabro-cli/Cargo.toml
+++ b/lib/apps/fabro-cli/Cargo.toml
@@ -18,40 +18,40 @@ sleep_inhibitor = ["dep:core-foundation"]
workspace = true
[dependencies]
-fabro-auth = { path = "../fabro-auth" }
-fabro-config = { path = "../fabro-config" }
-fabro-environment = { path = "../fabro-environment" }
-fabro-llm = { path = "../fabro-llm" }
-fabro-model = { path = "../fabro-model" }
-fabro-oauth = { path = "../fabro-oauth" }
-fabro-github = { path = "../fabro-github" }
-fabro-agent = { path = "../fabro-agent" }
-fabro-dump = { path = "../fabro-dump" }
-fabro-hooks = { path = "../fabro-hooks" }
-fabro-install = { path = "../fabro-install" }
-fabro-interview = { path = "../fabro-interview" }
-fabro-mcp = { path = "../fabro-mcp" }
+fabro-auth = { path = "../../foundation/fabro-auth" }
+fabro-config = { path = "../../foundation/fabro-config" }
+fabro-environment = { path = "../../components/fabro-environment" }
+fabro-llm = { path = "../../components/fabro-llm" }
+fabro-model = { path = "../../foundation/fabro-model" }
+fabro-oauth = { path = "../../foundation/fabro-oauth" }
+fabro-github = { path = "../../components/fabro-github" }
+fabro-agent = { path = "../../components/fabro-agent" }
+fabro-dump = { path = "../../components/fabro-dump" }
+fabro-hooks = { path = "../../components/fabro-hooks" }
+fabro-install = { path = "../../components/fabro-install" }
+fabro-interview = { path = "../../components/fabro-interview" }
+fabro-mcp = { path = "../../components/fabro-mcp" }
fabro-mcp-server = { path = "../fabro-mcp-server" }
-fabro-manifest = { path = "../fabro-manifest" }
-fabro-proc = { path = "../fabro-proc" }
-fabro-sandbox = { path = "../fabro-sandbox", features = ["daytona"] }
-fabro-checkpoint = { path = "../fabro-checkpoint" }
-fabro-graphviz = { path = "../fabro-graphviz" }
-fabro-validate = { path = "../fabro-validate" }
-fabro-workflow = { path = "../fabro-workflow" }
+fabro-manifest = { path = "../../components/fabro-manifest" }
+fabro-proc = { path = "../../foundation/fabro-proc" }
+fabro-sandbox = { path = "../../components/fabro-sandbox", features = ["daytona"] }
+fabro-checkpoint = { path = "../../components/fabro-checkpoint" }
+fabro-graphviz = { path = "../../components/fabro-graphviz" }
+fabro-validate = { path = "../../components/fabro-validate" }
+fabro-workflow = { path = "../../components/fabro-workflow" }
fabro-server = { path = "../fabro-server" }
-fabro-client = { path = "../fabro-client" }
-fabro-api = { path = "../fabro-api" }
-fabro-telemetry = { path = "../fabro-telemetry" }
-fabro-store = { path = "../fabro-store" }
-fabro-vault = { path = "../fabro-vault" }
-fabro-types = { path = "../fabro-types", features = ["clap"] }
+fabro-client = { path = "../../foundation/fabro-client" }
+fabro-api = { path = "../../foundation/fabro-api" }
+fabro-telemetry = { path = "../../foundation/fabro-telemetry" }
+fabro-store = { path = "../../components/fabro-store" }
+fabro-vault = { path = "../../foundation/fabro-vault" }
+fabro-types = { path = "../../foundation/fabro-types", features = ["clap"] }
fabro-redact.workspace = true
-fabro-util = { path = "../fabro-util" }
+fabro-util = { path = "../../foundation/fabro-util" }
fabro-http.workspace = true
fabro-static.workspace = true
-fabro-template = { path = "../fabro-template" }
-fabro-tool = { path = "../fabro-tool" }
+fabro-template = { path = "../../foundation/fabro-template" }
+fabro-tool = { path = "../../components/fabro-tool" }
clap.workspace = true
clap_complete.workspace = true
cli-table.workspace = true
@@ -111,15 +111,16 @@ core-foundation = { version = "0.9", optional = true }
openssl = { version = "0.10", features = ["vendored"] }
[build-dependencies]
-fabro-build-support = { path = "../build-support" }
+fabro-build-support = { path = "../../foundation/build-support" }
chrono = { workspace = true }
[dev-dependencies]
assert_cmd = "2"
-fabro-acp = { path = "../fabro-acp", features = ["test-support"] }
-fabro-build-support = { path = "../build-support" }
+fabro-acp = { path = "../../components/fabro-acp", features = ["test-support"] }
+fabro-build-support = { path = "../../foundation/build-support" }
fabro-server = { path = "../fabro-server", features = ["test-support"] }
-fabro-types = { path = "../fabro-types", features = ["clap", "test-support"] }
+fabro-workflow = { path = "../../components/fabro-workflow", features = ["test-support"] }
+fabro-types = { path = "../../foundation/fabro-types", features = ["clap", "test-support"] }
insta = { workspace = true, features = ["filters"] }
paste = "1"
predicates = "3"
@@ -128,7 +129,7 @@ tempfile = "3"
temp-env = "0.3"
httpmock = "0.8"
fabro-test = { workspace = true }
-fabro-macros = { path = "../fabro-macros" }
+fabro-macros = { path = "../../foundation/fabro-macros" }
hkdf.workspace = true
reqwest = { workspace = true, features = ["cookies"] }
tokio = { workspace = true, features = ["test-util", "macros"] }
diff --git a/lib/crates/fabro-cli/build.rs b/lib/apps/fabro-cli/build.rs
similarity index 100%
rename from lib/crates/fabro-cli/build.rs
rename to lib/apps/fabro-cli/build.rs
diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/apps/fabro-cli/src/args.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/args.rs
rename to lib/apps/fabro-cli/src/args.rs
diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/apps/fabro-cli/src/command_context.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/command_context.rs
rename to lib/apps/fabro-cli/src/command_context.rs
diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/apps/fabro-cli/src/commands/artifact/cp.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/artifact/cp.rs
rename to lib/apps/fabro-cli/src/commands/artifact/cp.rs
diff --git a/lib/crates/fabro-cli/src/commands/artifact/list.rs b/lib/apps/fabro-cli/src/commands/artifact/list.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/artifact/list.rs
rename to lib/apps/fabro-cli/src/commands/artifact/list.rs
diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/apps/fabro-cli/src/commands/artifact/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/artifact/mod.rs
rename to lib/apps/fabro-cli/src/commands/artifact/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/auth/login.rs b/lib/apps/fabro-cli/src/commands/auth/login.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/auth/login.rs
rename to lib/apps/fabro-cli/src/commands/auth/login.rs
diff --git a/lib/crates/fabro-cli/src/commands/auth/logout.rs b/lib/apps/fabro-cli/src/commands/auth/logout.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/auth/logout.rs
rename to lib/apps/fabro-cli/src/commands/auth/logout.rs
diff --git a/lib/crates/fabro-cli/src/commands/auth/mod.rs b/lib/apps/fabro-cli/src/commands/auth/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/auth/mod.rs
rename to lib/apps/fabro-cli/src/commands/auth/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/auth/status.rs b/lib/apps/fabro-cli/src/commands/auth/status.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/auth/status.rs
rename to lib/apps/fabro-cli/src/commands/auth/status.rs
diff --git a/lib/crates/fabro-cli/src/commands/cli_reference.rs b/lib/apps/fabro-cli/src/commands/cli_reference.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/cli_reference.rs
rename to lib/apps/fabro-cli/src/commands/cli_reference.rs
diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/apps/fabro-cli/src/commands/config/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/config/mod.rs
rename to lib/apps/fabro-cli/src/commands/config/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/apps/fabro-cli/src/commands/doctor.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/doctor.rs
rename to lib/apps/fabro-cli/src/commands/doctor.rs
diff --git a/lib/crates/fabro-cli/src/commands/dump.rs b/lib/apps/fabro-cli/src/commands/dump.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/dump.rs
rename to lib/apps/fabro-cli/src/commands/dump.rs
diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/apps/fabro-cli/src/commands/exec.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/exec.rs
rename to lib/apps/fabro-cli/src/commands/exec.rs
diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/apps/fabro-cli/src/commands/graph.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/graph.rs
rename to lib/apps/fabro-cli/src/commands/graph.rs
diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/apps/fabro-cli/src/commands/install.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/install.rs
rename to lib/apps/fabro-cli/src/commands/install.rs
diff --git a/lib/crates/fabro-cli/src/commands/mcp/mod.rs b/lib/apps/fabro-cli/src/commands/mcp/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/mcp/mod.rs
rename to lib/apps/fabro-cli/src/commands/mcp/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/mod.rs b/lib/apps/fabro-cli/src/commands/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/mod.rs
rename to lib/apps/fabro-cli/src/commands/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/apps/fabro-cli/src/commands/model.rs
similarity index 80%
rename from lib/crates/fabro-cli/src/commands/model.rs
rename to lib/apps/fabro-cli/src/commands/model.rs
index e63b926f4..f84bd44e2 100644
--- a/lib/crates/fabro-cli/src/commands/model.rs
+++ b/lib/apps/fabro-cli/src/commands/model.rs
@@ -45,10 +45,21 @@ struct CompletedModelTest {
status: String,
}
-fn find_model_by_id_or_alias(models: &[Model], id: &str) -> Option {
+fn model_matches_selector(model: &Model, selector: &str) -> bool {
+ model.id == selector || model.aliases.iter().any(|alias| alias == selector)
+}
+
+fn find_model_by_id_or_alias(
+ models: &[Model],
+ id: &str,
+ provider: Option<&ProviderId>,
+) -> Option {
models
.iter()
- .find(|model| model.id == id || model.aliases.iter().any(|alias| alias == id))
+ .find(|model| {
+ provider.is_none_or(|provider| &model.provider == provider)
+ && model_matches_selector(model, id)
+ })
.cloned()
}
@@ -116,7 +127,7 @@ fn model_row(model: &Model, use_color: bool) -> Vec {
format_cost(model.costs.output_cost_per_mtok),
);
vec![
- model.id.clone().cell().bold(use_color),
+ model.id.as_str().cell().bold(use_color),
model
.provider
.as_str()
@@ -172,9 +183,20 @@ fn print_models_table(models: &[Model], styles: &Styles) {
}
fn configured_model_test_status(
+ expected: &Model,
result: Result,
) -> (Color, String, bool) {
match result {
+ Ok(resp) if resp.provider != expected.provider || resp.model_id != expected.id.as_str() => {
+ (
+ Color::Red,
+ format!(
+ "error: server tested unexpected offering {}/{}",
+ resp.provider, resp.model_id
+ ),
+ true,
+ )
+ }
Ok(resp) if resp.status == api_types::ModelTestResultStatus::Ok => {
(Color::Green, "ok".to_string(), false)
}
@@ -197,21 +219,21 @@ fn model_test_row_from_status(model: &Model, status: &str, result_color: Color)
let trimmed = status.trim();
match result_color {
Color::Green => ModelTestRow {
- model: model.id.clone(),
+ model: model.id.to_string(),
provider: model.provider.clone(),
result: ModelTestResultKind::Pass,
detail: None,
error: None,
},
Color::Yellow => ModelTestRow {
- model: model.id.clone(),
+ model: model.id.to_string(),
provider: model.provider.clone(),
result: ModelTestResultKind::Skip,
detail: Some(trimmed.to_string()),
error: None,
},
_ => ModelTestRow {
- model: model.id.clone(),
+ model: model.id.to_string(),
provider: model.provider.clone(),
result: ModelTestResultKind::Fail,
detail: None,
@@ -251,21 +273,45 @@ async fn test_models_via_server(
let mut skipped = 0u32;
let mut skipped_providers: Vec = Vec::new();
if let Some(model_id) = model {
- let listed_models = client.list_models(None, Some(model_id)).await?;
- let listed_info = find_model_by_id_or_alias(&listed_models, model_id);
+ let requested_provider = provider.map(ProviderId::new);
+ let listed_models = client.list_models(provider, Some(model_id)).await?;
+ let listed_info = find_model_by_id_or_alias(&listed_models, model_id, None);
if !json_output {
eprint!("Testing {model_id}...");
}
- let result = client.test_model(model_id, request_mode).await;
+ let has_configured_match = listed_models
+ .iter()
+ .any(|model| model.configured && model_matches_selector(model, model_id));
+ let result =
+ if requested_provider.is_none() && listed_info.is_some() && !has_configured_match {
+ None
+ } else {
+ Some(
+ client
+ .test_model(model_id, requested_provider.as_ref(), request_mode)
+ .await,
+ )
+ };
if !json_output {
eprintln!(" done");
}
let (info, result_color, status) = match result {
- Ok(resp) => {
- let info = find_model_by_id_or_alias(&listed_models, &resp.model_id).with_context(
- || format!("Unknown model returned by server: {}", resp.model_id),
- )?;
+ None => {
+ let info = listed_info.with_context(|| format!("Unknown model: {model_id}"))?;
+ failures += 1;
+ skipped += 1;
+ (info, Color::Yellow, "not configured".to_string())
+ }
+ Some(Ok(resp)) => {
+ let info =
+ find_model_by_id_or_alias(&listed_models, &resp.model_id, Some(&resp.provider))
+ .with_context(|| {
+ format!(
+ "Unknown model returned by server: {}/{}",
+ resp.provider, resp.model_id
+ )
+ })?;
if resp.status == api_types::ModelTestResultStatus::Ok {
(info, Color::Green, "ok".to_string())
} else if resp.status == api_types::ModelTestResultStatus::Skip {
@@ -280,10 +326,10 @@ async fn test_models_via_server(
(info, Color::Red, format!("error: {message}"))
}
}
- Err(err) if err.to_string().contains("Model not found") => {
+ Some(Err(err)) if err.to_string().contains("Model not found") => {
bail!("Unknown model: {model_id}");
}
- Err(err) => {
+ Some(Err(err)) => {
let info = listed_info.with_context(|| format!("Unknown model: {model_id}"))?;
failures += 1;
(info, Color::Red, format!("error: {err}"))
@@ -328,11 +374,14 @@ async fn test_models_via_server(
.map(|(index, info)| {
let client = client.clone();
async move {
- let result = client.test_model(&info.id, request_mode).await;
+ let result = client
+ .test_model(info.id.as_str(), Some(&info.provider), request_mode)
+ .await;
if !json_output {
eprintln!("Testing {}... done", info.id);
}
- let (result_color, status, failed) = configured_model_test_status(result);
+ let (result_color, status, failed) =
+ configured_model_test_status(&info, result);
(
CompletedModelTest {
index,
@@ -478,7 +527,7 @@ mod tests {
fn test_model_json(id: &str, provider: ProviderId) -> serde_json::Value {
serde_json::to_value(Model {
- id: id.to_string(),
+ id: id.into(),
provider,
family: "test".to_string(),
display_name: format!("{id} display"),
@@ -489,12 +538,13 @@ mod tests {
training: None,
knowledge_cutoff: None,
features: ModelFeatures {
- tools: true,
- vision: false,
- reasoning: false,
- reasoning_effort: ReasoningEffortFeature::None,
- prompt_cache: false,
- sampling_params: true,
+ tools: true,
+ vision: false,
+ reasoning: false,
+ reasoning_effort: ReasoningEffortFeature::None,
+ prompt_cache: false,
+ cache_control_breakpoints: false,
+ sampling_params: true,
},
costs: ModelCosts {
input_cost_per_mtok: Some(1.0),
@@ -512,7 +562,7 @@ mod tests {
fn custom_model_json(id: &str, provider: &str) -> serde_json::Value {
serde_json::to_value(Model {
- id: id.to_string(),
+ id: id.into(),
provider: ProviderId::new(provider),
family: "test".to_string(),
display_name: format!("{id} display"),
@@ -523,12 +573,13 @@ mod tests {
training: None,
knowledge_cutoff: None,
features: ModelFeatures {
- tools: true,
- vision: false,
- reasoning: false,
- reasoning_effort: ReasoningEffortFeature::None,
- prompt_cache: false,
- sampling_params: true,
+ tools: true,
+ vision: false,
+ reasoning: false,
+ reasoning_effort: ReasoningEffortFeature::None,
+ prompt_cache: false,
+ cache_control_breakpoints: false,
+ sampling_params: true,
},
costs: ModelCosts {
input_cost_per_mtok: Some(1.0),
@@ -610,6 +661,7 @@ mod tests {
.body(
serde_json::json!({
"model_id": "test-model",
+ "provider": "anthropic",
"status": "ok"
})
.to_string(),
@@ -618,7 +670,7 @@ mod tests {
.await;
let client = test_client(&server.url(""));
- let response = client.test_model("test-model", None).await.unwrap();
+ let response = client.test_model("test-model", None, None).await.unwrap();
assert_eq!(response.status, api_types::ModelTestResultStatus::Ok);
assert!(response.error_message.is_none());
@@ -637,6 +689,7 @@ mod tests {
.body(
serde_json::json!({
"model_id": "test-model",
+ "provider": "anthropic",
"status": "error",
"error_message": "timeout"
})
@@ -647,7 +700,7 @@ mod tests {
let client = test_client(&server.url(""));
let response = client
- .test_model("test-model", Some(ModelTestMode::Deep))
+ .test_model("test-model", None, Some(ModelTestMode::Deep))
.await
.unwrap();
@@ -666,6 +719,7 @@ mod tests {
.body(
serde_json::json!({
"model_id": "kimi-k2.5",
+ "provider": "kimi",
"status": "skip"
})
.to_string(),
@@ -674,7 +728,7 @@ mod tests {
.await;
let client = test_client(&server.url(""));
- let response = client.test_model("kimi-k2.5", None).await.unwrap();
+ let response = client.test_model("kimi-k2.5", None, None).await.unwrap();
assert_eq!(response.status, api_types::ModelTestResultStatus::Skip);
assert!(response.error_message.is_none());
@@ -698,7 +752,7 @@ mod tests {
.await;
let client = test_client(&server.url(""));
- let result = client.test_model("bad-model", None).await;
+ let result = client.test_model("bad-model", None, None).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Model not found"));
}
@@ -732,6 +786,7 @@ mod tests {
.body(
serde_json::json!({
"model_id": "venice-large",
+ "provider": "venice",
"status": "ok"
})
.to_string(),
@@ -754,6 +809,80 @@ mod tests {
.unwrap();
}
+ #[tokio::test]
+ async fn bulk_model_test_keeps_duplicate_ids_scoped_by_provider() {
+ let server = httpmock::MockServer::start_async().await;
+ server
+ .mock_async(|when, then| {
+ when.method("GET")
+ .path("/api/v1/models")
+ .query_param("page[limit]", "100")
+ .query_param("page[offset]", "0");
+ then.status(200)
+ .header("Content-Type", "application/json")
+ .body(
+ serde_json::json!({
+ "data": [
+ custom_model_json("portable-model", "openai"),
+ custom_model_json("portable-model", "openrouter")
+ ],
+ "meta": { "has_more": false }
+ })
+ .to_string(),
+ );
+ })
+ .await;
+ let openai = server
+ .mock_async(|when, then| {
+ when.method("POST")
+ .path("/api/v1/models/portable-model/test")
+ .query_param("provider", "openai");
+ then.status(200)
+ .header("Content-Type", "application/json")
+ .body(
+ serde_json::json!({
+ "model_id": "portable-model",
+ "provider": "openai",
+ "status": "ok"
+ })
+ .to_string(),
+ );
+ })
+ .await;
+ let openrouter = server
+ .mock_async(|when, then| {
+ when.method("POST")
+ .path("/api/v1/models/portable-model/test")
+ .query_param("provider", "openrouter");
+ then.status(200)
+ .header("Content-Type", "application/json")
+ .body(
+ serde_json::json!({
+ "model_id": "portable-model",
+ "provider": "openrouter",
+ "status": "ok"
+ })
+ .to_string(),
+ );
+ })
+ .await;
+
+ test_models_via_server(
+ &test_client(&server.url("")),
+ None,
+ None,
+ false,
+ 2,
+ &Styles::new(false),
+ true,
+ )
+ .await
+ .unwrap();
+
+ openai.assert_async().await;
+ openrouter.assert_async().await;
+ }
+
#[tokio::test]
async fn fetch_models_from_server_parses_response() {
let server = httpmock::MockServer::start_async().await;
diff --git a/lib/crates/fabro-cli/src/commands/parent/link.rs b/lib/apps/fabro-cli/src/commands/parent/link.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/parent/link.rs
rename to lib/apps/fabro-cli/src/commands/parent/link.rs
diff --git a/lib/crates/fabro-cli/src/commands/parent/mod.rs b/lib/apps/fabro-cli/src/commands/parent/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/parent/mod.rs
rename to lib/apps/fabro-cli/src/commands/parent/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/parent/unlink.rs b/lib/apps/fabro-cli/src/commands/parent/unlink.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/parent/unlink.rs
rename to lib/apps/fabro-cli/src/commands/parent/unlink.rs
diff --git a/lib/crates/fabro-cli/src/commands/parse.rs b/lib/apps/fabro-cli/src/commands/parse.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/parse.rs
rename to lib/apps/fabro-cli/src/commands/parse.rs
diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/apps/fabro-cli/src/commands/pr/close.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/pr/close.rs
rename to lib/apps/fabro-cli/src/commands/pr/close.rs
diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/apps/fabro-cli/src/commands/pr/create.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/pr/create.rs
rename to lib/apps/fabro-cli/src/commands/pr/create.rs
diff --git a/lib/crates/fabro-cli/src/commands/pr/link.rs b/lib/apps/fabro-cli/src/commands/pr/link.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/pr/link.rs
rename to lib/apps/fabro-cli/src/commands/pr/link.rs
diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/apps/fabro-cli/src/commands/pr/merge.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/pr/merge.rs
rename to lib/apps/fabro-cli/src/commands/pr/merge.rs
diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/apps/fabro-cli/src/commands/pr/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/pr/mod.rs
rename to lib/apps/fabro-cli/src/commands/pr/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/pr/unlink.rs b/lib/apps/fabro-cli/src/commands/pr/unlink.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/pr/unlink.rs
rename to lib/apps/fabro-cli/src/commands/pr/unlink.rs
diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/apps/fabro-cli/src/commands/pr/view.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/pr/view.rs
rename to lib/apps/fabro-cli/src/commands/pr/view.rs
diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/apps/fabro-cli/src/commands/preflight.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/preflight.rs
rename to lib/apps/fabro-cli/src/commands/preflight.rs
diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/apps/fabro-cli/src/commands/provider/login.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/provider/login.rs
rename to lib/apps/fabro-cli/src/commands/provider/login.rs
diff --git a/lib/crates/fabro-cli/src/commands/provider/mod.rs b/lib/apps/fabro-cli/src/commands/provider/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/provider/mod.rs
rename to lib/apps/fabro-cli/src/commands/provider/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/render_graph.rs b/lib/apps/fabro-cli/src/commands/render_graph.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/render_graph.rs
rename to lib/apps/fabro-cli/src/commands/render_graph.rs
diff --git a/lib/crates/fabro-cli/src/commands/repo/deinit.rs b/lib/apps/fabro-cli/src/commands/repo/deinit.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/repo/deinit.rs
rename to lib/apps/fabro-cli/src/commands/repo/deinit.rs
diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/apps/fabro-cli/src/commands/repo/init.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/repo/init.rs
rename to lib/apps/fabro-cli/src/commands/repo/init.rs
diff --git a/lib/crates/fabro-cli/src/commands/repo/mod.rs b/lib/apps/fabro-cli/src/commands/repo/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/repo/mod.rs
rename to lib/apps/fabro-cli/src/commands/repo/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/ask.rs b/lib/apps/fabro-cli/src/commands/run/ask.rs
similarity index 96%
rename from lib/crates/fabro-cli/src/commands/run/ask.rs
rename to lib/apps/fabro-cli/src/commands/run/ask.rs
index 1b38ad734..2c7819186 100644
--- a/lib/crates/fabro-cli/src/commands/run/ask.rs
+++ b/lib/apps/fabro-cli/src/commands/run/ask.rs
@@ -11,8 +11,9 @@ pub(crate) async fn run(args: AskArgs, base_ctx: &CommandContext) -> Result<()>
let run_id = client.resolve_run(&args.run).await?.id;
let session = client
.create_run_session(run_id, CreateRunSessionRequest {
- title: Some(session_title(&args.prompt)),
- model: args.model,
+ title: Some(session_title(&args.prompt)),
+ model: args.model,
+ provider: None,
})
.await?;
let mut stream = client
diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/apps/fabro-cli/src/commands/run/attach.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/attach.rs
rename to lib/apps/fabro-cli/src/commands/run/attach.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/checkpoints.rs b/lib/apps/fabro-cli/src/commands/run/checkpoints.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/checkpoints.rs
rename to lib/apps/fabro-cli/src/commands/run/checkpoints.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/apps/fabro-cli/src/commands/run/command.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/command.rs
rename to lib/apps/fabro-cli/src/commands/run/command.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/apps/fabro-cli/src/commands/run/cp.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/cp.rs
rename to lib/apps/fabro-cli/src/commands/run/cp.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/create.rs b/lib/apps/fabro-cli/src/commands/run/create.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/create.rs
rename to lib/apps/fabro-cli/src/commands/run/create.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/apps/fabro-cli/src/commands/run/diff.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/diff.rs
rename to lib/apps/fabro-cli/src/commands/run/diff.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/events.rs b/lib/apps/fabro-cli/src/commands/run/events.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/events.rs
rename to lib/apps/fabro-cli/src/commands/run/events.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/apps/fabro-cli/src/commands/run/fork.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/fork.rs
rename to lib/apps/fabro-cli/src/commands/run/fork.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/apps/fabro-cli/src/commands/run/logs.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/logs.rs
rename to lib/apps/fabro-cli/src/commands/run/logs.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/apps/fabro-cli/src/commands/run/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/mod.rs
rename to lib/apps/fabro-cli/src/commands/run/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/apps/fabro-cli/src/commands/run/output.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/output.rs
rename to lib/apps/fabro-cli/src/commands/run/output.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/overrides.rs b/lib/apps/fabro-cli/src/commands/run/overrides.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/overrides.rs
rename to lib/apps/fabro-cli/src/commands/run/overrides.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/apps/fabro-cli/src/commands/run/preview.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/preview.rs
rename to lib/apps/fabro-cli/src/commands/run/preview.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/apps/fabro-cli/src/commands/run/resume.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/resume.rs
rename to lib/apps/fabro-cli/src/commands/run/resume.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/apps/fabro-cli/src/commands/run/rewind.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/rewind.rs
rename to lib/apps/fabro-cli/src/commands/run/rewind.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/event.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/event.rs
similarity index 99%
rename from lib/crates/fabro-cli/src/commands/run/run_progress/event.rs
rename to lib/apps/fabro-cli/src/commands/run/run_progress/event.rs
index 6f8498411..91e8edaaf 100644
--- a/lib/crates/fabro-cli/src/commands/run/run_progress/event.rs
+++ b/lib/apps/fabro-cli/src/commands/run/run_progress/event.rs
@@ -323,7 +323,7 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option {
EventBody::ParallelCompleted(_) => Some(ProgressEvent::ParallelCompleted),
EventBody::AgentMessage(props) => Some(ProgressEvent::AssistantMessage {
stage_node_id: node_id,
- model: props.model.model_id.clone(),
+ model: props.model.model_id.to_string(),
}),
EventBody::AgentToolStarted(props) => Some(ProgressEvent::ToolCallStarted {
stage_node_id: node_id,
diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/info_display.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/info_display.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/run_progress/info_display.rs
rename to lib/apps/fabro-cli/src/commands/run/run_progress/info_display.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs
rename to lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/renderer.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs
rename to lib/apps/fabro-cli/src/commands/run/run_progress/renderer.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/setup_display.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/setup_display.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/run_progress/setup_display.rs
rename to lib/apps/fabro-cli/src/commands/run/run_progress/setup_display.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/stage_display.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs
rename to lib/apps/fabro-cli/src/commands/run/run_progress/stage_display.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/styles.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/styles.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/run_progress/styles.rs
rename to lib/apps/fabro-cli/src/commands/run/run_progress/styles.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/runner.rs b/lib/apps/fabro-cli/src/commands/run/runner.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/runner.rs
rename to lib/apps/fabro-cli/src/commands/run/runner.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/apps/fabro-cli/src/commands/run/ssh.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/ssh.rs
rename to lib/apps/fabro-cli/src/commands/run/ssh.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/start.rs b/lib/apps/fabro-cli/src/commands/run/start.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/start.rs
rename to lib/apps/fabro-cli/src/commands/run/start.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/steer.rs b/lib/apps/fabro-cli/src/commands/run/steer.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/steer.rs
rename to lib/apps/fabro-cli/src/commands/run/steer.rs
diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/apps/fabro-cli/src/commands/run/wait.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/run/wait.rs
rename to lib/apps/fabro-cli/src/commands/run/wait.rs
diff --git a/lib/crates/fabro-cli/src/commands/runs/approval.rs b/lib/apps/fabro-cli/src/commands/runs/approval.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/runs/approval.rs
rename to lib/apps/fabro-cli/src/commands/runs/approval.rs
diff --git a/lib/crates/fabro-cli/src/commands/runs/archive.rs b/lib/apps/fabro-cli/src/commands/runs/archive.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/runs/archive.rs
rename to lib/apps/fabro-cli/src/commands/runs/archive.rs
diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/apps/fabro-cli/src/commands/runs/inspect.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/runs/inspect.rs
rename to lib/apps/fabro-cli/src/commands/runs/inspect.rs
diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/apps/fabro-cli/src/commands/runs/list.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/runs/list.rs
rename to lib/apps/fabro-cli/src/commands/runs/list.rs
diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/apps/fabro-cli/src/commands/runs/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/runs/mod.rs
rename to lib/apps/fabro-cli/src/commands/runs/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/apps/fabro-cli/src/commands/runs/rm.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/runs/rm.rs
rename to lib/apps/fabro-cli/src/commands/runs/rm.rs
diff --git a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs b/lib/apps/fabro-cli/src/commands/sandbox/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/sandbox/mod.rs
rename to lib/apps/fabro-cli/src/commands/sandbox/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/secret/list.rs b/lib/apps/fabro-cli/src/commands/secret/list.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/secret/list.rs
rename to lib/apps/fabro-cli/src/commands/secret/list.rs
diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/apps/fabro-cli/src/commands/secret/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/secret/mod.rs
rename to lib/apps/fabro-cli/src/commands/secret/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/secret/rm.rs b/lib/apps/fabro-cli/src/commands/secret/rm.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/secret/rm.rs
rename to lib/apps/fabro-cli/src/commands/secret/rm.rs
diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/apps/fabro-cli/src/commands/secret/set.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/secret/set.rs
rename to lib/apps/fabro-cli/src/commands/secret/set.rs
diff --git a/lib/crates/fabro-cli/src/commands/server/foreground.rs b/lib/apps/fabro-cli/src/commands/server/foreground.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/server/foreground.rs
rename to lib/apps/fabro-cli/src/commands/server/foreground.rs
diff --git a/lib/crates/fabro-cli/src/commands/server/mod.rs b/lib/apps/fabro-cli/src/commands/server/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/server/mod.rs
rename to lib/apps/fabro-cli/src/commands/server/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/apps/fabro-cli/src/commands/server/start.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/server/start.rs
rename to lib/apps/fabro-cli/src/commands/server/start.rs
diff --git a/lib/crates/fabro-cli/src/commands/server/status.rs b/lib/apps/fabro-cli/src/commands/server/status.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/server/status.rs
rename to lib/apps/fabro-cli/src/commands/server/status.rs
diff --git a/lib/crates/fabro-cli/src/commands/server/stop.rs b/lib/apps/fabro-cli/src/commands/server/stop.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/server/stop.rs
rename to lib/apps/fabro-cli/src/commands/server/stop.rs
diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/apps/fabro-cli/src/commands/system/df.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/system/df.rs
rename to lib/apps/fabro-cli/src/commands/system/df.rs
diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/apps/fabro-cli/src/commands/system/events.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/system/events.rs
rename to lib/apps/fabro-cli/src/commands/system/events.rs
diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/apps/fabro-cli/src/commands/system/info.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/system/info.rs
rename to lib/apps/fabro-cli/src/commands/system/info.rs
diff --git a/lib/crates/fabro-cli/src/commands/system/mod.rs b/lib/apps/fabro-cli/src/commands/system/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/system/mod.rs
rename to lib/apps/fabro-cli/src/commands/system/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/apps/fabro-cli/src/commands/system/prune.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/system/prune.rs
rename to lib/apps/fabro-cli/src/commands/system/prune.rs
diff --git a/lib/crates/fabro-cli/src/commands/system/repair.rs b/lib/apps/fabro-cli/src/commands/system/repair.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/system/repair.rs
rename to lib/apps/fabro-cli/src/commands/system/repair.rs
diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/apps/fabro-cli/src/commands/uninstall.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/uninstall.rs
rename to lib/apps/fabro-cli/src/commands/uninstall.rs
diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/apps/fabro-cli/src/commands/upgrade.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/upgrade.rs
rename to lib/apps/fabro-cli/src/commands/upgrade.rs
diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/apps/fabro-cli/src/commands/validate.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/validate.rs
rename to lib/apps/fabro-cli/src/commands/validate.rs
diff --git a/lib/crates/fabro-cli/src/commands/variable/get.rs b/lib/apps/fabro-cli/src/commands/variable/get.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/variable/get.rs
rename to lib/apps/fabro-cli/src/commands/variable/get.rs
diff --git a/lib/crates/fabro-cli/src/commands/variable/list.rs b/lib/apps/fabro-cli/src/commands/variable/list.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/variable/list.rs
rename to lib/apps/fabro-cli/src/commands/variable/list.rs
diff --git a/lib/crates/fabro-cli/src/commands/variable/mod.rs b/lib/apps/fabro-cli/src/commands/variable/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/variable/mod.rs
rename to lib/apps/fabro-cli/src/commands/variable/mod.rs
diff --git a/lib/crates/fabro-cli/src/commands/variable/rm.rs b/lib/apps/fabro-cli/src/commands/variable/rm.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/variable/rm.rs
rename to lib/apps/fabro-cli/src/commands/variable/rm.rs
diff --git a/lib/crates/fabro-cli/src/commands/variable/set.rs b/lib/apps/fabro-cli/src/commands/variable/set.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/variable/set.rs
rename to lib/apps/fabro-cli/src/commands/variable/set.rs
diff --git a/lib/crates/fabro-cli/src/commands/version.rs b/lib/apps/fabro-cli/src/commands/version.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/version.rs
rename to lib/apps/fabro-cli/src/commands/version.rs
diff --git a/lib/crates/fabro-cli/src/commands/workflow/create.rs b/lib/apps/fabro-cli/src/commands/workflow/create.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/workflow/create.rs
rename to lib/apps/fabro-cli/src/commands/workflow/create.rs
diff --git a/lib/crates/fabro-cli/src/commands/workflow/list.rs b/lib/apps/fabro-cli/src/commands/workflow/list.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/workflow/list.rs
rename to lib/apps/fabro-cli/src/commands/workflow/list.rs
diff --git a/lib/crates/fabro-cli/src/commands/workflow/mod.rs b/lib/apps/fabro-cli/src/commands/workflow/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/commands/workflow/mod.rs
rename to lib/apps/fabro-cli/src/commands/workflow/mod.rs
diff --git a/lib/crates/fabro-cli/src/gh.rs b/lib/apps/fabro-cli/src/gh.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/gh.rs
rename to lib/apps/fabro-cli/src/gh.rs
diff --git a/lib/crates/fabro-cli/src/landing.rs b/lib/apps/fabro-cli/src/landing.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/landing.rs
rename to lib/apps/fabro-cli/src/landing.rs
diff --git a/lib/crates/fabro-cli/src/local_server.rs b/lib/apps/fabro-cli/src/local_server.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/local_server.rs
rename to lib/apps/fabro-cli/src/local_server.rs
diff --git a/lib/crates/fabro-cli/src/logging.rs b/lib/apps/fabro-cli/src/logging.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/logging.rs
rename to lib/apps/fabro-cli/src/logging.rs
diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/apps/fabro-cli/src/main.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/main.rs
rename to lib/apps/fabro-cli/src/main.rs
diff --git a/lib/crates/fabro-cli/src/manifest_args.rs b/lib/apps/fabro-cli/src/manifest_args.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/manifest_args.rs
rename to lib/apps/fabro-cli/src/manifest_args.rs
diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/apps/fabro-cli/src/server_client.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/server_client.rs
rename to lib/apps/fabro-cli/src/server_client.rs
diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/apps/fabro-cli/src/server_runs.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/server_runs.rs
rename to lib/apps/fabro-cli/src/server_runs.rs
diff --git a/lib/crates/fabro-cli/src/shared/github.rs b/lib/apps/fabro-cli/src/shared/github.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/shared/github.rs
rename to lib/apps/fabro-cli/src/shared/github.rs
diff --git a/lib/crates/fabro-cli/src/shared/mod.rs b/lib/apps/fabro-cli/src/shared/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/shared/mod.rs
rename to lib/apps/fabro-cli/src/shared/mod.rs
diff --git a/lib/crates/fabro-cli/src/shared/provider_auth.rs b/lib/apps/fabro-cli/src/shared/provider_auth.rs
similarity index 98%
rename from lib/crates/fabro-cli/src/shared/provider_auth.rs
rename to lib/apps/fabro-cli/src/shared/provider_auth.rs
index 70e258393..1d3992f1f 100644
--- a/lib/crates/fabro-cli/src/shared/provider_auth.rs
+++ b/lib/apps/fabro-cli/src/shared/provider_auth.rs
@@ -100,9 +100,10 @@ pub(crate) async fn validate_api_key(
.await
.context("failed to create LLM client")?;
- let probe_model = catalog
- .probe_for_provider(provider)
- .map_or_else(|| format!("unknown-{provider}"), |model| model.id.clone());
+ let probe_model = catalog.probe_for_provider(provider).map_or_else(
+ || format!("unknown-{provider}"),
+ |model| model.id.to_string(),
+ );
let params = GenerateParams::new(probe_model, Arc::new(client))
.provider(provider.to_string())
diff --git a/lib/crates/fabro-cli/src/shared/repo.rs b/lib/apps/fabro-cli/src/shared/repo.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/shared/repo.rs
rename to lib/apps/fabro-cli/src/shared/repo.rs
diff --git a/lib/crates/fabro-cli/src/shared/utilities.rs b/lib/apps/fabro-cli/src/shared/utilities.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/shared/utilities.rs
rename to lib/apps/fabro-cli/src/shared/utilities.rs
diff --git a/lib/crates/fabro-cli/src/sleep_inhibitor/dummy.rs b/lib/apps/fabro-cli/src/sleep_inhibitor/dummy.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/sleep_inhibitor/dummy.rs
rename to lib/apps/fabro-cli/src/sleep_inhibitor/dummy.rs
diff --git a/lib/crates/fabro-cli/src/sleep_inhibitor/iokit_bindings.rs b/lib/apps/fabro-cli/src/sleep_inhibitor/iokit_bindings.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/sleep_inhibitor/iokit_bindings.rs
rename to lib/apps/fabro-cli/src/sleep_inhibitor/iokit_bindings.rs
diff --git a/lib/crates/fabro-cli/src/sleep_inhibitor/linux.rs b/lib/apps/fabro-cli/src/sleep_inhibitor/linux.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/sleep_inhibitor/linux.rs
rename to lib/apps/fabro-cli/src/sleep_inhibitor/linux.rs
diff --git a/lib/crates/fabro-cli/src/sleep_inhibitor/macos.rs b/lib/apps/fabro-cli/src/sleep_inhibitor/macos.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/sleep_inhibitor/macos.rs
rename to lib/apps/fabro-cli/src/sleep_inhibitor/macos.rs
diff --git a/lib/crates/fabro-cli/src/sleep_inhibitor/mod.rs b/lib/apps/fabro-cli/src/sleep_inhibitor/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/sleep_inhibitor/mod.rs
rename to lib/apps/fabro-cli/src/sleep_inhibitor/mod.rs
diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/apps/fabro-cli/src/user_config.rs
similarity index 100%
rename from lib/crates/fabro-cli/src/user_config.rs
rename to lib/apps/fabro-cli/src/user_config.rs
diff --git a/lib/crates/fabro-cli/tests/build_git_metadata.rs b/lib/apps/fabro-cli/tests/build_git_metadata.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/build_git_metadata.rs
rename to lib/apps/fabro-cli/tests/build_git_metadata.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/approve.rs b/lib/apps/fabro-cli/tests/it/cmd/approve.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/approve.rs
rename to lib/apps/fabro-cli/tests/it/cmd/approve.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/archive.rs b/lib/apps/fabro-cli/tests/it/cmd/archive.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/archive.rs
rename to lib/apps/fabro-cli/tests/it/cmd/archive.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs b/lib/apps/fabro-cli/tests/it/cmd/artifact_cp.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs
rename to lib/apps/fabro-cli/tests/it/cmd/artifact_cp.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs b/lib/apps/fabro-cli/tests/it/cmd/artifact_list.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs
rename to lib/apps/fabro-cli/tests/it/cmd/artifact_list.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/apps/fabro-cli/tests/it/cmd/attach.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/attach.rs
rename to lib/apps/fabro-cli/tests/it/cmd/attach.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/auth.rs b/lib/apps/fabro-cli/tests/it/cmd/auth.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/auth.rs
rename to lib/apps/fabro-cli/tests/it/cmd/auth.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/cli_reference.rs b/lib/apps/fabro-cli/tests/it/cmd/cli_reference.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/cli_reference.rs
rename to lib/apps/fabro-cli/tests/it/cmd/cli_reference.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/apps/fabro-cli/tests/it/cmd/config.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/config.rs
rename to lib/apps/fabro-cli/tests/it/cmd/config.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/create.rs b/lib/apps/fabro-cli/tests/it/cmd/create.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/create.rs
rename to lib/apps/fabro-cli/tests/it/cmd/create.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/deny.rs b/lib/apps/fabro-cli/tests/it/cmd/deny.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/deny.rs
rename to lib/apps/fabro-cli/tests/it/cmd/deny.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/diff.rs b/lib/apps/fabro-cli/tests/it/cmd/diff.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/diff.rs
rename to lib/apps/fabro-cli/tests/it/cmd/diff.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/discord.rs b/lib/apps/fabro-cli/tests/it/cmd/discord.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/discord.rs
rename to lib/apps/fabro-cli/tests/it/cmd/discord.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/docs.rs b/lib/apps/fabro-cli/tests/it/cmd/docs.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/docs.rs
rename to lib/apps/fabro-cli/tests/it/cmd/docs.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/doctor.rs b/lib/apps/fabro-cli/tests/it/cmd/doctor.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/doctor.rs
rename to lib/apps/fabro-cli/tests/it/cmd/doctor.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/dump.rs b/lib/apps/fabro-cli/tests/it/cmd/dump.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/dump.rs
rename to lib/apps/fabro-cli/tests/it/cmd/dump.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/events.rs b/lib/apps/fabro-cli/tests/it/cmd/events.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/events.rs
rename to lib/apps/fabro-cli/tests/it/cmd/events.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/exec.rs b/lib/apps/fabro-cli/tests/it/cmd/exec.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/exec.rs
rename to lib/apps/fabro-cli/tests/it/cmd/exec.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs b/lib/apps/fabro-cli/tests/it/cmd/fabro.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/fabro.rs
rename to lib/apps/fabro-cli/tests/it/cmd/fabro.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/fork.rs b/lib/apps/fabro-cli/tests/it/cmd/fork.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/fork.rs
rename to lib/apps/fabro-cli/tests/it/cmd/fork.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/graph.rs b/lib/apps/fabro-cli/tests/it/cmd/graph.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/graph.rs
rename to lib/apps/fabro-cli/tests/it/cmd/graph.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/apps/fabro-cli/tests/it/cmd/inspect.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/inspect.rs
rename to lib/apps/fabro-cli/tests/it/cmd/inspect.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/install.rs b/lib/apps/fabro-cli/tests/it/cmd/install.rs
similarity index 95%
rename from lib/crates/fabro-cli/tests/it/cmd/install.rs
rename to lib/apps/fabro-cli/tests/it/cmd/install.rs
index fe2c9cf61..e0eb0b75a 100644
--- a/lib/crates/fabro-cli/tests/it/cmd/install.rs
+++ b/lib/apps/fabro-cli/tests/it/cmd/install.rs
@@ -338,10 +338,16 @@ fn keep_existing_settings_persists_secrets_without_rewriting_server_target() {
let mut context = test_context!();
let storage_dir = context.temp_dir.join("install-storage");
context.manage_storage_dir(&storage_dir);
- let existing_web_url = unused_loopback_web_url();
- let requested_web_url = unused_loopback_web_url();
- write_http_install_settings(&context, &storage_dir, &existing_web_url, "keep-me");
- login_with_storage_dev_token(&context, &storage_dir, &existing_web_url);
+ let socket_path = context.temp_dir.join("install-storage.sock");
+ let existing_web_url = "https://existing.example.test".to_string();
+ let requested_web_url = "https://requested.example.test".to_string();
+ write_unix_install_settings(
+ &context,
+ &storage_dir,
+ &socket_path,
+ &existing_web_url,
+ "keep-me",
+ );
let path = fake_gh_path(&context, "ghp_keep_existing");
let output = context
@@ -382,15 +388,19 @@ fn keep_existing_settings_persists_secrets_without_rewriting_server_target() {
.and_then(toml::Value::as_str),
Some(existing_web_url.as_str())
);
+ let target = parsed
+ .get("cli")
+ .and_then(toml::Value::as_table)
+ .and_then(|cli| cli.get("target"))
+ .and_then(toml::Value::as_table)
+ .expect("cli.target should remain configured");
assert_eq!(
- parsed
- .get("cli")
- .and_then(toml::Value::as_table)
- .and_then(|cli| cli.get("target"))
- .and_then(toml::Value::as_table)
- .and_then(|target| target.get("url"))
- .and_then(toml::Value::as_str),
- Some(existing_web_url.as_str())
+ target.get("type").and_then(toml::Value::as_str),
+ Some("unix")
+ );
+ assert_eq!(
+ target.get("path").and_then(toml::Value::as_str),
+ socket_path.to_str()
);
assert!(
!settings.contains(&requested_web_url),
@@ -874,6 +884,53 @@ mode = "{}"
);
}
+fn write_unix_install_settings(
+ context: &fabro_test::TestContext,
+ storage_dir: &std::path::Path,
+ socket_path: &std::path::Path,
+ web_url: &str,
+ metadata_mode: &str,
+) {
+ write_raw_home_settings(
+ context,
+ &format!(
+ r#"
+_version = 1
+
+[server.storage]
+root = "{}"
+
+[server.api]
+url = "{}/api/v1"
+
+[server.web]
+enabled = true
+url = "{}"
+
+[server.auth]
+methods = ["dev-token"]
+
+[server.listen]
+type = "unix"
+path = "{}"
+
+[cli.target]
+type = "unix"
+path = "{}"
+
+[project.metadata]
+mode = "{}"
+"#,
+ storage_dir.display(),
+ web_url,
+ web_url,
+ socket_path.display(),
+ socket_path.display(),
+ metadata_mode
+ ),
+ );
+}
+
fn login_with_storage_dev_token(
context: &fabro_test::TestContext,
storage_dir: &std::path::Path,
diff --git a/lib/crates/fabro-cli/tests/it/cmd/json_global.rs b/lib/apps/fabro-cli/tests/it/cmd/json_global.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/json_global.rs
rename to lib/apps/fabro-cli/tests/it/cmd/json_global.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/logs.rs b/lib/apps/fabro-cli/tests/it/cmd/logs.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/logs.rs
rename to lib/apps/fabro-cli/tests/it/cmd/logs.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/mcp.rs b/lib/apps/fabro-cli/tests/it/cmd/mcp.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/mcp.rs
rename to lib/apps/fabro-cli/tests/it/cmd/mcp.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/mod.rs b/lib/apps/fabro-cli/tests/it/cmd/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/mod.rs
rename to lib/apps/fabro-cli/tests/it/cmd/mod.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/model.rs b/lib/apps/fabro-cli/tests/it/cmd/model.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/model.rs
rename to lib/apps/fabro-cli/tests/it/cmd/model.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/model_list.rs b/lib/apps/fabro-cli/tests/it/cmd/model_list.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/model_list.rs
rename to lib/apps/fabro-cli/tests/it/cmd/model_list.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/model_test.rs b/lib/apps/fabro-cli/tests/it/cmd/model_test.rs
similarity index 99%
rename from lib/crates/fabro-cli/tests/it/cmd/model_test.rs
rename to lib/apps/fabro-cli/tests/it/cmd/model_test.rs
index f7d731d2d..70e64ac77 100644
--- a/lib/crates/fabro-cli/tests/it/cmd/model_test.rs
+++ b/lib/apps/fabro-cli/tests/it/cmd/model_test.rs
@@ -202,6 +202,7 @@ fn model_test_does_not_announce_unconfigured() {
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"model_id": "claude-opus-4-7",
+ "provider": "anthropic",
"status": "ok"
}));
});
@@ -251,6 +252,7 @@ fn model_test_skipped_footer_sources_from_listing() {
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"model_id": "claude-opus-4-7",
+ "provider": "anthropic",
"status": "ok"
}));
});
@@ -282,6 +284,7 @@ fn model_test_post_list_race_is_a_failure() {
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"model_id": "claude-opus-4-7",
+ "provider": "anthropic",
"status": "skip"
}));
});
@@ -323,6 +326,7 @@ fn model_test_json_partitions_skip_and_fail() {
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"model_id": "claude-opus-4-7",
+ "provider": "anthropic",
"status": "skip"
}));
});
@@ -529,6 +533,7 @@ async fn concurrent_test_model(
Json(serde_json::json!({
"model_id": id,
+ "provider": "anthropic",
"status": "ok"
}))
}
diff --git a/lib/crates/fabro-cli/tests/it/cmd/parent.rs b/lib/apps/fabro-cli/tests/it/cmd/parent.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/parent.rs
rename to lib/apps/fabro-cli/tests/it/cmd/parent.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/parse.rs b/lib/apps/fabro-cli/tests/it/cmd/parse.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/parse.rs
rename to lib/apps/fabro-cli/tests/it/cmd/parse.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr.rs b/lib/apps/fabro-cli/tests/it/cmd/pr.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/pr.rs
rename to lib/apps/fabro-cli/tests/it/cmd/pr.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_close.rs b/lib/apps/fabro-cli/tests/it/cmd/pr_close.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/pr_close.rs
rename to lib/apps/fabro-cli/tests/it/cmd/pr_close.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_create.rs b/lib/apps/fabro-cli/tests/it/cmd/pr_create.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/pr_create.rs
rename to lib/apps/fabro-cli/tests/it/cmd/pr_create.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_link.rs b/lib/apps/fabro-cli/tests/it/cmd/pr_link.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/pr_link.rs
rename to lib/apps/fabro-cli/tests/it/cmd/pr_link.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_merge.rs b/lib/apps/fabro-cli/tests/it/cmd/pr_merge.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/pr_merge.rs
rename to lib/apps/fabro-cli/tests/it/cmd/pr_merge.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_unlink.rs b/lib/apps/fabro-cli/tests/it/cmd/pr_unlink.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/pr_unlink.rs
rename to lib/apps/fabro-cli/tests/it/cmd/pr_unlink.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs b/lib/apps/fabro-cli/tests/it/cmd/pr_view.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/pr_view.rs
rename to lib/apps/fabro-cli/tests/it/cmd/pr_view.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/preflight.rs b/lib/apps/fabro-cli/tests/it/cmd/preflight.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/preflight.rs
rename to lib/apps/fabro-cli/tests/it/cmd/preflight.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/provider.rs b/lib/apps/fabro-cli/tests/it/cmd/provider.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/provider.rs
rename to lib/apps/fabro-cli/tests/it/cmd/provider.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/provider_login.rs b/lib/apps/fabro-cli/tests/it/cmd/provider_login.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/provider_login.rs
rename to lib/apps/fabro-cli/tests/it/cmd/provider_login.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/ps.rs b/lib/apps/fabro-cli/tests/it/cmd/ps.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/ps.rs
rename to lib/apps/fabro-cli/tests/it/cmd/ps.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/render_graph.rs b/lib/apps/fabro-cli/tests/it/cmd/render_graph.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/render_graph.rs
rename to lib/apps/fabro-cli/tests/it/cmd/render_graph.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/repo.rs b/lib/apps/fabro-cli/tests/it/cmd/repo.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/repo.rs
rename to lib/apps/fabro-cli/tests/it/cmd/repo.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/repo_deinit.rs b/lib/apps/fabro-cli/tests/it/cmd/repo_deinit.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/repo_deinit.rs
rename to lib/apps/fabro-cli/tests/it/cmd/repo_deinit.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/repo_init.rs b/lib/apps/fabro-cli/tests/it/cmd/repo_init.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/repo_init.rs
rename to lib/apps/fabro-cli/tests/it/cmd/repo_init.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/resume.rs b/lib/apps/fabro-cli/tests/it/cmd/resume.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/resume.rs
rename to lib/apps/fabro-cli/tests/it/cmd/resume.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs b/lib/apps/fabro-cli/tests/it/cmd/rewind.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/rewind.rs
rename to lib/apps/fabro-cli/tests/it/cmd/rewind.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/rm.rs b/lib/apps/fabro-cli/tests/it/cmd/rm.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/rm.rs
rename to lib/apps/fabro-cli/tests/it/cmd/rm.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/apps/fabro-cli/tests/it/cmd/run.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/run.rs
rename to lib/apps/fabro-cli/tests/it/cmd/run.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/runner.rs b/lib/apps/fabro-cli/tests/it/cmd/runner.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/runner.rs
rename to lib/apps/fabro-cli/tests/it/cmd/runner.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/sandbox_cp.rs b/lib/apps/fabro-cli/tests/it/cmd/sandbox_cp.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/sandbox_cp.rs
rename to lib/apps/fabro-cli/tests/it/cmd/sandbox_cp.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/sandbox_preview.rs b/lib/apps/fabro-cli/tests/it/cmd/sandbox_preview.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/sandbox_preview.rs
rename to lib/apps/fabro-cli/tests/it/cmd/sandbox_preview.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/sandbox_ssh.rs b/lib/apps/fabro-cli/tests/it/cmd/sandbox_ssh.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/sandbox_ssh.rs
rename to lib/apps/fabro-cli/tests/it/cmd/sandbox_ssh.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/secret.rs b/lib/apps/fabro-cli/tests/it/cmd/secret.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/secret.rs
rename to lib/apps/fabro-cli/tests/it/cmd/secret.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/secret_list.rs b/lib/apps/fabro-cli/tests/it/cmd/secret_list.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/secret_list.rs
rename to lib/apps/fabro-cli/tests/it/cmd/secret_list.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/secret_rm.rs b/lib/apps/fabro-cli/tests/it/cmd/secret_rm.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/secret_rm.rs
rename to lib/apps/fabro-cli/tests/it/cmd/secret_rm.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/secret_set.rs b/lib/apps/fabro-cli/tests/it/cmd/secret_set.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/secret_set.rs
rename to lib/apps/fabro-cli/tests/it/cmd/secret_set.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/send_analytics.rs b/lib/apps/fabro-cli/tests/it/cmd/send_analytics.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/send_analytics.rs
rename to lib/apps/fabro-cli/tests/it/cmd/send_analytics.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/send_panic.rs b/lib/apps/fabro-cli/tests/it/cmd/send_panic.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/send_panic.rs
rename to lib/apps/fabro-cli/tests/it/cmd/send_panic.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs b/lib/apps/fabro-cli/tests/it/cmd/server_start.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/server_start.rs
rename to lib/apps/fabro-cli/tests/it/cmd/server_start.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_status.rs b/lib/apps/fabro-cli/tests/it/cmd/server_status.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/server_status.rs
rename to lib/apps/fabro-cli/tests/it/cmd/server_status.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_stop.rs b/lib/apps/fabro-cli/tests/it/cmd/server_stop.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/server_stop.rs
rename to lib/apps/fabro-cli/tests/it/cmd/server_stop.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/start.rs b/lib/apps/fabro-cli/tests/it/cmd/start.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/start.rs
rename to lib/apps/fabro-cli/tests/it/cmd/start.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/apps/fabro-cli/tests/it/cmd/support.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/support.rs
rename to lib/apps/fabro-cli/tests/it/cmd/support.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/system.rs b/lib/apps/fabro-cli/tests/it/cmd/system.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/system.rs
rename to lib/apps/fabro-cli/tests/it/cmd/system.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/system_df.rs b/lib/apps/fabro-cli/tests/it/cmd/system_df.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/system_df.rs
rename to lib/apps/fabro-cli/tests/it/cmd/system_df.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/system_events.rs b/lib/apps/fabro-cli/tests/it/cmd/system_events.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/system_events.rs
rename to lib/apps/fabro-cli/tests/it/cmd/system_events.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/system_info.rs b/lib/apps/fabro-cli/tests/it/cmd/system_info.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/system_info.rs
rename to lib/apps/fabro-cli/tests/it/cmd/system_info.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/system_prune.rs b/lib/apps/fabro-cli/tests/it/cmd/system_prune.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/system_prune.rs
rename to lib/apps/fabro-cli/tests/it/cmd/system_prune.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/system_repair.rs b/lib/apps/fabro-cli/tests/it/cmd/system_repair.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/system_repair.rs
rename to lib/apps/fabro-cli/tests/it/cmd/system_repair.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/test_panic.rs b/lib/apps/fabro-cli/tests/it/cmd/test_panic.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/test_panic.rs
rename to lib/apps/fabro-cli/tests/it/cmd/test_panic.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/top_level.rs b/lib/apps/fabro-cli/tests/it/cmd/top_level.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/top_level.rs
rename to lib/apps/fabro-cli/tests/it/cmd/top_level.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/unarchive.rs b/lib/apps/fabro-cli/tests/it/cmd/unarchive.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/unarchive.rs
rename to lib/apps/fabro-cli/tests/it/cmd/unarchive.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/uninstall.rs b/lib/apps/fabro-cli/tests/it/cmd/uninstall.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/uninstall.rs
rename to lib/apps/fabro-cli/tests/it/cmd/uninstall.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/upgrade.rs b/lib/apps/fabro-cli/tests/it/cmd/upgrade.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/upgrade.rs
rename to lib/apps/fabro-cli/tests/it/cmd/upgrade.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/validate.rs b/lib/apps/fabro-cli/tests/it/cmd/validate.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/validate.rs
rename to lib/apps/fabro-cli/tests/it/cmd/validate.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/variable.rs b/lib/apps/fabro-cli/tests/it/cmd/variable.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/variable.rs
rename to lib/apps/fabro-cli/tests/it/cmd/variable.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/variable_get.rs b/lib/apps/fabro-cli/tests/it/cmd/variable_get.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/variable_get.rs
rename to lib/apps/fabro-cli/tests/it/cmd/variable_get.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/variable_list.rs b/lib/apps/fabro-cli/tests/it/cmd/variable_list.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/variable_list.rs
rename to lib/apps/fabro-cli/tests/it/cmd/variable_list.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/variable_rm.rs b/lib/apps/fabro-cli/tests/it/cmd/variable_rm.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/variable_rm.rs
rename to lib/apps/fabro-cli/tests/it/cmd/variable_rm.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/variable_set.rs b/lib/apps/fabro-cli/tests/it/cmd/variable_set.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/variable_set.rs
rename to lib/apps/fabro-cli/tests/it/cmd/variable_set.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/version.rs b/lib/apps/fabro-cli/tests/it/cmd/version.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/version.rs
rename to lib/apps/fabro-cli/tests/it/cmd/version.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/wait.rs b/lib/apps/fabro-cli/tests/it/cmd/wait.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/wait.rs
rename to lib/apps/fabro-cli/tests/it/cmd/wait.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs b/lib/apps/fabro-cli/tests/it/cmd/worker_auth.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs
rename to lib/apps/fabro-cli/tests/it/cmd/worker_auth.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow.rs b/lib/apps/fabro-cli/tests/it/cmd/workflow.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/workflow.rs
rename to lib/apps/fabro-cli/tests/it/cmd/workflow.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs b/lib/apps/fabro-cli/tests/it/cmd/workflow_create.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs
rename to lib/apps/fabro-cli/tests/it/cmd/workflow_create.rs
diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow_list.rs b/lib/apps/fabro-cli/tests/it/cmd/workflow_list.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/cmd/workflow_list.rs
rename to lib/apps/fabro-cli/tests/it/cmd/workflow_list.rs
diff --git a/lib/crates/fabro-cli/tests/it/main.rs b/lib/apps/fabro-cli/tests/it/main.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/main.rs
rename to lib/apps/fabro-cli/tests/it/main.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/archive.rs b/lib/apps/fabro-cli/tests/it/scenario/archive.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/archive.rs
rename to lib/apps/fabro-cli/tests/it/scenario/archive.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/artifacts.rs b/lib/apps/fabro-cli/tests/it/scenario/artifacts.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/artifacts.rs
rename to lib/apps/fabro-cli/tests/it/scenario/artifacts.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/auth.rs b/lib/apps/fabro-cli/tests/it/scenario/auth.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/auth.rs
rename to lib/apps/fabro-cli/tests/it/scenario/auth.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/exec.rs b/lib/apps/fabro-cli/tests/it/scenario/exec.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/exec.rs
rename to lib/apps/fabro-cli/tests/it/scenario/exec.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/apps/fabro-cli/tests/it/scenario/lifecycle.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs
rename to lib/apps/fabro-cli/tests/it/scenario/lifecycle.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/mod.rs b/lib/apps/fabro-cli/tests/it/scenario/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/mod.rs
rename to lib/apps/fabro-cli/tests/it/scenario/mod.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/server_lifecycle.rs b/lib/apps/fabro-cli/tests/it/scenario/server_lifecycle.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/server_lifecycle.rs
rename to lib/apps/fabro-cli/tests/it/scenario/server_lifecycle.rs
diff --git a/lib/crates/fabro-cli/tests/it/scenario/smoke.rs b/lib/apps/fabro-cli/tests/it/scenario/smoke.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/scenario/smoke.rs
rename to lib/apps/fabro-cli/tests/it/scenario/smoke.rs
diff --git a/lib/crates/fabro-cli/tests/it/support/auth_harness.rs b/lib/apps/fabro-cli/tests/it/support/auth_harness.rs
similarity index 98%
rename from lib/crates/fabro-cli/tests/it/support/auth_harness.rs
rename to lib/apps/fabro-cli/tests/it/support/auth_harness.rs
index cfaf06fd0..315465096 100644
--- a/lib/crates/fabro-cli/tests/it/support/auth_harness.rs
+++ b/lib/apps/fabro-cli/tests/it/support/auth_harness.rs
@@ -24,6 +24,7 @@ use fabro_server::auth::GithubEndpoints;
use fabro_server::jwt_auth::resolve_auth_mode_with_lookup;
use fabro_server::server::{RouterOptions, build_router_with_options};
use fabro_server::test_support::TestAppStateBuilder;
+use fabro_static::EnvVars;
use fabro_test::{GitHubAppState, TestContext, apply_test_isolation};
use serde_json::Value;
use tokio::net::TcpListener;
@@ -88,7 +89,10 @@ impl RealAuthHarness {
.max_concurrent_runs(5)
.env_lookup(|_| None)
.server_secret_env(secrets)
- .vault_entries([("GITHUB_APP_CLIENT_SECRET", github_client_secret.as_str())])
+ .vault_entries([
+ ("GITHUB_APP_CLIENT_SECRET", github_client_secret.as_str()),
+ (EnvVars::OPENAI_API_KEY, "test-openai-api-key"),
+ ])
.build();
let github_base = github_base_url(&twin.base_url);
let router = build_router_with_options(state, &auth_mode, RouterOptions {
diff --git a/lib/crates/fabro-cli/tests/it/support/auth_tokens.rs b/lib/apps/fabro-cli/tests/it/support/auth_tokens.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/support/auth_tokens.rs
rename to lib/apps/fabro-cli/tests/it/support/auth_tokens.rs
diff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/apps/fabro-cli/tests/it/support/mod.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/support/mod.rs
rename to lib/apps/fabro-cli/tests/it/support/mod.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/acp.rs b/lib/apps/fabro-cli/tests/it/workflow/acp.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/acp.rs
rename to lib/apps/fabro-cli/tests/it/workflow/acp.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs b/lib/apps/fabro-cli/tests/it/workflow/agent_linear.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs
rename to lib/apps/fabro-cli/tests/it/workflow/agent_linear.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs b/lib/apps/fabro-cli/tests/it/workflow/artifacts.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/artifacts.rs
rename to lib/apps/fabro-cli/tests/it/workflow/artifacts.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs b/lib/apps/fabro-cli/tests/it/workflow/command_agent_mixed.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs
rename to lib/apps/fabro-cli/tests/it/workflow/command_agent_mixed.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs b/lib/apps/fabro-cli/tests/it/workflow/command_pipeline.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs
rename to lib/apps/fabro-cli/tests/it/workflow/command_pipeline.rs
diff --git a/lib/apps/fabro-cli/tests/it/workflow/command_routing.rs b/lib/apps/fabro-cli/tests/it/workflow/command_routing.rs
new file mode 100644
index 000000000..1052ae476
--- /dev/null
+++ b/lib/apps/fabro-cli/tests/it/workflow/command_routing.rs
@@ -0,0 +1,34 @@
+use fabro_test::test_context;
+
+use super::{completed_nodes, find_run_dir, fixture, read_conclusion, sandbox_tests, timeout_for};
+
+sandbox_tests!(command_routing);
+
+fn scenario_command_routing(sandbox: &str) {
+ let context = test_context!();
+ let workflow = fixture("command_routing.fabro");
+
+ context.validate().arg(&workflow).assert().success();
+
+ context
+ .run_cmd()
+ .args(["--auto-approve", "--environment", sandbox])
+ .arg(workflow)
+ .timeout(timeout_for(sandbox))
+ .assert()
+ .success();
+
+ let run_dir = find_run_dir(&context);
+ let conclusion = read_conclusion(&run_dir);
+ assert_eq!(conclusion["status"].as_str(), Some("succeeded"));
+
+ let nodes = completed_nodes(&run_dir);
+ assert!(
+ nodes.contains(&"kept".to_string()),
+ "kept node should be in completed_nodes: {nodes:?}"
+ );
+ assert!(
+ !nodes.contains(&"none".to_string()),
+ "none node should NOT be in completed_nodes: {nodes:?}"
+ );
+}
diff --git a/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs b/lib/apps/fabro-cli/tests/it/workflow/conditional_branching.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs
rename to lib/apps/fabro-cli/tests/it/workflow/conditional_branching.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs b/lib/apps/fabro-cli/tests/it/workflow/dry_run_examples.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs
rename to lib/apps/fabro-cli/tests/it/workflow/dry_run_examples.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/fixtures/agent_linear.fabro b/lib/apps/fabro-cli/tests/it/workflow/fixtures/agent_linear.fabro
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/fixtures/agent_linear.fabro
rename to lib/apps/fabro-cli/tests/it/workflow/fixtures/agent_linear.fabro
diff --git a/lib/crates/fabro-cli/tests/it/workflow/fixtures/command_agent_mixed.fabro b/lib/apps/fabro-cli/tests/it/workflow/fixtures/command_agent_mixed.fabro
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/fixtures/command_agent_mixed.fabro
rename to lib/apps/fabro-cli/tests/it/workflow/fixtures/command_agent_mixed.fabro
diff --git a/lib/crates/fabro-cli/tests/it/workflow/fixtures/command_pipeline.fabro b/lib/apps/fabro-cli/tests/it/workflow/fixtures/command_pipeline.fabro
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/fixtures/command_pipeline.fabro
rename to lib/apps/fabro-cli/tests/it/workflow/fixtures/command_pipeline.fabro
diff --git a/lib/apps/fabro-cli/tests/it/workflow/fixtures/command_routing.fabro b/lib/apps/fabro-cli/tests/it/workflow/fixtures/command_routing.fabro
new file mode 100644
index 000000000..0c47eccff
--- /dev/null
+++ b/lib/apps/fabro-cli/tests/it/workflow/fixtures/command_routing.fabro
@@ -0,0 +1,23 @@
+digraph CommandRouting {
+ graph [goal="Route on structured output from a command"]
+ rankdir=LR
+
+ start [shape=Mdiamond, label="Start"]
+ exit [shape=Msquare, label="Exit"]
+
+ classify [
+ label="Classify",
+ shape=parallelogram,
+ script="echo '{\"context_updates\":{\"kept_count\":2}}'",
+ output_schema="routing"
+ ]
+ gate [label="Check Count", shape=diamond]
+ kept [label="Kept", shape=parallelogram, script="echo kept"]
+ none [label="None", shape=parallelogram, script="echo none"]
+
+ start -> classify -> gate
+ gate -> kept [label="Kept", condition="context.kept_count > 0"]
+ gate -> none [label="None"]
+ kept -> exit
+ none -> exit
+}
diff --git a/lib/crates/fabro-cli/tests/it/workflow/fixtures/conditional_branching.fabro b/lib/apps/fabro-cli/tests/it/workflow/fixtures/conditional_branching.fabro
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/fixtures/conditional_branching.fabro
rename to lib/apps/fabro-cli/tests/it/workflow/fixtures/conditional_branching.fabro
diff --git a/lib/crates/fabro-cli/tests/it/workflow/fixtures/full_stack.fabro b/lib/apps/fabro-cli/tests/it/workflow/fixtures/full_stack.fabro
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/fixtures/full_stack.fabro
rename to lib/apps/fabro-cli/tests/it/workflow/fixtures/full_stack.fabro
diff --git a/lib/crates/fabro-cli/tests/it/workflow/fixtures/human_gate.fabro b/lib/apps/fabro-cli/tests/it/workflow/fixtures/human_gate.fabro
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/fixtures/human_gate.fabro
rename to lib/apps/fabro-cli/tests/it/workflow/fixtures/human_gate.fabro
diff --git a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs b/lib/apps/fabro-cli/tests/it/workflow/full_stack.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/full_stack.rs
rename to lib/apps/fabro-cli/tests/it/workflow/full_stack.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/hooks.rs b/lib/apps/fabro-cli/tests/it/workflow/hooks.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/hooks.rs
rename to lib/apps/fabro-cli/tests/it/workflow/hooks.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs b/lib/apps/fabro-cli/tests/it/workflow/human_gate.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/it/workflow/human_gate.rs
rename to lib/apps/fabro-cli/tests/it/workflow/human_gate.rs
diff --git a/lib/crates/fabro-cli/tests/it/workflow/mod.rs b/lib/apps/fabro-cli/tests/it/workflow/mod.rs
similarity index 99%
rename from lib/crates/fabro-cli/tests/it/workflow/mod.rs
rename to lib/apps/fabro-cli/tests/it/workflow/mod.rs
index 365707d2c..5e0dd3738 100644
--- a/lib/crates/fabro-cli/tests/it/workflow/mod.rs
+++ b/lib/apps/fabro-cli/tests/it/workflow/mod.rs
@@ -8,6 +8,7 @@ mod agent_linear;
mod artifacts;
mod command_agent_mixed;
mod command_pipeline;
+mod command_routing;
mod conditional_branching;
mod dry_run_examples;
mod full_stack;
diff --git a/lib/crates/fabro-cli/tests/manifest_path_round_trip.rs b/lib/apps/fabro-cli/tests/manifest_path_round_trip.rs
similarity index 100%
rename from lib/crates/fabro-cli/tests/manifest_path_round_trip.rs
rename to lib/apps/fabro-cli/tests/manifest_path_round_trip.rs
diff --git a/lib/crates/fabro-mcp-server/Cargo.toml b/lib/apps/fabro-mcp-server/Cargo.toml
similarity index 59%
rename from lib/crates/fabro-mcp-server/Cargo.toml
rename to lib/apps/fabro-mcp-server/Cargo.toml
index de3caa633..cdbf6b65f 100644
--- a/lib/crates/fabro-mcp-server/Cargo.toml
+++ b/lib/apps/fabro-mcp-server/Cargo.toml
@@ -15,15 +15,15 @@ workspace = true
[dependencies]
anyhow.workspace = true
chrono = { workspace = true, features = ["serde"] }
-fabro-api = { path = "../fabro-api" }
-fabro-client = { path = "../fabro-client" }
-fabro-manifest = { path = "../fabro-manifest" }
-fabro-config = { path = "../fabro-config" }
-fabro-model = { path = "../fabro-model" }
+fabro-api = { path = "../../foundation/fabro-api" }
+fabro-client = { path = "../../foundation/fabro-client" }
+fabro-manifest = { path = "../../components/fabro-manifest" }
+fabro-config = { path = "../../foundation/fabro-config" }
+fabro-model = { path = "../../foundation/fabro-model" }
fabro-server = { path = "../fabro-server" }
-fabro-tool = { path = "../fabro-tool" }
-fabro-types = { path = "../fabro-types" }
-fabro-util = { path = "../fabro-util" }
+fabro-tool = { path = "../../components/fabro-tool" }
+fabro-types = { path = "../../foundation/fabro-types" }
+fabro-util = { path = "../../foundation/fabro-util" }
futures.workspace = true
rmcp = { workspace = true, features = ["server", "macros", "schemars", "transport-io"] }
schemars = "1.2.1"
diff --git a/lib/crates/fabro-mcp-server/src/config.rs b/lib/apps/fabro-mcp-server/src/config.rs
similarity index 100%
rename from lib/crates/fabro-mcp-server/src/config.rs
rename to lib/apps/fabro-mcp-server/src/config.rs
diff --git a/lib/crates/fabro-mcp-server/src/lib.rs b/lib/apps/fabro-mcp-server/src/lib.rs
similarity index 100%
rename from lib/crates/fabro-mcp-server/src/lib.rs
rename to lib/apps/fabro-mcp-server/src/lib.rs
diff --git a/lib/crates/fabro-mcp-server/src/manifest_builder.rs b/lib/apps/fabro-mcp-server/src/manifest_builder.rs
similarity index 100%
rename from lib/crates/fabro-mcp-server/src/manifest_builder.rs
rename to lib/apps/fabro-mcp-server/src/manifest_builder.rs
diff --git a/lib/crates/fabro-mcp-server/src/server.rs b/lib/apps/fabro-mcp-server/src/server.rs
similarity index 100%
rename from lib/crates/fabro-mcp-server/src/server.rs
rename to lib/apps/fabro-mcp-server/src/server.rs
diff --git a/lib/crates/fabro-server/Cargo.toml b/lib/apps/fabro-server/Cargo.toml
similarity index 56%
rename from lib/crates/fabro-server/Cargo.toml
rename to lib/apps/fabro-server/Cargo.toml
index b951b339f..1f2c5bb61 100644
--- a/lib/crates/fabro-server/Cargo.toml
+++ b/lib/apps/fabro-server/Cargo.toml
@@ -21,35 +21,35 @@ required-features = ["test-support"]
workspace = true
[dependencies]
-fabro-automation = { path = "../fabro-automation" }
-fabro-auth = { path = "../fabro-auth" }
-fabro-install = { path = "../fabro-install" }
+fabro-automation = { path = "../../components/fabro-automation" }
+fabro-auth = { path = "../../foundation/fabro-auth" }
+fabro-install = { path = "../../components/fabro-install" }
fabro-spa = { path = "../fabro-spa" }
-fabro-config = { path = "../fabro-config" }
+fabro-config = { path = "../../foundation/fabro-config" }
fabro-environment.workspace = true
-fabro-graphviz = { path = "../fabro-graphviz" }
-fabro-hooks = { path = "../fabro-hooks" }
-fabro-interview = { path = "../fabro-interview" }
-fabro-slack = { path = "../fabro-slack" }
-fabro-workflow = { path = "../fabro-workflow" }
-fabro-validate = { path = "../fabro-validate" }
-fabro-sandbox = { path = "../fabro-sandbox", features = ["daytona", "docker"] }
-fabro-github = { path = "../fabro-github" }
-fabro-agent = { path = "../fabro-agent" }
-fabro-llm = { path = "../fabro-llm" }
-fabro-manifest = { path = "../fabro-manifest" }
-fabro-mcp-store = { path = "../fabro-mcp-store" }
-fabro-model = { path = "../fabro-model" }
-fabro-proc = { path = "../fabro-proc" }
-fabro-tool = { path = "../fabro-tool" }
-fabro-types = { path = "../fabro-types" }
-fabro-util = { path = "../fabro-util" }
-fabro-api = { path = "../fabro-api" }
-fabro-client = { path = "../fabro-client" }
-fabro-db = { path = "../fabro-db" }
-fabro-store = { path = "../fabro-store" }
-fabro-vault = { path = "../fabro-vault" }
-fabro-variable = { path = "../fabro-variable" }
+fabro-graphviz = { path = "../../components/fabro-graphviz" }
+fabro-hooks = { path = "../../components/fabro-hooks" }
+fabro-interview = { path = "../../components/fabro-interview" }
+fabro-slack = { path = "../../components/fabro-slack" }
+fabro-workflow = { path = "../../components/fabro-workflow" }
+fabro-validate = { path = "../../components/fabro-validate" }
+fabro-sandbox = { path = "../../components/fabro-sandbox", features = ["daytona", "docker"] }
+fabro-github = { path = "../../components/fabro-github" }
+fabro-agent = { path = "../../components/fabro-agent" }
+fabro-llm = { path = "../../components/fabro-llm" }
+fabro-manifest = { path = "../../components/fabro-manifest" }
+fabro-mcp-store = { path = "../../components/fabro-mcp-store" }
+fabro-model = { path = "../../foundation/fabro-model" }
+fabro-proc = { path = "../../foundation/fabro-proc" }
+fabro-tool = { path = "../../components/fabro-tool" }
+fabro-types = { path = "../../foundation/fabro-types" }
+fabro-util = { path = "../../foundation/fabro-util" }
+fabro-api = { path = "../../foundation/fabro-api" }
+fabro-client = { path = "../../foundation/fabro-client" }
+fabro-db = { path = "../../foundation/fabro-db" }
+fabro-store = { path = "../../components/fabro-store" }
+fabro-vault = { path = "../../foundation/fabro-vault" }
+fabro-variable = { path = "../../components/fabro-variable" }
fabro-http.workspace = true
fabro-redact.workspace = true
fabro-static.workspace = true
@@ -104,7 +104,7 @@ strum.workspace = true
sysinfo = { version = "0.35.2", default-features = false, features = ["system", "disk"] }
[build-dependencies]
-fabro-build-support = { path = "../build-support" }
+fabro-build-support = { path = "../../foundation/build-support" }
chrono = { workspace = true }
[dev-dependencies]
@@ -117,7 +117,7 @@ sqlx.workspace = true
tracing-subscriber.workspace = true
tokio-util.workspace = true
tokio-tungstenite.workspace = true
-fabro-macros = { path = "../fabro-macros" }
-fabro-sandbox = { path = "../fabro-sandbox", features = ["test-support"] }
+fabro-macros = { path = "../../foundation/fabro-macros" }
+fabro-sandbox = { path = "../../components/fabro-sandbox", features = ["test-support"] }
fabro-test = { workspace = true }
-fabro-types = { path = "../fabro-types", features = ["test-support"] }
+fabro-types = { path = "../../foundation/fabro-types", features = ["test-support"] }
diff --git a/lib/crates/fabro-server/build.rs b/lib/apps/fabro-server/build.rs
similarity index 100%
rename from lib/crates/fabro-server/build.rs
rename to lib/apps/fabro-server/build.rs
diff --git a/lib/crates/fabro-server/migrations/2026051801_legacy_vault_entries.rs b/lib/apps/fabro-server/migrations/2026051801_legacy_vault_entries.rs
similarity index 100%
rename from lib/crates/fabro-server/migrations/2026051801_legacy_vault_entries.rs
rename to lib/apps/fabro-server/migrations/2026051801_legacy_vault_entries.rs
diff --git a/lib/crates/fabro-server/migrations/2026052501_optional_server_env_secrets_to_vault.rs b/lib/apps/fabro-server/migrations/2026052501_optional_server_env_secrets_to_vault.rs
similarity index 100%
rename from lib/crates/fabro-server/migrations/2026052501_optional_server_env_secrets_to_vault.rs
rename to lib/apps/fabro-server/migrations/2026052501_optional_server_env_secrets_to_vault.rs
diff --git a/lib/crates/fabro-server/src/auth/browser_shell.rs b/lib/apps/fabro-server/src/auth/browser_shell.rs
similarity index 100%
rename from lib/crates/fabro-server/src/auth/browser_shell.rs
rename to lib/apps/fabro-server/src/auth/browser_shell.rs
diff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/apps/fabro-server/src/auth/cli_flow.rs
similarity index 100%
rename from lib/crates/fabro-server/src/auth/cli_flow.rs
rename to lib/apps/fabro-server/src/auth/cli_flow.rs
diff --git a/lib/crates/fabro-server/src/auth/github_endpoints.rs b/lib/apps/fabro-server/src/auth/github_endpoints.rs
similarity index 100%
rename from lib/crates/fabro-server/src/auth/github_endpoints.rs
rename to lib/apps/fabro-server/src/auth/github_endpoints.rs
diff --git a/lib/crates/fabro-server/src/auth/jwt.rs b/lib/apps/fabro-server/src/auth/jwt.rs
similarity index 100%
rename from lib/crates/fabro-server/src/auth/jwt.rs
rename to lib/apps/fabro-server/src/auth/jwt.rs
diff --git a/lib/crates/fabro-server/src/auth/keys.rs b/lib/apps/fabro-server/src/auth/keys.rs
similarity index 100%
rename from lib/crates/fabro-server/src/auth/keys.rs
rename to lib/apps/fabro-server/src/auth/keys.rs
diff --git a/lib/crates/fabro-server/src/auth/mod.rs b/lib/apps/fabro-server/src/auth/mod.rs
similarity index 100%
rename from lib/crates/fabro-server/src/auth/mod.rs
rename to lib/apps/fabro-server/src/auth/mod.rs
diff --git a/lib/crates/fabro-server/src/auth/translate.rs b/lib/apps/fabro-server/src/auth/translate.rs
similarity index 100%
rename from lib/crates/fabro-server/src/auth/translate.rs
rename to lib/apps/fabro-server/src/auth/translate.rs
diff --git a/lib/crates/fabro-server/src/automation_materializer.rs b/lib/apps/fabro-server/src/automation_materializer.rs
similarity index 100%
rename from lib/crates/fabro-server/src/automation_materializer.rs
rename to lib/apps/fabro-server/src/automation_materializer.rs
diff --git a/lib/crates/fabro-server/src/canonical_host.rs b/lib/apps/fabro-server/src/canonical_host.rs
similarity index 100%
rename from lib/crates/fabro-server/src/canonical_host.rs
rename to lib/apps/fabro-server/src/canonical_host.rs
diff --git a/lib/crates/fabro-server/src/canonical_origin.rs b/lib/apps/fabro-server/src/canonical_origin.rs
similarity index 100%
rename from lib/crates/fabro-server/src/canonical_origin.rs
rename to lib/apps/fabro-server/src/canonical_origin.rs
diff --git a/lib/crates/fabro-server/src/csp.rs b/lib/apps/fabro-server/src/csp.rs
similarity index 100%
rename from lib/crates/fabro-server/src/csp.rs
rename to lib/apps/fabro-server/src/csp.rs
diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/apps/fabro-server/src/demo/mod.rs
similarity index 100%
rename from lib/crates/fabro-server/src/demo/mod.rs
rename to lib/apps/fabro-server/src/demo/mod.rs
diff --git a/lib/crates/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs
similarity index 99%
rename from lib/crates/fabro-server/src/diagnostics.rs
rename to lib/apps/fabro-server/src/diagnostics.rs
index b88f93716..988f84404 100644
--- a/lib/crates/fabro-server/src/diagnostics.rs
+++ b/lib/apps/fabro-server/src/diagnostics.rs
@@ -254,11 +254,11 @@ async fn probe_single_provider(
};
let model_id = model.id.clone();
- let outcome = run_basic_model_probe(&model_id, &provider, client).await;
+ let outcome = run_basic_model_probe(model_id.as_str(), &provider, client).await;
match outcome.status {
ModelTestStatus::Ok => ProviderProbeResult {
provider,
- model_id: Some(model_id),
+ model_id: Some(model_id.to_string()),
status: ProviderProbeStatus::Ok,
error_message: None,
diagnostic_detail: None,
@@ -267,7 +267,12 @@ async fn probe_single_provider(
let raw = outcome
.error_message
.unwrap_or_else(|| "provider probe failed".to_string());
- provider_probe_error(provider, Some(model_id), redact_string(&raw), None)
+ provider_probe_error(
+ provider,
+ Some(model_id.to_string()),
+ redact_string(&raw),
+ None,
+ )
}
}
}
diff --git a/lib/crates/fabro-server/src/error.rs b/lib/apps/fabro-server/src/error.rs
similarity index 100%
rename from lib/crates/fabro-server/src/error.rs
rename to lib/apps/fabro-server/src/error.rs
diff --git a/lib/crates/fabro-server/src/git_checkout.rs b/lib/apps/fabro-server/src/git_checkout.rs
similarity index 100%
rename from lib/crates/fabro-server/src/git_checkout.rs
rename to lib/apps/fabro-server/src/git_checkout.rs
diff --git a/lib/crates/fabro-server/src/github_webhooks.rs b/lib/apps/fabro-server/src/github_webhooks.rs
similarity index 100%
rename from lib/crates/fabro-server/src/github_webhooks.rs
rename to lib/apps/fabro-server/src/github_webhooks.rs
diff --git a/lib/crates/fabro-server/src/install.rs b/lib/apps/fabro-server/src/install.rs
similarity index 99%
rename from lib/crates/fabro-server/src/install.rs
rename to lib/apps/fabro-server/src/install.rs
index 841f405b5..12bb5fbb7 100644
--- a/lib/crates/fabro-server/src/install.rs
+++ b/lib/apps/fabro-server/src/install.rs
@@ -2167,7 +2167,7 @@ async fn validate_llm_provider(
})?
.id
.clone();
- let params = GenerateParams::new(probe_model, Arc::new(client))
+ let params = GenerateParams::new(probe_model.to_string(), Arc::new(client))
.provider(input.provider.to_string())
.prompt("Say OK")
.max_tokens(16);
diff --git a/lib/crates/fabro-server/src/interp.rs b/lib/apps/fabro-server/src/interp.rs
similarity index 100%
rename from lib/crates/fabro-server/src/interp.rs
rename to lib/apps/fabro-server/src/interp.rs
diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/apps/fabro-server/src/jwt_auth.rs
similarity index 100%
rename from lib/crates/fabro-server/src/jwt_auth.rs
rename to lib/apps/fabro-server/src/jwt_auth.rs
diff --git a/lib/crates/fabro-server/src/lib.rs b/lib/apps/fabro-server/src/lib.rs
similarity index 100%
rename from lib/crates/fabro-server/src/lib.rs
rename to lib/apps/fabro-server/src/lib.rs
diff --git a/lib/crates/fabro-server/src/manifest_validation.rs b/lib/apps/fabro-server/src/manifest_validation.rs
similarity index 100%
rename from lib/crates/fabro-server/src/manifest_validation.rs
rename to lib/apps/fabro-server/src/manifest_validation.rs
diff --git a/lib/crates/fabro-server/src/migrations.rs b/lib/apps/fabro-server/src/migrations.rs
similarity index 100%
rename from lib/crates/fabro-server/src/migrations.rs
rename to lib/apps/fabro-server/src/migrations.rs
diff --git a/lib/crates/fabro-server/src/principal_middleware.rs b/lib/apps/fabro-server/src/principal_middleware.rs
similarity index 100%
rename from lib/crates/fabro-server/src/principal_middleware.rs
rename to lib/apps/fabro-server/src/principal_middleware.rs
diff --git a/lib/crates/fabro-server/src/request_id.rs b/lib/apps/fabro-server/src/request_id.rs
similarity index 100%
rename from lib/crates/fabro-server/src/request_id.rs
rename to lib/apps/fabro-server/src/request_id.rs
diff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/apps/fabro-server/src/run_files.rs
similarity index 100%
rename from lib/crates/fabro-server/src/run_files.rs
rename to lib/apps/fabro-server/src/run_files.rs
diff --git a/lib/crates/fabro-server/src/run_files_security.rs b/lib/apps/fabro-server/src/run_files_security.rs
similarity index 100%
rename from lib/crates/fabro-server/src/run_files_security.rs
rename to lib/apps/fabro-server/src/run_files_security.rs
diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs
similarity index 98%
rename from lib/crates/fabro-server/src/run_manifest.rs
rename to lib/apps/fabro-server/src/run_manifest.rs
index 93c6660c6..259e3d2b5 100644
--- a/lib/crates/fabro-server/src/run_manifest.rs
+++ b/lib/apps/fabro-server/src/run_manifest.rs
@@ -476,19 +476,20 @@ async fn build_preflight_report(
let catalog = state.catalog();
let llm_result = state.resolve_llm_client().await;
- let configured_providers = match &llm_result {
- Ok(result) => result.provider_ids(),
- Err(err) => {
- warn!(error = ?err, "Failed to resolve LLM client while checking ready providers");
- Vec::new()
- }
- };
+ if let Err(err) = &llm_result {
+ warn!(error = ?err, "Failed to resolve LLM client while checking ready providers");
+ }
+ // Preflight is credential-independent static validation. Materialize
+ // against every enabled catalog provider so aliases and defaults can be
+ // inspected even when the corresponding adapter is not currently ready;
+ // `run_llm_check` below reports actual credential/registration readiness.
+ let enabled_providers = catalog.all_provider_ids().into_iter().collect::>();
let materialized = materialize_run(
prepared.settings.clone(),
graph,
catalog.as_ref(),
- &configured_providers,
- );
+ &enabled_providers,
+ )?;
let resolved_run = materialized.run;
let server_settings = state.server_settings();
let github_integration = &server_settings.server.integrations.github;
@@ -1049,8 +1050,15 @@ async fn run_llm_check(
has_llm_nodes = true;
let node_model = node.model().unwrap_or(model);
let node_provider = node.provider().unwrap_or(default_provider);
- let (resolved_model, resolved_provider) = if let Some(info) = catalog.get(node_model) {
- (info.id.clone(), info.provider.to_string())
+ let resolved = if node.provider().is_some() {
+ catalog.get_on_provider(&ProviderId::new(node_provider), node_model)
+ } else {
+ catalog
+ .select(node_model, None, &catalog.all_provider_ids())
+ .ok()
+ };
+ let (resolved_model, resolved_provider) = if let Some(info) = resolved {
+ (info.id.to_string(), info.provider.to_string())
} else {
(node_model.to_string(), node_provider.to_string())
};
@@ -1534,6 +1542,7 @@ enabled = {clone_enabled}
Catalog::builtin(),
&[ProviderId::anthropic()],
)
+ .unwrap()
.run;
(prepared, resolved)
@@ -2381,9 +2390,8 @@ digraph Demo {
assert!(response_mock.calls_async().await >= 1);
}
- #[tokio::test]
- async fn preflight_unknown_llm_provider_reports_not_configured() {
- let state = crate::test_support::test_app_state();
+ #[test]
+ fn static_validation_rejects_unknown_llm_provider() {
let mut manifest = minimal_manifest();
manifest.workflows.get_mut("workflow.fabro").unwrap().source = r#"
digraph Demo {
@@ -2399,29 +2407,16 @@ digraph Demo {
&manifest,
)
.unwrap();
- let validated = validate_prepared_manifest(&prepared, test_catalog()).unwrap();
+ let Err(error) = validate_prepared_manifest(&prepared, test_catalog()) else {
+ panic!("unknown provider should fail static validation");
+ };
- let (response, ok) = run_preflight(state.as_ref(), &prepared, &validated)
- .await
- .unwrap();
-
- assert!(!ok);
- let llm_check = response.checks.sections[0]
- .checks
- .iter()
- .find(|check| check.name == "LLM" && check.summary == "missing-model")
- .expect("preflight should include the requested custom LLM provider");
- assert_eq!(llm_check.status, types::PreflightCheckResultStatus::Warning);
- assert_eq!(
- llm_check.remediation.as_deref(),
- Some("Provider \"missing-provider\" is not configured")
- );
- assert!(
- llm_check
- .details
- .iter()
- .any(|detail| detail.text == "Provider: missing-provider")
- );
+ assert!(matches!(
+ error,
+ WorkflowError::ModelSelection(fabro_model::ModelSelectionError::UnknownProvider {
+ provider
+ }) if provider.as_str() == "missing-provider"
+ ));
}
#[tokio::test]
@@ -2437,17 +2432,16 @@ base_url = "https://api.acme.test/v1"
[providers.acme.auth]
credentials = ["env:ACME_API_KEY"]
-[models."acme-large"]
-provider = "acme"
+[providers.acme.models."acme-large"]
display_name = "Acme Large"
family = "acme"
default = true
aliases = ["vl"]
-[models."acme-large".limits]
+[providers.acme.models."acme-large".limits]
context_window = 128000
-[models."acme-large".features]
+[providers.acme.models."acme-large".features]
tools = true
vision = false
reasoning = false
@@ -2472,7 +2466,7 @@ digraph Demo {
&manifest,
)
.unwrap();
- let validated = validate_prepared_manifest(&prepared, test_catalog()).unwrap();
+ let validated = validate_prepared_manifest(&prepared, state.catalog()).unwrap();
let (response, ok) = run_preflight(state.as_ref(), &prepared, &validated)
.await
diff --git a/lib/crates/fabro-server/src/run_selector.rs b/lib/apps/fabro-server/src/run_selector.rs
similarity index 100%
rename from lib/crates/fabro-server/src/run_selector.rs
rename to lib/apps/fabro-server/src/run_selector.rs
diff --git a/lib/crates/fabro-server/src/run_title_generation.rs b/lib/apps/fabro-server/src/run_title_generation.rs
similarity index 100%
rename from lib/crates/fabro-server/src/run_title_generation.rs
rename to lib/apps/fabro-server/src/run_title_generation.rs
diff --git a/lib/crates/fabro-server/src/run_tool_manifest.rs b/lib/apps/fabro-server/src/run_tool_manifest.rs
similarity index 100%
rename from lib/crates/fabro-server/src/run_tool_manifest.rs
rename to lib/apps/fabro-server/src/run_tool_manifest.rs
diff --git a/lib/crates/fabro-server/src/security_headers.rs b/lib/apps/fabro-server/src/security_headers.rs
similarity index 100%
rename from lib/crates/fabro-server/src/security_headers.rs
rename to lib/apps/fabro-server/src/security_headers.rs
diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/apps/fabro-server/src/serve.rs
similarity index 100%
rename from lib/crates/fabro-server/src/serve.rs
rename to lib/apps/fabro-server/src/serve.rs
diff --git a/lib/crates/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs
similarity index 99%
rename from lib/crates/fabro-server/src/server.rs
rename to lib/apps/fabro-server/src/server.rs
index ab4c0a726..98dcc4a62 100644
--- a/lib/crates/fabro-server/src/server.rs
+++ b/lib/apps/fabro-server/src/server.rs
@@ -1453,7 +1453,7 @@ impl AppState {
self.catalog()
.default_for_configured_ids(&provider_ids)
.id
- .clone(),
+ .to_string(),
)
};
AskFabroReadiness { default_model }
diff --git a/lib/crates/fabro-server/src/server/automation_scheduler.rs b/lib/apps/fabro-server/src/server/automation_scheduler.rs
similarity index 99%
rename from lib/crates/fabro-server/src/server/automation_scheduler.rs
rename to lib/apps/fabro-server/src/server/automation_scheduler.rs
index 4eb802fab..0abd3e988 100644
--- a/lib/crates/fabro-server/src/server/automation_scheduler.rs
+++ b/lib/apps/fabro-server/src/server/automation_scheduler.rs
@@ -336,6 +336,7 @@ fn run_due_schedules_once<'a>(
mod tests {
use fabro_api::types::RunManifest;
use fabro_automation::{AutomationDraft, AutomationTarget, AutomationTrigger, ScheduleTrigger};
+ use fabro_static::EnvVars;
use fabro_store::ListRunsQuery;
use fabro_types::RunStatus;
use serde_json::json;
@@ -428,6 +429,7 @@ mod tests {
fn test_state_with_materializer(materializer: TestAutomationRunMaterializer) -> Arc {
TestAppStateBuilder::new()
.env_lookup(|_| None)
+ .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")])
.automation_materializer(materializer)
.build()
}
diff --git a/lib/crates/fabro-server/src/server/handler/artifacts.rs b/lib/apps/fabro-server/src/server/handler/artifacts.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/artifacts.rs
rename to lib/apps/fabro-server/src/server/handler/artifacts.rs
diff --git a/lib/crates/fabro-server/src/server/handler/automations.rs b/lib/apps/fabro-server/src/server/handler/automations.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/automations.rs
rename to lib/apps/fabro-server/src/server/handler/automations.rs
diff --git a/lib/crates/fabro-server/src/server/handler/billing.rs b/lib/apps/fabro-server/src/server/handler/billing.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/billing.rs
rename to lib/apps/fabro-server/src/server/handler/billing.rs
diff --git a/lib/crates/fabro-server/src/server/handler/completions.rs b/lib/apps/fabro-server/src/server/handler/completions.rs
similarity index 84%
rename from lib/crates/fabro-server/src/server/handler/completions.rs
rename to lib/apps/fabro-server/src/server/handler/completions.rs
index d5df7299a..b53e56d31 100644
--- a/lib/crates/fabro-server/src/server/handler/completions.rs
+++ b/lib/apps/fabro-server/src/server/handler/completions.rs
@@ -1,10 +1,13 @@
+use std::collections::HashSet;
use std::sync::Arc;
+use fabro_model::{Catalog, ModelSelectionError};
+
use super::super::{
ApiError, AppState, CompletionResponse, CompletionToolChoiceMode, CompletionUsage,
CreateCompletionRequest, FinishReason, GenerateParams, IntoResponse, Json, LlmMessage,
- LlmRequest, RequiredUser, Response, Router, State, StatusCode, ToolChoice, ToolDefinition,
- Ulid, error, generate_object, info, post, warn,
+ LlmRequest, ProviderId, RequiredUser, Response, Router, State, StatusCode, ToolChoice,
+ ToolDefinition, Ulid, error, generate_object, info, post, warn,
};
use super::llm_sse;
@@ -28,21 +31,34 @@ async fn create_completion(
State(state): State>,
Json(req): Json,
) -> Response {
- // Resolve model
let catalog = state.catalog();
- let model_id = req
- .model
- .unwrap_or_else(|| catalog.default_model().id.clone());
-
- let catalog_info = catalog.get(&model_id);
-
- // Resolve provider: explicit request > catalog > None
- let explicit_provider = req.provider;
- let provider_name = explicit_provider
- .clone()
- .or_else(|| catalog_info.map(|i| i.provider.to_string()));
-
- info!(model = %model_id, provider = ?provider_name, "Completion request received");
+ let llm_result = match state.resolve_llm_client().await {
+ Ok(result) => result,
+ Err(err) => {
+ error!(error = ?err, "Failed to create LLM client");
+ return ApiError::new(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Failed to resolve LLM providers: {err}"),
+ )
+ .into_response();
+ }
+ };
+ for (provider, issue) in &llm_result.auth_issues {
+ warn!(provider = %provider, error = %issue, "LLM provider unavailable due to auth issue");
+ }
+ for issue in &llm_result.registration_issues {
+ warn!(provider = %issue.provider, error = %issue.error, "LLM provider unavailable due to registration issue");
+ }
+ let client = llm_result.client;
+ let (model_id, selected_provider) = match resolve_request_model(
+ catalog.as_ref(),
+ &client.provider_ids(),
+ req.model.as_deref(),
+ req.provider,
+ ) {
+ Ok(selection) => selection,
+ Err(error) => return ApiError::bad_request(error.to_string()).into_response(),
+ };
// Build messages list. Request messages are already the canonical
// `fabro_types::Message` — the API schema reuses it via build.rs
@@ -81,7 +97,7 @@ async fn create_completion(
let request = LlmRequest {
model: model_id.clone(),
messages,
- provider: provider_name.clone(),
+ provider: Some(selected_provider.to_string()),
tools,
tool_choice,
response_format: None,
@@ -98,35 +114,15 @@ async fn create_completion(
metadata: None,
provider_options: req.provider_options,
};
+ info!(
+ model = %model_id,
+ provider = %selected_provider,
+ "Completion request received"
+ );
// Force non-streaming for structured output
let use_stream = req.stream && req.schema.is_none();
- let llm_result = match state.resolve_llm_client().await {
- Ok(result) => result,
- Err(err) => {
- error!(error = ?err, "Failed to create LLM client");
- return ApiError::new(
- StatusCode::INTERNAL_SERVER_ERROR,
- format!("Failed to create LLM client: {err}"),
- )
- .into_response();
- }
- };
- for (provider, issue) in &llm_result.auth_issues {
- warn!(provider = %provider, error = %issue, "LLM provider unavailable due to auth issue");
- }
- for issue in &llm_result.registration_issues {
- warn!(provider = %issue.provider, error = %issue.error, "LLM provider unavailable due to registration issue");
- }
- let client = llm_result.client;
- if let Some(provider) = explicit_provider.as_deref() {
- if !client.has_provider(provider) {
- return ApiError::bad_request(format!("Provider \"{provider}\" is not configured"))
- .into_response();
- }
- }
-
if use_stream {
// Streaming path: forward all StreamEvents as SSE
let stream_result = match client.stream(&request).await {
@@ -170,6 +166,7 @@ async fn create_completion(
Json(CompletionResponse {
id: msg_id,
model: model_id,
+ provider: selected_provider,
message: response.message,
stop_reason,
usage: CompletionUsage {
@@ -192,6 +189,7 @@ async fn create_completion(
Json(CompletionResponse {
id: response.id,
model: response.model,
+ provider: ProviderId::new(response.provider),
message: response.message,
stop_reason,
usage: CompletionUsage {
@@ -210,3 +208,15 @@ async fn create_completion(
}
}
}
+
+pub(super) fn resolve_request_model(
+ catalog: &Catalog,
+ eligible: &HashSet,
+ requested_model: Option<&str>,
+ explicit_provider: Option,
+) -> Result<(String, ProviderId), ModelSelectionError> {
+ let explicit_provider = explicit_provider.map(ProviderId::new);
+ let selected =
+ catalog.resolve_selection(requested_model, explicit_provider.as_ref(), eligible)?;
+ Ok((selected.model, selected.provider))
+}
diff --git a/lib/crates/fabro-server/src/server/handler/environments.rs b/lib/apps/fabro-server/src/server/handler/environments.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/environments.rs
rename to lib/apps/fabro-server/src/server/handler/environments.rs
diff --git a/lib/crates/fabro-server/src/server/handler/events.rs b/lib/apps/fabro-server/src/server/handler/events.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/events.rs
rename to lib/apps/fabro-server/src/server/handler/events.rs
diff --git a/lib/crates/fabro-server/src/server/handler/graph.rs b/lib/apps/fabro-server/src/server/handler/graph.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/graph.rs
rename to lib/apps/fabro-server/src/server/handler/graph.rs
diff --git a/lib/crates/fabro-server/src/server/handler/lifecycle.rs b/lib/apps/fabro-server/src/server/handler/lifecycle.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/lifecycle.rs
rename to lib/apps/fabro-server/src/server/handler/lifecycle.rs
diff --git a/lib/crates/fabro-server/src/server/handler/llm_sse.rs b/lib/apps/fabro-server/src/server/handler/llm_sse.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/llm_sse.rs
rename to lib/apps/fabro-server/src/server/handler/llm_sse.rs
diff --git a/lib/crates/fabro-server/src/server/handler/mcp_servers.rs b/lib/apps/fabro-server/src/server/handler/mcp_servers.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/mcp_servers.rs
rename to lib/apps/fabro-server/src/server/handler/mcp_servers.rs
diff --git a/lib/crates/fabro-server/src/server/handler/mod.rs b/lib/apps/fabro-server/src/server/handler/mod.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/mod.rs
rename to lib/apps/fabro-server/src/server/handler/mod.rs
diff --git a/lib/crates/fabro-server/src/server/handler/models.rs b/lib/apps/fabro-server/src/server/handler/models.rs
similarity index 84%
rename from lib/crates/fabro-server/src/server/handler/models.rs
rename to lib/apps/fabro-server/src/server/handler/models.rs
index 3d5341270..06af1d3de 100644
--- a/lib/crates/fabro-server/src/server/handler/models.rs
+++ b/lib/apps/fabro-server/src/server/handler/models.rs
@@ -3,6 +3,7 @@ use std::sync::Arc;
use fabro_auth::ApiCredential;
use fabro_llm::client::Client as LlmClient;
use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe};
+use fabro_model::ModelSelectionError;
use fabro_redact::redact_string;
use super::super::{
@@ -40,7 +41,9 @@ struct ModelListParams {
#[derive(serde::Deserialize)]
struct ModelTestParams {
#[serde(default)]
- mode: Option,
+ mode: Option,
+ #[serde(default)]
+ provider: Option,
}
async fn list_models(
@@ -62,7 +65,7 @@ async fn list_models(
.into_iter()
.filter(|model| match &query {
Some(query) => {
- model.id.to_lowercase().contains(query)
+ model.id.as_str().to_lowercase().contains(query)
|| model.display_name.to_lowercase().contains(query)
|| model
.aliases
@@ -155,7 +158,7 @@ async fn test_provider_credentials(
.into_response();
};
- let outcome = run_basic_model_probe(&model.id, &provider_id, client).await;
+ let outcome = run_basic_model_probe(model.id.as_str(), &provider_id, client).await;
match outcome.status {
ModelTestStatus::Ok => (
StatusCode::OK,
@@ -204,11 +207,6 @@ async fn test_model(
},
None => ModelTestMode::Basic,
};
- let catalog = state.catalog();
- let Some(info) = catalog.get(&id) else {
- return ApiError::not_found(format!("Model not found: {id}")).into_response();
- };
-
let llm_result = match state.resolve_llm_client().await {
Ok(result) => result,
Err(err) => {
@@ -220,6 +218,23 @@ async fn test_model(
.into_response();
}
};
+ let catalog = state.catalog();
+ let eligible = llm_result
+ .provider_ids()
+ .into_iter()
+ .collect::>();
+ let explicit_provider = params.provider.map(ProviderId::new);
+ let info = if let Some(provider) = explicit_provider.as_ref() {
+ match catalog.resolve_on_provider(provider, &id) {
+ Ok(info) => info,
+ Err(error) => return model_selection_response(&error),
+ }
+ } else {
+ match catalog.select(&id, None, &eligible) {
+ Ok(info) => info,
+ Err(error) => return model_selection_response(&error),
+ }
+ };
if let Some((_, issue)) = llm_result
.auth_issues
.iter()
@@ -231,6 +246,7 @@ async fn test_model(
if !llm_result.client.has_provider(provider_name) {
return Json(serde_json::json!({
"model_id": info.id,
+ "provider": info.provider,
"status": "skip",
}))
.into_response();
@@ -240,8 +256,24 @@ async fn test_model(
let outcome = run_model_test(info, mode, client).await;
Json(serde_json::json!({
"model_id": info.id,
+ "provider": info.provider,
"status": <&'static str>::from(outcome.status),
"error_message": outcome.error_message,
}))
.into_response()
}
+
+fn model_selection_response(error: &ModelSelectionError) -> Response {
+ match error {
+ ModelSelectionError::UnknownProvider { .. }
+ | ModelSelectionError::UnknownSelector { .. }
+ | ModelSelectionError::UnknownSelectorOnProvider { .. } => {
+ ApiError::not_found(error.to_string()).into_response()
+ }
+ ModelSelectionError::ProviderUnavailable { .. }
+ | ModelSelectionError::NoEligibleOffering { .. }
+ | ModelSelectionError::NoDefaultModel { .. } => {
+ ApiError::bad_request(error.to_string()).into_response()
+ }
+ }
+}
diff --git a/lib/crates/fabro-server/src/server/handler/pair.rs b/lib/apps/fabro-server/src/server/handler/pair.rs
similarity index 99%
rename from lib/crates/fabro-server/src/server/handler/pair.rs
rename to lib/apps/fabro-server/src/server/handler/pair.rs
index 5b9d36270..67bc1ad05 100644
--- a/lib/crates/fabro-server/src/server/handler/pair.rs
+++ b/lib/apps/fabro-server/src/server/handler/pair.rs
@@ -883,7 +883,7 @@ mod tests {
text: "I found the issue.".to_string(),
model: ModelRef {
provider: ProviderId::new("openai"),
- model_id: "gpt-5.4".to_string(),
+ model_id: "gpt-5.4".into(),
speed: None,
},
billing: BilledTokenCounts::default(),
@@ -916,7 +916,7 @@ mod tests {
text: "wrong stage".to_string(),
model: ModelRef {
provider: ProviderId::new("openai"),
- model_id: "gpt-5.4".to_string(),
+ model_id: "gpt-5.4".into(),
speed: None,
},
billing: BilledTokenCounts::default(),
diff --git a/lib/crates/fabro-server/src/server/handler/playground.rs b/lib/apps/fabro-server/src/server/handler/playground.rs
similarity index 92%
rename from lib/crates/fabro-server/src/server/handler/playground.rs
rename to lib/apps/fabro-server/src/server/handler/playground.rs
index 0b3111d90..d404c2b58 100644
--- a/lib/crates/fabro-server/src/server/handler/playground.rs
+++ b/lib/apps/fabro-server/src/server/handler/playground.rs
@@ -109,11 +109,33 @@ async fn create_playground_chat(
}
let catalog = state.catalog();
- let model_id = req
- .model
- .unwrap_or_else(|| catalog.default_model().id.clone());
-
- info!(model = %model_id, "Playground chat turn");
+ let llm_result = match state.resolve_llm_client().await {
+ Ok(result) => result,
+ Err(err) => {
+ error!(error = ?err, "playground: failed to create LLM client");
+ return ApiError::new(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Failed to resolve LLM providers: {err}"),
+ )
+ .into_response();
+ }
+ };
+ for (provider, issue) in &llm_result.auth_issues {
+ warn!(provider = %provider, error = %issue, "playground: provider auth issue");
+ }
+ for issue in &llm_result.registration_issues {
+ warn!(provider = %issue.provider, error = %issue.error, "playground: provider registration issue");
+ }
+ let client = llm_result.client;
+ let (model_id, selected_provider) = match super::completions::resolve_request_model(
+ catalog.as_ref(),
+ &client.provider_ids(),
+ req.model.as_deref(),
+ req.provider.map(|provider| provider.to_string()),
+ ) {
+ Ok(selection) => selection,
+ Err(error) => return ApiError::bad_request(error.to_string()).into_response(),
+ };
// Request messages are already the canonical `fabro_types::Message` —
// the API schema reuses it via build.rs `with_replacement`.
@@ -124,7 +146,7 @@ async fn create_playground_chat(
let request = LlmRequest {
model: model_id,
messages,
- provider: None,
+ provider: Some(selected_provider.into_inner()),
tools: Some(playground_tools()),
tool_choice: Some(ToolChoice::Auto),
response_format: None,
@@ -137,22 +159,11 @@ async fn create_playground_chat(
metadata: None,
provider_options: None,
};
-
- let llm_result = match state.resolve_llm_client().await {
- Ok(r) => r,
- Err(err) => {
- error!(error = ?err, "playground: failed to create LLM client");
- return ApiError::new(
- StatusCode::INTERNAL_SERVER_ERROR,
- format!("Failed to create LLM client: {err}"),
- )
- .into_response();
- }
- };
- for (provider, issue) in &llm_result.auth_issues {
- warn!(provider = %provider, error = %issue, "playground: provider auth issue");
- }
- let client = llm_result.client;
+ info!(
+ model = %request.model,
+ provider = request.provider.as_deref().unwrap_or(""),
+ "Playground chat turn"
+ );
let stream_result = match client.stream(&request).await {
Ok(s) => s,
@@ -238,6 +249,7 @@ mod tests {
messages,
workflow_fabro,
model: None,
+ provider: None,
}
}
diff --git a/lib/crates/fabro-server/src/server/handler/prompts/playground_system.md b/lib/apps/fabro-server/src/server/handler/prompts/playground_system.md
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/prompts/playground_system.md
rename to lib/apps/fabro-server/src/server/handler/prompts/playground_system.md
diff --git a/lib/crates/fabro-server/src/server/handler/pull_requests.rs b/lib/apps/fabro-server/src/server/handler/pull_requests.rs
similarity index 99%
rename from lib/crates/fabro-server/src/server/handler/pull_requests.rs
rename to lib/apps/fabro-server/src/server/handler/pull_requests.rs
index b23af4d82..7acd8fb9d 100644
--- a/lib/crates/fabro-server/src/server/handler/pull_requests.rs
+++ b/lib/apps/fabro-server/src/server/handler/pull_requests.rs
@@ -321,7 +321,10 @@ async fn create_run_pull_request(
} else {
let catalog = state.catalog();
let configured = state.ready_llm_provider_ids().await;
- catalog.default_for_configured_ids(&configured).id.clone()
+ catalog
+ .default_for_configured_ids(&configured)
+ .id
+ .to_string()
};
let catalog = state.catalog();
diff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/apps/fabro-server/src/server/handler/runs.rs
similarity index 97%
rename from lib/crates/fabro-server/src/server/handler/runs.rs
rename to lib/apps/fabro-server/src/server/handler/runs.rs
index 6ea48942f..614f2d719 100644
--- a/lib/crates/fabro-server/src/server/handler/runs.rs
+++ b/lib/apps/fabro-server/src/server/handler/runs.rs
@@ -52,6 +52,8 @@ use crate::run_manifest;
use crate::run_selector::{ResolveRunError, resolve_run_by_selector};
use crate::run_title_generation::{self, GenerateTitleInput, TitlePromptInput, WorkflowSummary};
use crate::server_secrets::LlmClientResult;
+#[cfg(any(test, feature = "test-support"))]
+use crate::test_support as server_test_support;
pub(super) fn manifest_routes() -> Router> {
Router::new()
@@ -600,10 +602,23 @@ pub(crate) async fn create_run_from_manifest(
.as_ref()
.map(LlmClientResult::provider_ids)
.unwrap_or_default();
+ let run_materialization_provider_ids = {
+ #[cfg(any(test, feature = "test-support"))]
+ {
+ server_test_support::test_run_materialization_provider_ids(
+ catalog.as_ref(),
+ &ready_provider_ids,
+ )
+ }
+ #[cfg(not(any(test, feature = "test-support")))]
+ {
+ ready_provider_ids.clone()
+ }
+ };
let provenance = run_provenance(&headers, &actor);
let mut create_input = run_manifest::create_run_input(
prepared.clone(),
- ready_provider_ids.clone(),
+ run_materialization_provider_ids,
provenance,
web_url.clone(),
vars,
@@ -625,6 +640,9 @@ pub(crate) async fn create_run_from_manifest(
Err(WorkflowError::ValidationFailed { .. } | WorkflowError::Parse(_)) => {
return ApiError::bad_request("Validation failed").into_response();
}
+ Err(err @ (WorkflowError::ModelSelection(_) | WorkflowError::ModelReference(_))) => {
+ return ApiError::bad_request(err.to_string()).into_response();
+ }
Err(err) => {
return ApiError::new(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -681,7 +699,7 @@ pub(crate) async fn create_run_from_manifest(
workflow,
run_inputs,
client: llm_result.client,
- model_id: title_model_id,
+ model_id: title_model_id.to_string(),
provider_id: title_provider_id,
});
}
@@ -725,23 +743,6 @@ fn spawn_generated_title_task(task: GeneratedTitleTask) {
return;
}
- let current = match task
- .state
- .stores
- .runs
- .get_cached_summary(&task.run_id, Utc::now())
- .await
- {
- Ok(Some(summary)) => summary,
- Ok(None) => return,
- Err(err) => {
- tracing::debug!(run_id = %task.run_id, error = %err, "Failed to re-read run summary for title update");
- return;
- }
- };
- if current.title != task.deterministic_title {
- return;
- }
let run_store = match task.state.stores.runs.open_run(&task.run_id).await {
Ok(store) => store,
Err(err) => {
@@ -749,7 +750,8 @@ fn spawn_generated_title_task(task: GeneratedTitleTask) {
return;
}
};
- if let Err(err) = workflow_event::append_event(
+ let expected_title = task.deterministic_title;
+ if let Err(err) = workflow_event::append_event_if(
&run_store,
&task.run_id,
&workflow_event::Event::RunTitleUpdated {
@@ -758,6 +760,7 @@ fn spawn_generated_title_task(task: GeneratedTitleTask) {
system_kind: SystemActorKind::Engine,
}),
},
+ move |projection| projection.title().as_ref() == expected_title,
)
.await
{
diff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/apps/fabro-server/src/server/handler/sandbox.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/sandbox.rs
rename to lib/apps/fabro-server/src/server/handler/sandbox.rs
diff --git a/lib/crates/fabro-server/src/server/handler/sandboxes.rs b/lib/apps/fabro-server/src/server/handler/sandboxes.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/sandboxes.rs
rename to lib/apps/fabro-server/src/server/handler/sandboxes.rs
diff --git a/lib/crates/fabro-server/src/server/handler/secrets.rs b/lib/apps/fabro-server/src/server/handler/secrets.rs
similarity index 100%
rename from lib/crates/fabro-server/src/server/handler/secrets.rs
rename to lib/apps/fabro-server/src/server/handler/secrets.rs
diff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs
similarity index 84%
rename from lib/crates/fabro-server/src/server/handler/sessions.rs
rename to lib/apps/fabro-server/src/server/handler/sessions.rs
index 3e24a1f9d..f053207f6 100644
--- a/lib/crates/fabro-server/src/server/handler/sessions.rs
+++ b/lib/apps/fabro-server/src/server/handler/sessions.rs
@@ -22,7 +22,9 @@ use fabro_api::types::{
};
use fabro_llm::client::Client as LlmClient;
use fabro_llm::types::ToolDefinition;
-use fabro_model::{AgentProfileKind, Catalog, ModelHandle, ProviderId};
+use fabro_model::{
+ AgentProfileKind, Catalog, ModelHandle, ModelSelectionError, ProviderId, catalog,
+};
use fabro_sandbox::reconnect::reconnect_for_run;
use fabro_static::EnvVars;
use fabro_store::{
@@ -35,7 +37,7 @@ use fabro_types::run_event::{
RunSessionTurnFailedProps, RunSessionTurnInterruptedProps, RunSessionTurnStartedProps,
RunSessionTurnSucceededProps, RunSessionUserMessageProps,
};
-use fabro_types::settings::{ModelRef as SettingsModelRef, ModelRegistry, ResolvedModelRef};
+use fabro_types::settings::ModelRef as SettingsModelRef;
use fabro_types::{EventBody, EventEnvelope, RunEvent, RunId, SessionDetail, SessionId, TurnId};
use fabro_workflow::handler::llm::api::register_named_fabro_run_tools;
use fabro_workflow::services::FabroRunToolServices;
@@ -160,8 +162,24 @@ async fn create_run_session(
Ok(store) => store,
Err(response) => return response,
};
- let model = match canonical_session_model(state.catalog().as_ref(), request.model.as_deref()) {
- Ok(model) => model,
+ let llm_result = match state.resolve_llm_client().await {
+ Ok(result) => result,
+ Err(err) => {
+ return ApiError::new(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Failed to resolve LLM providers: {err}"),
+ )
+ .into_response();
+ }
+ };
+ let eligible = llm_result.provider_ids().into_iter().collect();
+ let (provider, model) = match canonical_session_model(
+ state.catalog().as_ref(),
+ &eligible,
+ request.model.as_deref(),
+ request.provider.as_ref(),
+ ) {
+ Ok(selection) => selection,
Err(err) => return err.into_response(),
};
@@ -180,8 +198,9 @@ async fn create_run_session(
run_id,
session_id,
EventBody::RunSessionCreated(RunSessionCreatedProps {
- title: request.title,
- model,
+ title: request.title,
+ model: Some(model),
+ provider: Some(provider),
}),
now,
)
@@ -782,17 +801,26 @@ fn selected_session_model(
llm_result: &LlmClientResult,
session: &ProjectedRunSession,
) -> Result<(ProviderId, String, AgentProfileKind), AskFabroBuildError> {
- let configured_provider_ids = llm_result.provider_ids();
- let selected = match session.record.model.as_deref() {
- Some(model_id) => catalog.get(model_id).ok_or_else(|| {
- AskFabroBuildError::ModelUnavailable(format!(
- "session model '{model_id}' is not in the catalog"
- ))
- })?,
- None => catalog.default_for_configured_ids(&configured_provider_ids),
- };
- let provider_id = selected.provider.clone();
- let model = selected.id.clone();
+ let eligible = llm_result
+ .provider_ids()
+ .into_iter()
+ .collect::>();
+ let record = &session.record;
+ let selected = catalog
+ .resolve_selection(record.model.as_deref(), record.provider.as_ref(), &eligible)
+ .map_err(|error| {
+ // A missing default with no provider pin means no LLM is
+ // configured at all; every other failure is about the requested
+ // model/provider.
+ if record.provider.is_none()
+ && matches!(error, ModelSelectionError::NoDefaultModel { .. })
+ {
+ AskFabroBuildError::LlmUnconfigured(error.to_string())
+ } else {
+ AskFabroBuildError::ModelUnavailable(error.to_string())
+ }
+ })?;
+ let (provider_id, model) = (selected.provider, selected.model);
let profile_kind = catalog
.effective_agent_profile(&provider_id, Some(&model))
.ok_or_else(|| {
@@ -805,79 +833,88 @@ fn selected_session_model(
fn canonical_session_model(
catalog: &Catalog,
+ eligible: &std::collections::HashSet,
requested: Option<&str>,
-) -> Result