Merge remote-tracking branch 'origin/main'

This commit is contained in:
Bryan Helmkamp 2026-05-09 11:43:59 -04:00
commit 48545f4a7d
No known key found for this signature in database
104 changed files with 403 additions and 1232 deletions

View file

@ -2,18 +2,20 @@ import { describe, expect, test } from "bun:test";
import { getVisibleNavigation } from "./app-shell";
describe("getVisibleNavigation", () => {
test("shows all nav items in demo mode", () => {
test("shows all nav items in demo mode with Start first", () => {
const items = getVisibleNavigation(true);
const names = items.map((i) => i.name);
expect(names[0]).toBe("Start");
expect(names).toContain("Workflows");
expect(names).toContain("Runs");
expect(names).toContain("Insights");
expect(names).toContain("Settings");
});
test("hides Workflows and Insights in production mode", () => {
test("hides Start, Workflows, and Insights in production mode", () => {
const items = getVisibleNavigation(false);
const names = items.map((i) => i.name);
expect(names).not.toContain("Start");
expect(names).not.toContain("Workflows");
expect(names).not.toContain("Insights");
expect(names).toContain("Runs");

View file

@ -14,6 +14,7 @@ import {
Cog6ToothIcon,
PlayIcon,
RectangleStackIcon,
SparklesIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Link, Outlet, useLocation, useMatches } from "react-router";
@ -24,6 +25,7 @@ import { useToggleDemoMode } from "../lib/mutations";
import { useAuthMe } from "../lib/queries";
const allNavigation = [
{ name: "Start", href: "/start", icon: SparklesIcon, demoOnly: true },
{ name: "Workflows", href: "/workflows", icon: RectangleStackIcon, demoOnly: true },
{ name: "Runs", href: "/runs", icon: PlayIcon, demoOnly: false },
{ name: "Insights", href: "/insights", icon: ChartBarIcon, demoOnly: true },

View file

@ -140,8 +140,6 @@ function SandboxPanel({ snapshot }: { snapshot: Snapshot }) {
const provider = getString(sandbox, "provider");
const docker = getObject(sandbox, "docker");
const dockerImage = getString(docker, "image");
const local = getObject(sandbox, "local");
const worktreeMode = getString(local, "worktree_mode");
return (
<Panel title="Sandbox">
<Row title="Provider" help="Execution environment for this run.">
@ -152,11 +150,6 @@ function SandboxPanel({ snapshot }: { snapshot: Snapshot }) {
<Mono>{dockerImage}</Mono>
</Row>
) : null}
{provider === "local" && worktreeMode ? (
<Row title="Worktree mode" help="How the local provider materializes the workspace.">
<Badge>{worktreeMode}</Badge>
</Row>
) : null}
<Row title="Devcontainer" help="Whether .devcontainer setup is honored.">
<Toggle on={getBool(sandbox, "devcontainer") ?? false} />
</Row>

View file

@ -44,7 +44,6 @@ function sampleSettings({
project: {
name: null,
description: null,
directory: ".",
metadata: {},
},
workflow: {
@ -69,7 +68,6 @@ function sampleSettings({
stop_on_terminal: true,
devcontainer: true,
env: {},
local: { worktree_mode: "dirty" },
docker: null,
daytona: {
auto_stop_interval: autoStopInterval,
@ -86,7 +84,7 @@ function sampleSettings({
},
},
notifications: {},
interviews: { provider: null, slack: null, discord: null, teams: null },
interviews: { provider: null, slack: null },
agent: { permissions: null, mcps: {} },
hooks: [],
scm: { provider: null, owner: null, repository: null, github: null },

View file

@ -108,7 +108,7 @@ The new design must optimize for:
- R62. Sandbox config must remain provider-specific because provider differences are too large to hide behind one flat abstraction.
- R63. Model config must remain intentionally provider-neutral. It should not grow provider-specific subtables. `run.model.fallbacks` is a single ordered array of model references. Each entry may be a bare provider token such as `openai`, a bare model alias or model id such as `gpt-5.4`, or a qualified reference such as `gemini/gemini-flash`. Bare references are allowed only when unambiguous. Ambiguous bare references must hard-error and require qualification. A bare provider token means “choose the best matching model from that provider.”
- R64. SCM config must be provider-neutral at the core (`[run.scm]`) with room for provider-specific nested tables such as `[run.scm.github]` only where necessary.
- R65. Chat platforms such as Slack, Discord, and Teams are integrations. Their server-owned setup lives under `[server.integrations.<provider>]`; run behavior lives under `[run.notifications.*]` and `[run.interviews]`.
- R65. Slack is the chat integration. Its server-owned setup lives under `[server.integrations.slack]`; run behavior lives under `[run.notifications.*]` and `[run.interviews]`.
- R66. Object-store-backed domains must use a shared pattern: a small provider-neutral envelope plus provider-specific nested tables.
- R67. For local object-store providers, default to `server.storage.root`, but allow explicit local override roots when needed.

View file

@ -75,7 +75,7 @@ This refactor is centered on four seams:
- notification route surface first pass:
- route envelope fields are `enabled`, `provider`, and `events`
- provider-specific destination fields live under `[run.notifications.<name>.<provider>]`
- first-pass chat destinations for Slack, Discord, and Teams use `channel`
- first-pass Slack destinations use `channel`
- duration parser first pass:
- one shared parser accepts a single unit suffix per value: `ms`, `s`, `m`, `h`, or `d`
- composed values like `1h30m` are not supported in first pass; use the smallest needed unit instead

View file

@ -560,7 +560,7 @@ These are lessons learned during Stages 15. Save yourself the pain.
enumerated list of known-provider subfields instead (that's why
`RunSandboxLayer`, `NotificationRouteLayer`, `InterviewsLayer`,
`RunScmLayer`, `ServerIntegrationsLayer`, etc. have explicit
`github`/`slack`/`discord`/`teams`/`local`/`s3` fields). Adding a new
`github`/`slack`/`local`/`s3` fields). Adding a new
provider means adding a new field.
## Repo conventions you'll hit

View file

@ -4826,11 +4826,6 @@ components:
type: boolean
preserve_sandbox:
type: boolean
worktree_mode:
type: string
description: |
Override `run.sandbox.local.worktree_mode` (e.g. `never` for
`--in-place`).
label:
type: array
items:
@ -6073,7 +6068,6 @@ components:
- run_id
- settings
- graph
- in_place
properties:
run_id:
type: string
@ -6106,8 +6100,6 @@ components:
oneOf:
- $ref: "#/components/schemas/ForkSourceRef"
- type: "null"
in_place:
type: boolean
RunProjection:
description: Raw internal run projection derived from the event log.
@ -6217,8 +6209,6 @@ components:
type: string
source_directory:
type: ["string", "null"]
in_place:
type: boolean
repo_origin_url:
type: ["string", "null"]
repository:
@ -6902,8 +6892,6 @@ components:
type: string
source_directory:
type: ["string", "null"]
in_place:
type: boolean
repo_origin_url:
type: ["string", "null"]
start_time:
@ -7848,16 +7836,12 @@ components:
ServerIntegrationsSettings:
type: object
required: [github, slack, discord, teams]
required: [github, slack]
properties:
github:
$ref: "#/components/schemas/GithubIntegrationSettings"
slack:
$ref: "#/components/schemas/SlackIntegrationSettings"
discord:
$ref: "#/components/schemas/DiscordIntegrationSettings"
teams:
$ref: "#/components/schemas/TeamsIntegrationSettings"
GithubIntegrationSettings:
type: object
@ -7897,20 +7881,6 @@ components:
default_channel:
type: ["string", "null"]
DiscordIntegrationSettings:
type: object
required: [enabled]
properties:
enabled:
type: boolean
TeamsIntegrationSettings:
type: object
required: [enabled]
properties:
enabled:
type: boolean
IntegrationWebhooksSettings:
type: object
required: [strategy, ip_allowlist]
@ -7959,14 +7929,12 @@ components:
ProjectNamespace:
type: object
required: [name, description, directory, metadata]
required: [name, description, metadata]
properties:
name:
type: ["string", "null"]
description:
type: ["string", "null"]
directory:
type: string
metadata:
$ref: "#/components/schemas/StringMap"
@ -8169,7 +8137,7 @@ components:
RunSandboxSettings:
type: object
required: [provider, preserve, stop_on_terminal, devcontainer, env, local, docker, daytona]
required: [provider, preserve, stop_on_terminal, devcontainer, env, docker, daytona]
properties:
provider:
type: string
@ -8183,8 +8151,6 @@ components:
type: object
additionalProperties:
$ref: "#/components/schemas/InterpString"
local:
$ref: "#/components/schemas/LocalSandboxSettings"
docker:
oneOf:
- $ref: "#/components/schemas/DockerSettings"
@ -8194,17 +8160,6 @@ components:
- $ref: "#/components/schemas/DaytonaSettings"
- type: "null"
LocalSandboxSettings:
type: object
required: [worktree_mode]
properties:
worktree_mode:
$ref: "#/components/schemas/WorktreeMode"
WorktreeMode:
type: string
enum: [always, clean, dirty, never]
DockerSettings:
type: object
required: [image, network_mode, memory_limit, cpu_quota, env_vars, skip_clone]
@ -8310,7 +8265,7 @@ components:
NotificationRouteSettings:
type: object
required: [enabled, provider, events, slack, discord, teams]
required: [enabled, provider, events, slack]
properties:
enabled:
type: boolean
@ -8324,14 +8279,6 @@ components:
oneOf:
- $ref: "#/components/schemas/NotificationProviderSettings"
- type: "null"
discord:
oneOf:
- $ref: "#/components/schemas/NotificationProviderSettings"
- type: "null"
teams:
oneOf:
- $ref: "#/components/schemas/NotificationProviderSettings"
- type: "null"
NotificationProviderSettings:
type: object
@ -8342,7 +8289,7 @@ components:
RunInterviewsSettings:
type: object
required: [provider, slack, discord, teams]
required: [provider, slack]
properties:
provider:
type: ["string", "null"]
@ -8350,14 +8297,6 @@ components:
oneOf:
- $ref: "#/components/schemas/InterviewProviderSettings"
- type: "null"
discord:
oneOf:
- $ref: "#/components/schemas/InterviewProviderSettings"
- type: "null"
teams:
oneOf:
- $ref: "#/components/schemas/InterviewProviderSettings"
- type: "null"
InterviewProviderSettings:
type: object

View file

@ -232,18 +232,9 @@ dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update"
| `network` | Network access mode: `"allow_all"` (default), `"block"`, or `{ allow_list = ["..."] }`. See [Sandboxing](/administration/sandboxing#network-access-control). |
| `skip_clone` | When `true`, start with an empty Daytona workspace instead of cloning the run's GitHub origin. |
#### `[run.sandbox.local]`
#### Local sandbox
Additional settings when using the local sandbox:
```toml title="run.toml"
[run.sandbox.local]
worktree_mode = "always"
```
| Field | Description |
|---|---|
| `worktree_mode` | Legacy compatibility setting. Local Git runs use a run-scoped worktree by default; the explicit in-place/no-checkpoints path is the supported opt-out. |
When `run.sandbox.provider = "local"`, Fabro runs directly in the resolved working directory. If you want local isolation, create or enter a separate clone or Git worktree yourself, then run with `--sandbox local`.
#### `[run.sandbox.env]`

View file

@ -303,7 +303,6 @@ fabro create [OPTIONS] <WORKFLOW>
| `--dry-run` | Execute with simulated LLM backend |
| `--goal <goal>` | Override the workflow goal (available as {{ goal }} in prompts) |
| `--goal-file <goal_file>` | Read the workflow goal from a file |
| `--in-place` | Run directly in the source checkout without git checkpoints |
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
| `--model <model>` | Override default LLM model |
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
@ -841,7 +840,6 @@ fabro run [OPTIONS] <WORKFLOW>
| `--dry-run` | Execute with simulated LLM backend |
| `--goal <goal>` | Override the workflow goal (available as {{ goal }} in prompts) |
| `--goal-file <goal_file>` | Read the workflow goal from a file |
| `--in-place` | Run directly in the source checkout without git checkpoints |
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
| `--model <model>` | Override default LLM model |
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |

View file

@ -1,33 +1,31 @@
# Fixed Project Workflow Directory Plan
**Summary**
Remove the `[project].directory` feature entirely so project workflows are always discovered, created, listed, and resolved from `<repo_root>/.fabro/workflows/*`. Because this is greenfield, configs that still contain `[project] directory = ...` should fail schema validation instead of being ignored or migrated. User-level workflows under `~/.fabro/workflows` remain unchanged as an additional fallback/list section.
Remove the `[project].directory` feature as an effective setting so project workflows are always discovered, created, listed, and resolved from `<repo_root>/.fabro/workflows/*`. Configs that still contain `[project] directory = ...` should continue parsing, but the field is deprecated and ignored. User-level workflows under `~/.fabro/workflows` remain unchanged as an additional fallback/list section.
**Key Changes**
- Remove `directory` from the project config schema and resolved settings type:
- Delete `ProjectLayer::directory` and `ProjectNamespace::directory`.
- Remove `directory` from the resolved settings/API shape while tolerating old config files:
- Keep `ProjectLayer::directory` as a deprecated parse-only field so existing `project.toml` files with the field do not fail schema validation.
- Delete `ProjectNamespace::directory`.
- Remove `[project] directory = "."` from built-in defaults.
- Update project resolver/tests so `[project]` only carries `name`, `description`, and `metadata`.
- Update project resolver, `fabro-types` fixtures, OpenAPI schema, and generated TypeScript client so resolved `[project]` settings only carry `name`, `description`, and `metadata`.
- Make project Fabro root fixed:
- Change `resolve_fabro_root` to validate `.fabro/project.toml`, then return `config_path.parent()` directly.
- Update callers to handle `Result<PathBuf>` and use `<discovered .fabro>/workflows`.
- Delete now-unused path normalization code for joining `project.directory`.
- Update public API shape:
- Remove `directory` from `ProjectNamespace` in `docs/public/api-reference/fabro-api.yaml`.
- Update `workflow_settings_round_trip` expectations.
- Regenerate `lib/packages/fabro-api-client` so TypeScript `ProjectNamespace` no longer has `directory`.
- Delete `resolve_fabro_root`; it only exists to apply `project.directory`.
- Delete `load_project_config` and project-root path normalization code if they have no remaining callers after `resolve_fabro_root` is removed.
- At workflow discovery/create/list call sites, use the discovered config path's parent directory as the Fabro root and append `workflows`.
- Do not add a new project-config validation step for workflow directory discovery; settings validation remains the responsibility of settings-loading paths.
- Update docs and hints:
- Remove references to `project.directory` and the old `fabro.root -> project.directory` rename hint.
- Remove public references to `project.directory` and replace the old `fabro.root -> project.directory` rename hint with guidance that project workflows now live under `.fabro/workflows`.
- Add/keep explicit docs that project workflows live at `<repo_root>/.fabro/workflows/<name>/`.
**Test Plan**
- Config tests:
- Empty settings still resolve project metadata defaults.
- `[project] directory = "..."` is rejected as an unknown field.
- `resolve_fabro_root` always returns the `.fabro` directory containing `project.toml`.
- `[project] directory = "..."` still parses but is ignored in resolved settings.
- Project workflow root calculation uses the `.fabro` directory containing `project.toml`, regardless of any deprecated `project.directory` value.
- CLI integration tests:
- Replace custom-root workflow create/list tests with fixed-root assertions.
- Confirm `fabro workflow create <name>` writes `.fabro/workflows/<name>/workflow.{fabro,toml}`.
- Confirm `fabro workflow create <name>` writes both `.fabro/workflows/<name>/workflow.fabro` and `.fabro/workflows/<name>/workflow.toml`.
- Confirm named workflow resolution reads `.fabro/workflows/<name>/workflow.toml`.
- API/client checks:
- `cargo build -p fabro-api`
@ -36,6 +34,6 @@ Remove the `[project].directory` feature entirely so project workflows are alway
- `cd apps/fabro-web && bun run typecheck`
**Assumptions**
- No backwards compatibility or migration period: old configs with `[project].directory` should fail.
- Backwards compatibility is parse-only: old configs with `[project].directory` should not fail, but the value has no effect and is not exposed in resolved settings or the API.
- This change only fixes the project workflow directory. User workflows remain available unless a separate decision removes them later.
- GitHub retrieval itself is out of scope here; this refactor makes the repo path deterministic for that future work.

View file

@ -48,14 +48,14 @@ Out of scope: new explicit branching/committing settings. Local direct runs shou
- Modify: `lib/crates/fabro-cli/src/args.rs`
- Modify: `lib/crates/fabro-cli/src/manifest_builder.rs`
- [ ] Remove `WorktreeMode`, `LocalSandboxSettings`, and the `RunSandboxSettings.local` field.
- [ ] Remove the `[run.sandbox.local] worktree_mode = "always"` default.
- [ ] Remove config-layer parsing/resolution for `[run.sandbox.local]` entirely.
- [ ] Remove `RunArgs::in_place`.
- [ ] Make `run_manifest_args` set `sandbox` only from `--sandbox`; do not synthesize local sandbox or `worktree_mode`.
- [ ] Make `preflight_manifest_args` stop carrying `worktree_mode`.
- [ ] Update compile errors in config tests and CLI tests by removing assertions that mention `WorktreeMode`, `worktree_mode`, or `--in-place`.
- [ ] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical matches related to config and CLI knobs.
- [x] Remove `WorktreeMode`, `LocalSandboxSettings`, and the `RunSandboxSettings.local` field.
- [x] Remove the `[run.sandbox.local] worktree_mode = "always"` default.
- [x] Remove config-layer parsing/resolution for `[run.sandbox.local]` entirely.
- [x] Remove `RunArgs::in_place`.
- [x] Make `run_manifest_args` set `sandbox` only from `--sandbox`; do not synthesize local sandbox or `worktree_mode`.
- [x] Make `preflight_manifest_args` stop carrying `worktree_mode`.
- [x] Update compile errors in config tests and CLI tests by removing assertions that mention `WorktreeMode`, `worktree_mode`, or `--in-place`.
- [x] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical matches related to config and CLI knobs.
### Task 2: Remove `in_place` From Run State and API Surfaces
@ -68,14 +68,14 @@ Out of scope: new explicit branching/committing settings. Local direct runs shou
- Modify: `lib/crates/fabro-server/src/run_manifest.rs`
- Modify: `docs/public/api-reference/fabro-api.yaml`
- [ ] Remove `in_place` from `RunSpec` and all run-created/run-summary event props.
- [ ] Remove `PreparedManifest.in_place`; local/direct behavior is now implied by `settings.run.sandbox.provider == "local"`.
- [ ] Remove `CreateRunInput.in_place` and `PersistCreateOptions.in_place`.
- [ ] Remove server and CLI code that serializes, displays, or filters by `in_place`.
- [ ] Remove `in_place` from the OpenAPI schema.
- [ ] Regenerate Rust API types with `cargo build -p fabro-api`.
- [ ] Regenerate TypeScript client with `cd lib/packages/fabro-api-client && bun run generate`.
- [ ] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical matches in event conversion, persistence, runtime store/test fixtures, run metadata, billing rollups, CLI run listing/server run wrappers, server demo data, and generated-schema call sites.
- [x] Remove `in_place` from `RunSpec` and all run-created/run-summary event props.
- [x] Remove `PreparedManifest.in_place`; local/direct behavior is now implied by `settings.run.sandbox.provider == "local"`.
- [x] Remove `CreateRunInput.in_place` and `PersistCreateOptions.in_place`.
- [x] Remove server and CLI code that serializes, displays, or filters by `in_place`.
- [x] Remove `in_place` from the OpenAPI schema.
- [x] Regenerate Rust API types with `cargo build -p fabro-api`.
- [x] Regenerate TypeScript client with `cd lib/packages/fabro-api-client && bun run generate`.
- [x] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical matches in event conversion, persistence, runtime store/test fixtures, run metadata, billing rollups, CLI run listing/server run wrappers, server demo data, and generated-schema call sites.
### Task 3: Simplify Workflow Initialization
@ -85,16 +85,16 @@ Out of scope: new explicit branching/committing settings. Local direct runs shou
- Modify: `lib/crates/fabro-workflow/src/operations/start.rs`
- Modify: `lib/crates/fabro-workflow/src/operations/fork.rs`
- [ ] Extract provider-specific dirty-worktree warning logic before deleting worktree planning: Docker/Daytona warn that uncommitted local changes are not included in the remote sandbox; local direct runs do not warn.
- [ ] Remove `worktree_mode` from initialization options.
- [ ] Delete `WorktreePlan`, `resolve_worktree_plan`, `resolve_worktree_base_sha`, and `worktree_skipped_notice`.
- [ ] Remove local `WorktreeSandbox` wrapping from `initialize`.
- [ ] Keep existing generic wrappers such as `ReadBeforeWriteSandbox`; this change only removes Fabro-managed git worktree materialization for local runs.
- [ ] Keep attach/resume reconnection for existing sandboxes.
- [ ] Keep normal sandbox build/initialize flow for new runs.
- [ ] Keep the generic `sandbox.setup_git(...)` block after sandbox initialization. This will continue to work for Docker/Daytona and continue to no-op for local.
- [ ] Remove fork validation that rejects `spec.in_place`; fork should now fail based on real missing prerequisites such as empty/missing checkpoint git SHA or missing repo origin.
- [ ] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical matches related to initialize, fork, and resume.
- [x] Extract provider-specific dirty-worktree warning logic before deleting worktree planning: Docker/Daytona warn that uncommitted local changes are not included in the remote sandbox; local direct runs do not warn.
- [x] Remove `worktree_mode` from initialization options.
- [x] Delete `WorktreePlan`, `resolve_worktree_plan`, `resolve_worktree_base_sha`, and `worktree_skipped_notice`.
- [x] Remove local `WorktreeSandbox` wrapping from `initialize`.
- [x] Keep existing generic wrappers such as `ReadBeforeWriteSandbox`; this change only removes Fabro-managed git worktree materialization for local runs.
- [x] Keep attach/resume reconnection for existing sandboxes.
- [x] Keep normal sandbox build/initialize flow for new runs.
- [x] Keep the generic `sandbox.setup_git(...)` block after sandbox initialization. This will continue to work for Docker/Daytona and continue to no-op for local.
- [x] Remove fork validation that rejects `spec.in_place`; fork should now fail based on real missing prerequisites such as empty/missing checkpoint git SHA or missing repo origin.
- [x] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical matches related to initialize, fork, and resume.
### Task 4: Keep Parallel Worktrees Intact
@ -102,10 +102,10 @@ Out of scope: new explicit branching/committing settings. Local direct runs shou
- Modify only if compile errors require it: `lib/crates/fabro-workflow/src/handler/parallel.rs`
- Do not delete: `lib/crates/fabro-sandbox/src/worktree.rs`
- [ ] Verify `WorktreeSandbox` remains available to parallel-node code.
- [ ] Remove only imports that were used exclusively by local-run worktree setup.
- [ ] Ensure parallel branch tests still create isolated branch worktrees and fan back in as before.
- [ ] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and confirm remaining worktree references are only parallel-node internals or historical docs intentionally kept.
- [x] Verify `WorktreeSandbox` remains available to parallel-node code.
- [x] Remove only imports that were used exclusively by local-run worktree setup.
- [x] Ensure parallel branch tests still create isolated branch worktrees and fan back in as before.
- [x] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and confirm remaining worktree references are only parallel-node internals or historical docs intentionally kept.
### Task 5: Update Docs and UI
@ -115,53 +115,53 @@ Out of scope: new explicit branching/committing settings. Local direct runs shou
- Modify: `apps/fabro-web/app/routes/run-settings.tsx`
- Modify: `apps/fabro-web/app/routes/workflow-detail.tsx`
- [ ] Document local sandbox semantics as direct execution in the resolved working directory.
- [ ] Remove `--in-place` from CLI reference docs.
- [ ] Remove `[run.sandbox.local] worktree_mode` docs.
- [ ] Remove UI rendering/seed data references to `worktree_mode`.
- [ ] Keep docs clear that user-managed local isolation is done by entering a separate clone/worktree and running with `--sandbox local`.
- [ ] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical docs/UI matches.
- [x] Document local sandbox semantics as direct execution in the resolved working directory.
- [x] Remove `--in-place` from CLI reference docs.
- [x] Remove `[run.sandbox.local] worktree_mode` docs.
- [x] Remove UI rendering/seed data references to `worktree_mode`.
- [x] Keep docs clear that user-managed local isolation is done by entering a separate clone/worktree and running with `--sandbox local`.
- [x] Run `rg -n "worktree_mode|WorktreeMode|--in-place|in_place" lib apps/fabro-web/app docs/public -g '!lib/packages/fabro-api-client/src/**' -g '!lib/crates/fabro-spa/assets/**'` and remove newly exposed non-historical docs/UI matches.
## Test Plan
- [ ] Run config/default tests:
- [x] Run config/default tests:
```bash
cargo nextest run -p fabro-config
```
- [ ] Run CLI manifest and command help tests:
- [x] Run CLI manifest and command help tests:
```bash
cargo nextest run -p fabro-cli
```
- [ ] Run workflow tests covering initialize, fork, checkpointing, parallel worktrees, and PR prerequisites:
- [x] Run workflow tests covering initialize, fork, checkpointing, parallel worktrees, and PR prerequisites:
```bash
cargo nextest run -p fabro-workflow
```
- [ ] Run server API tests for manifests, run summaries, PR endpoints, and OpenAPI conformance:
- [x] Run server API tests for manifests, run summaries, PR endpoints, and OpenAPI conformance:
```bash
cargo nextest run -p fabro-server
```
- [ ] Regenerate and verify API clients:
- [x] Regenerate and verify API clients:
```bash
cargo build -p fabro-api
cd lib/packages/fabro-api-client && bun run generate
```
- [ ] Typecheck web UI after generated-client and settings-shape changes:
- [x] Typecheck web UI after generated-client and settings-shape changes:
```bash
cd apps/fabro-web && bun run typecheck
```
- [ ] Run formatting and lint checks:
- [x] Run formatting and lint checks:
```bash
cargo +nightly-2026-04-14 fmt --check --all

View file

@ -315,16 +315,6 @@ fn main() {
"fabro_types::settings::server::SlackIntegrationSettings",
&[],
),
(
"DiscordIntegrationSettings",
"fabro_types::settings::server::DiscordIntegrationSettings",
&[],
),
(
"TeamsIntegrationSettings",
"fabro_types::settings::server::TeamsIntegrationSettings",
&[],
),
(
"IntegrationWebhooksSettings",
"fabro_types::settings::server::IntegrationWebhooksSettings",

View file

@ -16,13 +16,13 @@ mod generated {
pub mod types {
pub use fabro_model::{Model, ModelCosts, ModelFeatures, ModelLimits, ModelTestMode, Provider};
pub use fabro_types::settings::server::{
DiscordIntegrationSettings, GithubIntegrationSettings, GithubIntegrationStrategy,
IntegrationWebhooksSettings, IpAllowEntry, LogDestination, ObjectStoreSettings,
ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod,
ServerAuthSettings, ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings,
ServerIpAllowlistSettings, ServerListenSettings, ServerLoggingSettings,
ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings,
SlackIntegrationSettings, TeamsIntegrationSettings, WebhookStrategy,
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
IpAllowEntry, LogDestination, ObjectStoreSettings, ServerApiSettings,
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,
ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings,
ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
WebhookStrategy,
};
pub use fabro_types::settings::{FeaturesNamespace, ServerNamespace};
pub use fabro_types::status::{

View file

@ -20,8 +20,7 @@ fn run_event_round_trips_run_created() {
"settings": WorkflowSettings::default(),
"graph": Graph::new("test"),
"run_dir": "/tmp/fabro/run-1",
"source_directory": "/tmp/fabro/run-1",
"in_place": false
"source_directory": "/tmp/fabro/run-1"
}
});
@ -40,7 +39,6 @@ fn run_event_round_trips_run_created_with_web_url() {
"graph": Graph::new("test"),
"run_dir": "/tmp/fabro/run-1",
"source_directory": "/tmp/fabro/run-1",
"in_place": false,
"web_url": format!("http://localhost:3000/runs/{}", fixtures::RUN_1)
}
});

View file

@ -29,7 +29,6 @@ fn run_summary_json_matches_openapi_shape() {
"API title".to_string(),
HashMap::from([("team".to_string(), "core".to_string())]),
Some("/tmp/fabro".to_string()),
false,
None,
Some(created_at),
Some(last_event_at),
@ -70,7 +69,6 @@ fn run_summary_json_matches_openapi_shape() {
"team": "core"
},
"source_directory": "/tmp/fabro",
"in_place": false,
"repo_origin_url": null,
"repository": {
"name": "fabro"

View file

@ -32,7 +32,10 @@ approval = "auto"
.expect("settings should resolve");
let json = serde_json::to_value(&settings).expect("workflow settings should serialize");
assert_eq!(json["project"]["directory"], "workspace");
assert!(
json["project"].get("directory").is_none(),
"resolved project settings should not expose deprecated directory"
);
assert_eq!(json["workflow"]["graph"], "ship.fabro");
assert_eq!(json["run"]["goal"]["type"], "inline");
assert_eq!(json["run"]["goal"]["value"], "Ship it");

View file

@ -246,10 +246,6 @@ pub(crate) struct RunArgs {
#[arg(long, value_enum)]
pub(crate) sandbox: Option<CliSandboxProvider>,
/// Run directly in the source checkout without git checkpoints
#[arg(long, conflicts_with = "sandbox")]
pub(crate) in_place: bool,
/// Attach a label to this run (repeatable, format: KEY=VALUE)
#[arg(long = "label", value_name = "KEY=VALUE")]
pub(crate) label: Vec<String>,

View file

@ -46,7 +46,6 @@ pub(crate) async fn list_command(
"total_usd_micros": run.total_usd_micros(),
"source_directory": run.source_directory(),
"repo_origin_url": run.repo_origin_url(),
"in_place": run.in_place(),
"goal": run.goal(),
})
})
@ -83,7 +82,6 @@ pub(crate) async fn list_command(
"RUN ID".cell().bold(use_color),
"WORKFLOW".cell().bold(use_color),
"STATUS".cell().bold(use_color),
"IN-PLACE".cell().bold(use_color),
"DIRECTORY".cell().bold(use_color),
"DURATION".cell().bold(use_color),
"GOAL".cell().bold(use_color),
@ -113,7 +111,6 @@ pub(crate) async fn list_command(
.foreground_color(color_if(use_color, Color::Ansi256(8))),
run.workflow_name().cell(),
status_cell(run.status(), use_color),
in_place_cell(run.in_place(), use_color),
dir_display.cell(),
duration_display.cell(),
truncate_goal(&run.goal(), 50)
@ -140,16 +137,6 @@ pub(crate) async fn list_command(
Ok(())
}
fn in_place_cell(in_place: bool, use_color: bool) -> CellStruct {
if in_place {
return "yes"
.cell()
.foreground_color(color_if(use_color, Color::Yellow));
}
"no".cell()
.foreground_color(color_if(use_color, Color::Ansi256(8)))
}
fn status_cell(status: RunStatus, use_color: bool) -> CellStruct {
let text = run_status_kind(status);
let color = match status {

View file

@ -6,7 +6,7 @@
use std::path::Path;
use anyhow::{Context, Result, bail};
use fabro_config::project::{discover_project_config, resolve_fabro_root};
use fabro_config::project::discover_project_config;
use crate::args::WorkflowCreateArgs;
use crate::command_context::CommandContext;
@ -23,8 +23,10 @@ pub(super) fn create_command(args: &WorkflowCreateArgs, base_ctx: &CommandContex
);
};
let fabro_root = resolve_fabro_root(&config_path);
let created = write_workflow_scaffold(args, &fabro_root)?;
let fabro_root = config_path
.parent()
.expect("project config should have a parent directory");
let created = write_workflow_scaffold(args, fabro_root)?;
if base_ctx.json_output() {
let created: Vec<_> = created.iter().map(|path| relative_path(path)).collect();

View file

@ -3,7 +3,6 @@ use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_config::project::{
WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed,
resolve_fabro_root,
};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -26,7 +25,9 @@ pub(super) fn list_command(_args: &WorkflowListArgs, base_ctx: &CommandContext)
);
};
let fabro_root = resolve_fabro_root(&config_path);
let fabro_root = config_path
.parent()
.expect("project config should have a parent directory");
let project_wf_dir = fabro_root.join("workflows");
let user_wf_dir = Some(fabro_util::Home::from_env().workflows_dir());

View file

@ -182,15 +182,10 @@ pub(crate) fn run_manifest_args(args: &RunArgs) -> Option<types::ManifestArgs> {
provider: args.provider.clone(),
sandbox: args
.sandbox
.map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string())
.or_else(|| {
args.in_place
.then(|| fabro_sandbox::SandboxProvider::Local.to_string())
}),
.map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()),
docker_image: None,
input: args.inputs.values.clone(),
verbose: args.verbose.then_some(true),
worktree_mode: args.in_place.then(|| "never".to_string()),
};
(!manifest_args_is_empty(&payload)).then_some(payload)
}
@ -209,7 +204,6 @@ pub(crate) fn preflight_manifest_args(args: &PreflightArgs) -> Option<types::Man
docker_image: None,
input: args.inputs.values.clone(),
verbose: args.verbose.then_some(true),
worktree_mode: None,
};
(!manifest_args_is_empty(&payload)).then_some(payload)
}
@ -667,7 +661,6 @@ fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool {
&& args.docker_image.is_none()
&& args.input.is_empty()
&& args.verbose.is_none()
&& args.worktree_mode.is_none()
}
#[cfg(test)]

View file

@ -68,10 +68,6 @@ impl ServerRunSummaryInfo {
self.summary.repo_origin_url.as_deref()
}
pub(crate) fn in_place(&self) -> bool {
self.summary.in_place
}
pub(crate) fn goal(&self) -> String {
self.summary.goal.clone()
}

View file

@ -353,7 +353,6 @@ fn attach_replays_completed_detached_run() {
----- stdout -----
----- stderr -----
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
Start [TIME]
Run Tests [TIME]
@ -598,7 +597,6 @@ fn attach_before_completion_streams_to_finished_state() {
----- stdout -----
----- stderr -----
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
start [DURATION]
wait [DURATION]
@ -890,7 +888,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
}
}
},
"in_place": false,
"manifest_blob": "[BLOB_ID]",
"provenance": {
"client": {
@ -915,7 +912,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
"settings": {
"project": {
"description": null,
"directory": ".",
"metadata": {},
"name": null
},
@ -949,10 +945,8 @@ fn attach_json_errors_without_prompting_for_human_input() {
}
},
"interviews": {
"discord": null,
"provider": null,
"slack": null,
"teams": null
"slack": null
},
"metadata": {},
"model": {
@ -978,9 +972,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
"skip_clone": false
},
"env": {},
"local": {
"worktree_mode": "always"
},
"preserve": false,
"provider": "local",
"stop_on_terminal": true
@ -1036,21 +1027,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"actor": {
"kind": "worker",
"run_id": "[ULID]"
},
"event": "run.notice",
"id": "[EVENT_ID]",
"properties": {
"code": "worktree_skipped_no_git",
"level": "warn",
"message": "Worktree mode `always` requested but no Git repository was found; running without a worktree."
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"actor": {
"kind": "worker",

View file

@ -54,7 +54,6 @@ fn help() {
--provider <PROVIDER> Override default LLM provider
-v, --verbose Enable verbose output
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
--in-place Run directly in the source checkout without git checkpoints
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)
--preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)
-d, --detach Run the workflow in the background and print the run ID

View file

@ -181,9 +181,6 @@ goal = "Generate oversized command output and artifacts"
provider = "local"
preserve = true
[run.sandbox.local]
worktree_mode = "never"
[run.artifacts]
include = ["assets/**"]
"#,
@ -271,9 +268,9 @@ fn dump_exports_completed_run_snapshot() {
");
assert_snapshot!(dump_file_summary(&output_dir), @"
checkpoints/0014.json
checkpoints/0018.json
checkpoints/0022.json
checkpoints/0013.json
checkpoints/0017.json
checkpoints/0021.json
events.jsonl
graph.fabro
run.json

View file

@ -150,7 +150,6 @@ fn help() {
--provider <PROVIDER> Override default LLM provider
-v, --verbose Enable verbose output
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
--in-place Run directly in the source checkout without git checkpoints
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)
--preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)
-d, --detach Run the workflow in the background and print the run ID
@ -670,9 +669,6 @@ goal = "Show stored artifacts"
provider = "local"
preserve = true
[run.sandbox.local]
worktree_mode = "never"
[run.artifacts]
include = ["assets/**"]
"#,
@ -728,7 +724,6 @@ fn dry_run_simple() {
Run: [ULID]
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
Start [TIME]
Run Tests [TIME]

View file

@ -349,8 +349,6 @@ goal = "Exercise sandbox commands"
provider = "local"
preserve = true
[run.sandbox.local]
worktree_mode = "never"
"#,
);

View file

@ -39,7 +39,7 @@ fn list() {
"_version = 1\n\n[project]\ndirectory = \"..\"\n",
)
.write_temp(
"workflows/my_test_wf/workflow.toml",
".fabro/workflows/my_test_wf/workflow.toml",
"_version = 1\n\n[run]\ngoal = \"A test workflow\"\n",
);
@ -55,7 +55,7 @@ fn list() {
User Workflows (~/.fabro/workflows)
(none)
Project Workflows (workflows)
Project Workflows (.fabro/workflows)
NAME DESCRIPTION
my_test_wf A test workflow
");

View file

@ -160,7 +160,7 @@ fn workflow_create_errors_without_project_config() {
}
#[test]
fn workflow_create_json_uses_resolved_custom_root_paths() {
fn workflow_create_json_ignores_deprecated_project_directory() {
let context = test_context!();
let project_dir = context.temp_dir.join("project");
context.write_temp(
@ -188,20 +188,24 @@ fn workflow_create_json_uses_resolved_custom_root_paths() {
{
"name": "hello-world",
"created": [
"custom/fabro-data/workflows/hello-world/workflow.fabro",
"custom/fabro-data/workflows/hello-world/workflow.toml"
".fabro/workflows/hello-world/workflow.fabro",
".fabro/workflows/hello-world/workflow.toml"
]
}
"#);
assert!(
project_dir
.join("custom/fabro-data/workflows/hello-world/workflow.fabro")
.join(".fabro/workflows/hello-world/workflow.fabro")
.exists()
);
assert!(
project_dir
.join("custom/fabro-data/workflows/hello-world/workflow.toml")
.join(".fabro/workflows/hello-world/workflow.toml")
.exists()
);
assert!(
!project_dir.join("custom/fabro-data/workflows").exists(),
"deprecated project.directory should not redirect workflow creation"
);
}

View file

@ -21,7 +21,6 @@ fn dry_run_branching() {
warning [node: implement]: Node 'implement' has goal_gate=true but no retry_target or fallback_retry_target (goal_gate_has_retry)
Run: [ULID]
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
Start [TIME]
Plan [TIME]
@ -58,7 +57,6 @@ fn dry_run_conditions() {
Run: [ULID]
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
start [TIME]
Decide [TIME]
@ -93,7 +91,6 @@ fn dry_run_parallel() {
Run: [ULID]
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
start [TIME]
Fork Work [TIME]
@ -129,7 +126,6 @@ fn dry_run_styled() {
Run: [ULID]
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
start [TIME]
Plan [TIME]
@ -165,7 +161,6 @@ fn dry_run_legacy_tool() {
Run: [ULID]
Web UI: http://localhost:3000/runs/[ULID]
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
Sandbox: local (ready in [TIME])
Start [TIME]
Echo [TIME]

View file

@ -1,7 +1,7 @@
use std::fmt;
use std::path::Path;
use fabro_types::settings::{ProjectNamespace, RunNamespace, WorkflowNamespace};
use fabro_types::settings::{RunNamespace, WorkflowNamespace};
use fabro_types::{ServerSettings, UserSettings, WorkflowSettings};
use fabro_util::error::SharedError;
@ -419,15 +419,6 @@ impl WorkflowSettingsBuilder {
)
}
pub(crate) fn project_from_layer(
layer: &SettingsLayer,
) -> std::result::Result<ProjectNamespace, ResolveErrors> {
let layer = layer.clone().combine(DEFAULTS_LAYER.clone());
let mut errors = Vec::new();
let project = resolve_project(&layer.project.clone().unwrap_or_default(), &mut errors);
finish_dense_result(project, errors)
}
pub(crate) fn workflow_from_layer(
layer: &SettingsLayer,
) -> std::result::Result<WorkflowNamespace, ResolveErrors> {

View file

@ -2,9 +2,6 @@
# Dynamic and presence-gated defaults remain in Rust.
_version = 1
[project]
directory = "."
[workflow]
graph = "workflow.fabro"
@ -21,9 +18,6 @@ preserve = false
stop_on_terminal = true
devcontainer = false
[run.sandbox.local]
worktree_mode = "always"
[run.sandbox.docker]
image = "buildpack-deps:noble"
memory_limit = "4GB"

View file

@ -2,7 +2,7 @@ use std::collections::HashMap;
use fabro_types::settings::cli::{CliAuthStrategy, OutputFormat, OutputVerbosity};
use fabro_types::settings::run::{
AgentPermissions, ApprovalMode, DaytonaNetworkLayer, MergeStrategy, RunMode, WorktreeMode,
AgentPermissions, ApprovalMode, DaytonaNetworkLayer, MergeStrategy, RunMode,
};
use fabro_types::settings::server::{
GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod,
@ -15,8 +15,8 @@ use super::cli::{CliAuthLayer, CliLoggingLayer, CliTargetLayer};
use super::features::FeaturesLayer;
use super::run::{
DaytonaSnapshotLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
LocalSandboxLayer, ModelRefOrSplice, NotificationProviderLayer, RunArtifactsLayer,
RunCheckpointLayer, RunGoalLayer, RunPrepareLayer, ScmGitHubLayer, StringOrSplice,
ModelRefOrSplice, NotificationProviderLayer, RunArtifactsLayer, RunCheckpointLayer,
RunGoalLayer, RunPrepareLayer, ScmGitHubLayer, StringOrSplice,
};
use super::server::{
ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer, ServerAuthGithubLayer,
@ -75,7 +75,6 @@ impl_combine_or_option!(
HookTlsMode,
MergeStrategy,
RunMode,
WorktreeMode,
GithubIntegrationStrategy,
LogDestination,
ObjectStoreProvider,
@ -122,7 +121,6 @@ impl_combine_self!(
DaytonaNetworkLayer,
DaytonaSnapshotLayer,
InterviewProviderLayer,
LocalSandboxLayer,
NotificationProviderLayer,
RunArtifactsLayer,
RunGoalLayer,
@ -282,7 +280,6 @@ mod tests {
assert_option_leaf(HookTlsMode::NoVerify, HookTlsMode::Verify);
assert_option_leaf(MergeStrategy::Rebase, MergeStrategy::Squash);
assert_option_leaf(RunMode::DryRun, RunMode::Normal);
assert_option_leaf(WorktreeMode::Always, WorktreeMode::Never);
assert_option_leaf(
GithubIntegrationStrategy::App,
GithubIntegrationStrategy::Token,

View file

@ -22,19 +22,18 @@ pub use project::ProjectLayer;
pub use run::{
DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, DockerSandboxLayer,
GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
InterviewsLayer, LocalSandboxLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer,
InterviewsLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer,
NotificationRouteLayer, PrepareStep, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer,
RunLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunSandboxLayer, RunScmLayer,
ScmGitHubLayer, StringOrSplice,
};
pub use server::{
DiscordIntegrationLayer, GithubIntegrationLayer, IntegrationWebhooksLayer,
ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer, ServerArtifactsLayer,
ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,
ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,
SlackIntegrationLayer, TeamsIntegrationLayer,
GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,
ServerIntegrationsLayer, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerLayer,
ServerListenLayer, ServerLoggingLayer, ServerSchedulerLayer, ServerSlateDbLayer,
ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer,
};
pub(crate) use settings::SettingsLayer;
pub use workflow::WorkflowLayer;

View file

@ -12,8 +12,8 @@ pub struct ProjectLayer {
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The Fabro-managed project directory inside the repo. Defaults to
/// `.` after layering when unspecified.
/// Deprecated parse-only field. Project workflows always live under the
/// `.fabro` directory that contains `project.toml`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
#[serde(default, skip_serializing_if = "ReplaceMap::is_empty")]

View file

@ -4,7 +4,6 @@ use std::collections::HashMap;
use fabro_types::settings::run::{
AgentPermissions, ApprovalMode, DaytonaNetworkLayer, HookEvent, MergeStrategy, RunMode,
WorktreeMode,
};
use fabro_types::settings::{Duration, InterpString, ModelRef, Size};
use serde::{Deserialize, Serialize};
@ -264,20 +263,11 @@ pub struct RunSandboxLayer {
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
pub env: StickyMap<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<LocalSandboxLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub docker: Option<DockerSandboxLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daytona: Option<DaytonaSandboxLayer>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalSandboxLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree_mode: Option<WorktreeMode>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct DockerSandboxLayer {
@ -344,13 +334,9 @@ pub struct NotificationRouteLayer {
/// Raw Fabro event names. Splice marker supported at layering time.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<StringOrSplice>,
/// Provider-specific destination subtables. First-pass chat providers.
/// Provider-specific destination subtables.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slack: Option<NotificationProviderLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discord: Option<NotificationProviderLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub teams: Option<NotificationProviderLayer>,
}
/// A single string array entry that may be the splice marker.
@ -396,10 +382,6 @@ pub struct InterviewsLayer {
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slack: Option<InterviewProviderLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discord: Option<InterviewProviderLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub teams: Option<InterviewProviderLayer>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]

View file

@ -189,21 +189,15 @@ pub struct ServerLoggingLayer {
pub destination: Option<LogDestination>,
}
/// `[server.integrations.<provider>]` — cohesive integration surface for chat
/// platforms and git providers (GitHub App, webhooks, etc.). First-pass
/// integrations enumerate known providers rather than using a flatten-HashMap
/// shape so strict unknown-field validation still holds.
/// `[server.integrations.<provider>]` — cohesive integration surface for Slack
/// and git providers (GitHub App, webhooks, etc.).
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct ServerIntegrationsLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub github: Option<GithubIntegrationLayer>,
pub github: Option<GithubIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slack: Option<SlackIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discord: Option<DiscordIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub teams: Option<TeamsIntegrationLayer>,
pub slack: Option<SlackIntegrationLayer>,
}
/// `[server.integrations.github]` — GitHub App, credentials, and inbound
@ -235,22 +229,6 @@ pub struct SlackIntegrationLayer {
pub default_channel: Option<InterpString>,
}
/// `[server.integrations.discord]` — Discord workspace configuration.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct DiscordIntegrationLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
/// `[server.integrations.teams]` — Microsoft Teams configuration.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct TeamsIntegrationLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct IntegrationWebhooksLayer {

View file

@ -40,19 +40,18 @@ pub use input_overrides::{InputOverrideParseError, parse_input_overrides};
pub use layers::{
CliAuthLayer, CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliLoggingLayer,
CliOutputLayer, CliTargetLayer, CliUpdatesLayer, DaytonaDockerfileLayer, DaytonaSandboxLayer,
DaytonaSnapshotLayer, DiscordIntegrationLayer, DockerSandboxLayer, FeaturesLayer,
GitAuthorLayer, GithubIntegrationLayer, HookAgentMarker, HookEntry, HookTlsMode,
IntegrationWebhooksLayer, InterviewProviderLayer, InterviewsLayer, LocalSandboxLayer,
LogFilter, McpEntryLayer, MergeMap, ModelRefOrSplice, NotificationProviderLayer,
NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, PrepareStep, ProjectLayer,
ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunExecutionLayer,
RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer,
RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunSandboxLayer, RunScmLayer,
DaytonaSnapshotLayer, DockerSandboxLayer, FeaturesLayer, GitAuthorLayer,
GithubIntegrationLayer, HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer,
InterviewProviderLayer, InterviewsLayer, LogFilter, McpEntryLayer, MergeMap, ModelRefOrSplice,
NotificationProviderLayer, NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
PrepareStep, ProjectLayer, ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer,
RunLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunSandboxLayer, RunScmLayer,
ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,
ServerIntegrationsLayer, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerLayer,
ServerListenLayer, ServerLoggingLayer, ServerSchedulerLayer, ServerSlateDbLayer,
ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer, StickyMap, StringOrSplice,
TeamsIntegrationLayer, WorkflowLayer,
WorkflowLayer,
};
pub(crate) use layers::{Combine, SettingsLayer};
pub use logging::{resolve_log_destination, resolve_log_destination_with_env};

View file

@ -113,7 +113,7 @@ fn rename_hint(key: &str) -> Option<String> {
"artifact_storage" => "rename to `[server.artifacts]`",
"storage_dir" | "data_dir" => "rename to `[server.storage] root`",
"max_concurrent_runs" => "rename to `[server.scheduler]` field",
"fabro" => "rename to `[project]`; `fabro.root` becomes `project.directory`",
"fabro" => "rename to `[project]`; project workflows now live under `.fabro/workflows`",
"git" => "split into `[run.git]` (local git behavior) and `[server.integrations.github]`",
"github" => {
"split into `[server.integrations.github]` (App identity/auth) and \

View file

@ -10,13 +10,12 @@
)]
use std::fmt::Write;
use std::path::{Component, Path, PathBuf};
use std::path::{Path, PathBuf};
use fabro_types::settings::{InterpString, RunNamespace};
use serde::Serialize;
use crate::load::load_settings_path;
use crate::{Error, Result, SettingsLayer, WorkflowSettingsBuilder, run};
use crate::{Error, Result, WorkflowSettingsBuilder, run};
const CONFIG_FILENAME: &str = ".fabro/project.toml";
#[derive(Clone, Debug)]
@ -27,19 +26,6 @@ pub struct WorkflowPathResolution {
pub workflow_slug: Option<String>,
}
/// Load a project config from a file path.
///
/// Goes through [`load_settings_path`] so that relative `run.goal.file`
/// paths are anchored at the directory of `path` at load time.
fn load_project_config(path: &Path) -> Result<SettingsLayer> {
let config = load_settings_path(path)?;
let root = WorkflowSettingsBuilder::project_from_layer(&config)
.map_err(|errors| Error::resolve("Failed to resolve project settings", errors.into()))?
.directory;
tracing::debug!(path = %path.display(), root = %root, "Loaded project config");
Ok(config)
}
/// Walk ancestor directories from `start` looking for `.fabro/project.toml`.
/// Returns the config file path, or `None` if not found.
pub fn discover_project_config(start: &Path) -> Result<Option<PathBuf>> {
@ -146,7 +132,9 @@ fn resolve_workflow_arg_impl(
let name = arg.to_string_lossy();
match discover_project_config(start_dir) {
Ok(Some(config_path)) => {
let fabro_root = resolve_fabro_root(&config_path);
let fabro_root = config_path
.parent()
.expect("project config should have a parent directory");
let project_candidate = fabro_root
.join("workflows")
.join(&*name)
@ -334,40 +322,6 @@ pub fn resolve_workflow(arg: &Path) -> Result<PathBuf> {
Ok(resolution.dot_path)
}
fn normalize_joined_path(base_dir: &Path, reference: &Path) -> PathBuf {
if reference.is_absolute() {
return reference.to_path_buf();
}
let mut normalized = PathBuf::new();
for component in base_dir.join(reference).components() {
match component {
Component::CurDir => {}
Component::Normal(part) => normalized.push(part),
Component::ParentDir => {
normalized.pop();
}
Component::RootDir => normalized.push(Path::new("/")),
Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
}
}
normalized
}
/// Resolve the fabro root directory from a config file path and its config.
/// The returned path is the config file's parent directory joined with the
/// `project.directory` value (default: `.`).
pub fn resolve_fabro_root(config_path: &Path) -> PathBuf {
let project_dir = config_path
.parent()
.expect("config_path should have a parent directory");
let config = load_project_config(config_path).expect("project config should load");
let root = WorkflowSettingsBuilder::project_from_layer(&config)
.expect("project settings should resolve")
.directory;
normalize_joined_path(project_dir, Path::new(&root))
}
#[cfg(test)]
mod tests {
use std::fs;
@ -386,19 +340,31 @@ mod tests {
#[test]
fn parse_with_project_directory() {
assert_eq!(
WorkflowSettingsBuilder::from_toml(
r#"
r#"
_version = 1
[project]
directory = "custom/"
"#
.parse::<crate::SettingsLayer>()
.unwrap()
.project
.and_then(|project| project.directory),
Some("custom/".to_string())
);
let project = WorkflowSettingsBuilder::from_toml(
r#"
_version = 1
[project]
directory = "custom/"
"#,
)
.unwrap()
.project
.directory,
"custom/"
);
)
.unwrap()
.project;
let json = serde_json::to_value(&project).expect("project settings should serialize");
assert!(json.get("directory").is_none());
}
#[test]
@ -425,17 +391,6 @@ directory = "custom/"
);
}
#[test]
fn load_from_disk() {
let tmp = TempDir::new().unwrap();
let config_dir = tmp.path().join(".fabro");
fs::create_dir_all(&config_dir).unwrap();
let path = config_dir.join("project.toml");
fs::write(&path, "_version = 1\n").unwrap();
let config = load_project_config(&path).unwrap();
assert_eq!(config.version, Some(1));
}
#[test]
fn discover_walks_ancestors() {
let tmp = TempDir::new().unwrap();
@ -450,50 +405,14 @@ directory = "custom/"
}
#[test]
fn load_project_config_rewrites_relative_goal_file_path() {
use crate::RunGoalLayer;
let tmp = TempDir::new().unwrap();
let config_dir = tmp.path().join(".fabro");
fs::create_dir_all(&config_dir).unwrap();
let path = config_dir.join("project.toml");
fs::write(
&path,
r#"_version = 1
[run.goal]
file = "prompts/goal.md"
"#,
)
.unwrap();
let config = load_project_config(&path).unwrap();
let Some(RunGoalLayer::File { file }) =
config.run.as_ref().and_then(|run| run.goal.as_ref())
else {
panic!("expected file variant");
};
let expected = config_dir.join("prompts").join("goal.md");
assert_eq!(file.as_source(), expected.to_string_lossy());
}
#[test]
fn default_directory_resolves_to_config_parent() {
let tmp = TempDir::new().unwrap();
let config_dir = tmp.path().join(".fabro");
fs::create_dir_all(&config_dir).unwrap();
let config_path = config_dir.join("project.toml");
fs::write(&config_path, "_version = 1\n").unwrap();
assert_eq!(resolve_fabro_root(&config_path), config_dir);
}
#[test]
fn custom_relative_directory_resolves_from_config_parent() {
fn deprecated_project_directory_does_not_change_fabro_root() {
let tmp = TempDir::new().unwrap();
let config_dir = tmp.path().join(".fabro");
fs::create_dir_all(&config_dir).unwrap();
let config_path = config_dir.join("project.toml");
let workflow_dir = config_dir.join("workflows/demo");
fs::create_dir_all(&workflow_dir).unwrap();
fs::write(workflow_dir.join("workflow.toml"), "_version = 1\n").unwrap();
fs::write(
&config_path,
r#"_version = 1
@ -504,37 +423,9 @@ directory = "../custom"
)
.unwrap();
assert_eq!(resolve_fabro_root(&config_path), tmp.path().join("custom"));
}
#[test]
fn relative_goal_file_resolves_from_config_dir() {
use crate::RunGoalLayer;
let tmp = TempDir::new().unwrap();
let config_dir = tmp.path().join(".fabro");
fs::create_dir_all(&config_dir).unwrap();
let config_path = config_dir.join("project.toml");
fs::write(
&config_path,
r#"_version = 1
[run.goal]
file = "prompts/goal.md"
"#,
)
.unwrap();
let config = load_project_config(&config_path).unwrap();
let Some(RunGoalLayer::File { file }) =
config.run.as_ref().and_then(|run| run.goal.as_ref())
else {
panic!("expected file variant");
};
assert_eq!(
file.as_source(),
config_dir.join("prompts").join("goal.md").to_string_lossy()
resolve_workflow_arg_impl(Path::new("demo"), tmp.path(), None).unwrap(),
config_dir.join("workflows/demo/workflow.toml")
);
}

View file

@ -7,10 +7,6 @@ pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) ->
ProjectNamespace {
name: layer.name.clone(),
description: layer.description.clone(),
directory: layer
.directory
.clone()
.expect("defaults.toml should provide project.directory"),
metadata: layer.metadata.clone().into_inner(),
}
}

View file

@ -1,12 +1,12 @@
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerSettings, DockerfileSource,
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, LocalSandboxSettings,
McpServerSettings, McpTransport, MergeStrategy, NotificationProviderSettings,
NotificationRouteSettings, PullRequestSettings, RunAgentSettings, RunCheckpointSettings,
RunExecutionSettings, RunGitSettings, RunGoal, RunIntegrationsGithubSettings,
RunIntegrationsSettings, RunInterviewsSettings, RunModelSettings, RunNamespace,
RunPrepareSettings, RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings,
McpTransport, MergeStrategy, NotificationProviderSettings, NotificationRouteSettings,
PullRequestSettings, RunAgentSettings, RunCheckpointSettings, RunExecutionSettings,
RunGitSettings, RunGoal, RunIntegrationsGithubSettings, RunIntegrationsSettings,
RunInterviewsSettings, RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings,
RunScmSettings, ScmGitHubSettings, TlsMode,
};
use super::ResolveError;
@ -183,25 +183,11 @@ fn resolve_sandbox(
.devcontainer
.expect("defaults.toml should provide run.sandbox.devcontainer"),
env: sandbox.env.clone().into_inner(),
local: resolve_local_sandbox(sandbox),
docker: sandbox.docker.as_ref().map(resolve_docker),
daytona: sandbox.daytona.as_ref().map(resolve_daytona),
}
}
fn resolve_local_sandbox(sandbox: &RunSandboxLayer) -> LocalSandboxSettings {
let local = sandbox
.local
.as_ref()
.expect("defaults.toml should provide run.sandbox.local");
LocalSandboxSettings {
worktree_mode: local
.worktree_mode
.expect("defaults.toml should provide run.sandbox.local.worktree_mode"),
}
}
fn resolve_docker(docker: &crate::DockerSandboxLayer) -> DockerSettings {
DockerSettings {
image: docker.image.clone().unwrap_or_default(),
@ -256,8 +242,6 @@ fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRou
})
.collect(),
slack: route.slack.as_ref().map(resolve_notification_provider),
discord: route.discord.as_ref().map(resolve_notification_provider),
teams: route.teams.as_ref().map(resolve_notification_provider),
}
}
@ -277,8 +261,6 @@ fn resolve_interviews(interviews: Option<&InterviewsLayer>) -> RunInterviewsSett
RunInterviewsSettings {
provider: interviews.provider.clone(),
slack: interviews.slack.as_ref().map(resolve_interview_provider),
discord: interviews.discord.as_ref().map(resolve_interview_provider),
teams: interviews.teams.as_ref().map(resolve_interview_provider),
}
}

View file

@ -1,12 +1,12 @@
use fabro_types::settings::InterpString;
use fabro_types::settings::server::{
DiscordIntegrationSettings, GithubIntegrationSettings, GithubIntegrationStrategy,
IntegrationWebhooksSettings, IpAllowEntry, ObjectStoreProvider, ObjectStoreSettings,
ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod,
ServerAuthSettings, ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings,
ServerIpAllowlistSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace,
ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings,
SlackIntegrationSettings, TeamsIntegrationSettings, WebhookStrategy,
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
IpAllowEntry, ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings,
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,
ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings,
ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
WebhookStrategy,
};
use fabro_util::Home;
@ -454,7 +454,7 @@ fn resolve_integrations(
errors: &mut Vec<ResolveError>,
) -> ServerIntegrationsSettings {
ServerIntegrationsSettings {
github: layer
github: layer
.and_then(|integrations| integrations.github.as_ref())
.map(|github| GithubIntegrationSettings {
enabled: github.enabled.unwrap_or(true),
@ -467,25 +467,13 @@ fn resolve_integrations(
}),
})
.unwrap_or_default(),
slack: layer
slack: layer
.and_then(|integrations| integrations.slack.as_ref())
.map(|slack| SlackIntegrationSettings {
enabled: slack.enabled.unwrap_or(true),
default_channel: slack.default_channel.clone(),
})
.unwrap_or_default(),
discord: layer
.and_then(|integrations| integrations.discord.as_ref())
.map(|discord| DiscordIntegrationSettings {
enabled: discord.enabled.unwrap_or(true),
})
.unwrap_or_default(),
teams: layer
.and_then(|integrations| integrations.teams.as_ref())
.map(|teams| TeamsIntegrationSettings {
enabled: teams.enabled.unwrap_or(true),
})
.unwrap_or_default(),
}
}

View file

@ -103,7 +103,7 @@ fallbacks = ["anthropic", "..."]
}
#[test]
fn hooks_replace_by_id_in_place() {
fn hooks_replace_by_id() {
let lower = parse(
r#"
[[run.hooks]]

View file

@ -1,5 +1,5 @@
use fabro_types::settings::cli::OutputFormat;
use fabro_types::settings::run::{ApprovalMode, RunMode, WorktreeMode};
use fabro_types::settings::run::{ApprovalMode, RunMode};
use fabro_types::settings::server::ObjectStoreProvider;
use crate::{Combine, ServerSettingsBuilder, SettingsLayer, WorkflowSettingsBuilder};
@ -18,12 +18,13 @@ fn embedded_defaults() -> SettingsLayer {
fn embedded_defaults_parse_successfully() {
let defaults = embedded_defaults();
assert_eq!(
assert!(
defaults
.project
.as_ref()
.and_then(|project| project.directory.as_deref()),
Some(".")
.and_then(|project| project.directory.as_deref())
.is_none(),
"built-in defaults should not materialize deprecated project.directory"
);
assert_eq!(
defaults
@ -38,12 +39,13 @@ fn embedded_defaults_parse_successfully() {
fn apply_builtin_defaults_materializes_expected_layer() {
let layer = SettingsLayer::default().combine(embedded_defaults());
assert_eq!(
assert!(
layer
.project
.as_ref()
.and_then(|project| project.directory.as_deref()),
Some(".")
.and_then(|project| project.directory.as_deref())
.is_none(),
"built-in defaults should not materialize deprecated project.directory"
);
assert_eq!(
layer
@ -76,15 +78,6 @@ fn apply_builtin_defaults_materializes_expected_layer() {
.and_then(|execution| execution.approval),
Some(ApprovalMode::Prompt)
);
assert_eq!(
layer
.run
.as_ref()
.and_then(|run| run.sandbox.as_ref())
.and_then(|sandbox| sandbox.local.as_ref())
.and_then(|local| local.worktree_mode),
Some(WorktreeMode::Always)
);
assert_eq!(
layer
.cli

View file

@ -8,14 +8,18 @@ fn resolves_project_defaults_from_empty_settings() {
.expect("empty settings should resolve")
.project;
assert_eq!(project.directory, ".");
let json = serde_json::to_value(&project).expect("project settings should serialize");
assert!(
json.get("directory").is_none(),
"resolved project settings should not expose deprecated directory"
);
assert!(project.name.is_none());
assert!(project.description.is_none());
assert!(project.metadata.is_empty());
}
#[test]
fn resolves_project_directory_and_metadata() {
fn resolves_project_metadata_and_ignores_deprecated_directory() {
let project = WorkflowSettingsBuilder::from_toml(
r#"
_version = 1
@ -34,7 +38,11 @@ team = "platform"
assert_eq!(project.name.as_deref(), Some("Acme"));
assert_eq!(project.description.as_deref(), Some("Automation"));
assert_eq!(project.directory, ".fabro");
let json = serde_json::to_value(&project).expect("project settings should serialize");
assert!(
json.get("directory").is_none(),
"resolved project settings should not expose deprecated directory"
);
assert_eq!(
project.metadata.get("team").map(String::as_str),
Some("platform")

View file

@ -92,7 +92,12 @@ name = "gpt-5"
WorkflowSettingsBuilder::from_toml(source).expect("workflow settings should resolve");
let server = ServerSettingsBuilder::from_toml(source).expect("server settings should resolve");
assert_eq!(workflow_settings.project.directory, ".fabro");
let project_json = serde_json::to_value(&workflow_settings.project)
.expect("project settings should serialize");
assert!(
project_json.get("directory").is_none(),
"resolved project settings should not expose deprecated directory"
);
assert_eq!(workflow_settings.workflow.graph, "graphs/workflow.dot");
assert_eq!(server.server.storage.root.as_source(), "/srv/fabro");
assert_eq!(
@ -121,7 +126,12 @@ fn workflow_settings_resolve_defaults_and_expose_fields() {
let resolved = fabro_config::WorkflowSettingsBuilder::from_layer(&settings)
.expect("defaults should resolve");
assert_eq!(resolved.project.directory, ".");
let project_json =
serde_json::to_value(&resolved.project).expect("project settings should serialize");
assert!(
project_json.get("directory").is_none(),
"resolved project settings should not expose deprecated directory"
);
assert_eq!(resolved.workflow.graph, "workflow.fabro");
assert_eq!(resolved.run.execution.mode, RunMode::Normal);
}

View file

@ -1,5 +1,5 @@
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{ApprovalMode, RunGoal, RunMode, WorktreeMode};
use fabro_types::settings::run::{ApprovalMode, RunGoal, RunMode};
use crate::{SettingsLayer, WorkflowSettingsBuilder};
@ -14,7 +14,6 @@ fn resolves_run_defaults_from_empty_settings() {
assert_eq!(settings.prepare.timeout_ms, 300_000);
assert_eq!(settings.sandbox.provider, "docker");
assert!(settings.sandbox.stop_on_terminal);
assert_eq!(settings.sandbox.local.worktree_mode, WorktreeMode::Always);
let docker = settings
.sandbox
.docker
@ -27,6 +26,92 @@ fn resolves_run_defaults_from_empty_settings() {
assert!(settings.pull_request.is_none());
}
#[test]
fn resolved_run_chat_surfaces_are_slack_only() {
let settings = WorkflowSettingsBuilder::from_toml(
r##"
_version = 1
[run.notifications.ops]
enabled = true
provider = "slack"
events = ["run.completed"]
[run.notifications.ops.slack]
channel = "#ops"
[run.interviews]
provider = "slack"
[run.interviews.slack]
channel = "#ops"
"##,
)
.expect("slack-only chat settings should resolve")
.run;
let route = settings
.notifications
.get("ops")
.expect("notification route should resolve");
assert_eq!(
serde_json::to_value(route).expect("route should serialize"),
serde_json::json!({
"enabled": true,
"provider": "slack",
"events": ["run.completed"],
"slack": {
"channel": "#ops",
},
})
);
assert_eq!(
serde_json::to_value(&settings.interviews).expect("interviews should serialize"),
serde_json::json!({
"provider": "slack",
"slack": {
"channel": "#ops",
},
})
);
}
#[test]
fn parsing_rejects_unknown_run_chat_destinations() {
let notifications = r##"
_version = 1
[run.notifications.ops.chatapp]
channel = "#ops"
"##;
let err = notifications
.parse::<SettingsLayer>()
.expect_err("unknown notification destination should be rejected");
let message = err.to_string();
assert!(
message.contains("chatapp") || message.contains("unknown field"),
"expected notification parse error for unknown chat provider, got: {message}"
);
let interviews = r##"
_version = 1
[run.interviews.chatapp]
channel = "#ops"
"##;
let err = interviews
.parse::<SettingsLayer>()
.expect_err("unknown interview destination should be rejected");
let message = err.to_string();
assert!(
message.contains("chatapp") || message.contains("unknown field"),
"expected interview parse error for unknown chat provider, got: {message}"
);
}
#[test]
fn resolves_explicit_stop_on_terminal_false() {
let settings = WorkflowSettingsBuilder::from_toml(

View file

@ -109,6 +109,51 @@ fn resolves_server_defaults_from_empty_settings() {
assert!(!settings.slatedb.disk_cache);
}
#[test]
fn resolved_server_integrations_are_slack_only_for_chat() {
let settings = resolve_server(&empty_settings_with_auth_methods());
let integrations =
serde_json::to_value(&settings.integrations).expect("integrations should serialize");
assert_eq!(
integrations,
serde_json::json!({
"github": {
"enabled": false,
"strategy": "token",
"app_id": null,
"client_id": null,
"slug": null,
"webhooks": null,
},
"slack": {
"enabled": false,
"default_channel": null,
},
})
);
}
#[test]
fn parsing_rejects_unknown_server_integrations() {
let source = r#"
_version = 1
[server.integrations.chatapp]
enabled = true
"#;
let err = source
.parse::<SettingsLayer>()
.expect_err("unknown chat integration should be rejected");
let message = err.to_string();
assert!(
message.contains("chatapp") || message.contains("unknown field"),
"expected parse error for unknown chat provider, got: {message}"
);
}
#[test]
fn resolves_server_logging_destination_from_settings() {
let file = parse(

View file

@ -498,7 +498,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
}
}

View file

@ -338,26 +338,6 @@ description = "Uncovered a DigitalOcean OAuth Refresh Token, which could allow p
regex = '''(?i)\b(dor_v1_[a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
keywords = ["dor_v1_"]
[[rules]]
id = "discord-api-token"
description = "Detected a Discord API key, potentially compromising communication channels and user data privacy on Discord."
regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:{1,3}=|\|\|:|<=|=>|:|\?=)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
keywords = ["discord"]
[[rules]]
id = "discord-client-id"
description = "Identified a Discord client ID, which may lead to unauthorized integrations and data exposure in Discord applications."
regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:{1,3}=|\|\|:|<=|=>|:|\?=)(?:'|\"|\s|=|\x60){0,5}([0-9]{18})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
entropy = 2
keywords = ["discord"]
[[rules]]
id = "discord-client-secret"
description = "Discovered a potential Discord client secret, risking compromised Discord bot integrations and data leaks."
regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:{1,3}=|\|\|:|<=|=>|:|\?=)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
entropy = 2
keywords = ["discord"]
[[rules]]
id = "doppler-api-token"
description = "Discovered a Doppler API token, posing a risk to environment and secrets management security."
@ -2479,16 +2459,6 @@ keywords = [
"message_bird",
]
[[rules]]
id = "microsoft-teams-webhook"
description = "Uncovered a Microsoft Teams Webhook, which could lead to unauthorized access to team collaboration tools and data leaks."
regex = '''https://[a-z0-9]+\.webhook\.office\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}'''
keywords = [
"webhook.office.com",
"webhookb2",
"incomingwebhook",
]
[[rules]]
id = "netlify-access-token"
description = "Detected a Netlify Access Token, potentially compromising web hosting services and site management."
@ -3047,4 +3017,3 @@ id = "zendesk-secret-key"
description = "Detected a Zendesk Secret Key, risking unauthorized access to customer support services and sensitive ticketing data."
regex = '''(?i)[\w.-]{0,50}?(?:zendesk)(?:[ \t\w.-]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:{1,3}=|\|\|:|<=|=>|:|\?=)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
keywords = ["zendesk"]

View file

@ -71,19 +71,19 @@ fn collect_replacements(v: &Value) -> Vec<(String, String)> {
repls
}
fn redact_value_in_place(value: &mut Value, skip_field: bool) {
fn redact_json_tree(value: &mut Value, skip_field: bool) {
match value {
Value::Object(obj) => {
if should_skip_object(obj) {
return;
}
for (key, child) in obj {
redact_value_in_place(child, should_skip_field(key));
redact_json_tree(child, should_skip_field(key));
}
}
Value::Array(arr) => {
for child in arr {
redact_value_in_place(child, false);
redact_json_tree(child, false);
}
}
Value::String(text) if !skip_field => {
@ -97,7 +97,7 @@ fn redact_value_in_place(value: &mut Value, skip_field: bool) {
}
pub fn redact_json_value(mut value: Value) -> Value {
redact_value_in_place(&mut value, false);
redact_json_tree(&mut value, false);
value
}

View file

@ -9,7 +9,6 @@
use std::collections::HashMap;
use fabro_types::settings::run::WorktreeMode as V2WorktreeMode;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Serialize};
@ -123,24 +122,3 @@ pub struct DaytonaSnapshotSettings {
pub disk: Option<i32>,
pub dockerfile: Option<DockerfileSource>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WorktreeMode {
Always,
#[default]
Clean,
Dirty,
Never,
}
/// Convert a v2 [`V2WorktreeMode`] into the runtime [`WorktreeMode`].
#[must_use]
pub fn bridge_worktree_mode(m: V2WorktreeMode) -> WorktreeMode {
match m {
V2WorktreeMode::Always => WorktreeMode::Always,
V2WorktreeMode::Clean => WorktreeMode::Clean,
V2WorktreeMode::Dirty => WorktreeMode::Dirty,
V2WorktreeMode::Never => WorktreeMode::Never,
}
}

View file

@ -875,8 +875,8 @@ mod runs {
use fabro_api::types::*;
use fabro_types::settings::run::{
DaytonaSettings, DaytonaSnapshotSettings, LocalSandboxSettings, RunGoal, RunModelSettings,
RunNamespace, RunPrepareSettings, RunSandboxSettings,
DaytonaSettings, DaytonaSnapshotSettings, RunGoal, RunModelSettings, RunNamespace,
RunPrepareSettings, RunSandboxSettings,
};
use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};
use fabro_types::{RunId, StageId, WorkflowSettings};
@ -934,7 +934,6 @@ mod runs {
fabro_types::infer_run_title(goal),
labels(entries),
Some(format!("/demo/{repo_name}")),
false,
Some(format!("https://github.com/demo/{repo_name}.git")),
Some(created_at),
Some(created_at),
@ -1023,7 +1022,6 @@ mod runs {
elapsed_secs: summary.elapsed_secs,
goal: summary.goal,
source_directory: summary.source_directory,
in_place: Some(summary.in_place),
repo_origin_url: summary.repo_origin_url,
labels: summary.labels,
pending_control: summary.pending_control,
@ -1630,10 +1628,7 @@ mod runs {
pub(super) fn settings() -> serde_json::Value {
let settings = WorkflowSettings {
project: ProjectNamespace {
directory: "/workspace/api-server".into(),
..ProjectNamespace::default()
},
project: ProjectNamespace::default(),
workflow: WorkflowNamespace {
graph: "workflow.fabro".into(),
..WorkflowNamespace::default()
@ -1658,7 +1653,6 @@ mod runs {
stop_on_terminal: true,
devcontainer: false,
env: HashMap::new(),
local: LocalSandboxSettings::default(),
docker: None,
daytona: Some(DaytonaSettings {
auto_stop_interval: Some(60),

View file

@ -9,9 +9,9 @@ use fabro_api::types;
use fabro_auth::auth_issue_message;
use fabro_config::run::parse_run_layer_from_settings_toml;
use fabro_config::{
CliLayer, CliOutputLayer, DaytonaDockerfileLayer, DockerSandboxLayer, LocalSandboxLayer,
ReplaceMap, RunExecutionLayer, RunLayer, RunModelLayer, RunSandboxLayer,
WorkflowSettingsBuilder, parse_input_overrides,
CliLayer, CliOutputLayer, DaytonaDockerfileLayer, DockerSandboxLayer, ReplaceMap,
RunExecutionLayer, RunLayer, RunModelLayer, RunSandboxLayer, WorkflowSettingsBuilder,
parse_input_overrides,
};
use fabro_graphviz::graph::{Graph, is_llm_handler_type};
use fabro_graphviz::render::apply_direction;
@ -29,7 +29,7 @@ use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::interp::InterpString;
use fabro_types::settings::run::{
ApprovalMode, DaytonaNetworkLayer, DaytonaSettings, DockerSettings, DockerfileSource, RunGoal,
RunMode, RunNamespace, WorktreeMode,
RunMode, RunNamespace,
};
use fabro_types::{RunId, WorkflowSettings};
use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};
@ -57,7 +57,6 @@ pub(crate) struct PreparedManifest {
pub workflow_bundle: WorkflowBundle,
pub workflow_input: BundledWorkflow,
pub source_directory: PathBuf,
pub in_place: bool,
}
#[derive(Clone, Debug, Default)]
@ -136,9 +135,6 @@ pub(crate) fn prepare_manifest(
.map(|title| fabro_types::normalize_explicit_run_title(title.as_str()))
.transpose()?;
let in_place = settings.run.sandbox.provider == "local"
&& settings.run.sandbox.local.worktree_mode == WorktreeMode::Never;
Ok(PreparedManifest {
cwd: cwd.clone(),
git: manifest.git.clone(),
@ -155,7 +151,6 @@ pub(crate) fn prepare_manifest(
workflow_bundle,
workflow_input,
source_directory: resolve_working_directory(&settings, &cwd),
in_place,
})
}
@ -187,7 +182,6 @@ pub(crate) fn create_run_input(
title: prepared.title,
git: prepared.git,
fork_source_ref: None,
in_place: prepared.in_place,
provenance: None,
configured_providers,
web_url,
@ -327,27 +321,17 @@ fn manifest_args_overrides(
name: args.model.as_deref().map(InterpString::parse),
fallbacks: Vec::new(),
});
let local_worktree = args
.worktree_mode
.as_deref()
.and_then(parse_worktree_mode_arg)
.map(|mode| LocalSandboxLayer {
worktree_mode: Some(mode),
});
let sandbox = (args.sandbox.is_some()
|| args.preserve_sandbox.is_some()
|| args.docker_image.is_some()
|| local_worktree.is_some())
.then(|| RunSandboxLayer {
provider: args.sandbox.clone(),
preserve: args.preserve_sandbox,
local: local_worktree,
docker: args.docker_image.as_ref().map(|image| DockerSandboxLayer {
image: Some(image.clone()),
..DockerSandboxLayer::default()
}),
..RunSandboxLayer::default()
});
let sandbox =
(args.sandbox.is_some() || args.preserve_sandbox.is_some() || args.docker_image.is_some())
.then(|| RunSandboxLayer {
provider: args.sandbox.clone(),
preserve: args.preserve_sandbox,
docker: args.docker_image.as_ref().map(|image| DockerSandboxLayer {
image: Some(image.clone()),
..DockerSandboxLayer::default()
}),
..RunSandboxLayer::default()
});
let execution_has_any = args.dry_run.is_some() || args.auto_approve.is_some();
let execution = execution_has_any.then(|| RunExecutionLayer {
@ -392,16 +376,6 @@ fn manifest_args_overrides(
})
}
fn parse_worktree_mode_arg(value: &str) -> Option<WorktreeMode> {
match value {
"always" => Some(WorktreeMode::Always),
"clean" => Some(WorktreeMode::Clean),
"dirty" => Some(WorktreeMode::Dirty),
"never" => Some(WorktreeMode::Never),
_ => None,
}
}
fn parse_labels(labels: &[String]) -> HashMap<String, String> {
labels
.iter()
@ -1724,7 +1698,6 @@ root = "/srv/fabro"
docker_image: None,
input: Vec::new(),
verbose: None,
worktree_mode: None,
});
let prepared = prepare_manifest(&server_settings, &manifest).unwrap();
@ -1758,7 +1731,6 @@ override = "server"
docker_image: None,
input: vec!["override=cli".to_string()],
verbose: None,
worktree_mode: None,
});
let prepared = prepare_manifest(&server_settings, &manifest).unwrap();

View file

@ -2479,7 +2479,6 @@ async fn list_run_stages_distinguishes_visits() {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
},
workflow_event::Event::RunStarting,
@ -3389,7 +3388,6 @@ async fn create_completed_run_ready_for_pull_request(
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
};
create_durable_run_with_events(state, run_id, &[
@ -3409,7 +3407,6 @@ async fn create_completed_run_ready_for_pull_request(
manifest_blob: None,
git,
fork_source_ref: None,
in_place: false,
web_url: None,
},
workflow_event::Event::WorkflowRunStarted {
@ -7464,7 +7461,6 @@ async fn delete_run_with_preserved_sandbox_returns_handoff() {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
},
workflow_event::Event::RunSubmitted {
@ -7529,7 +7525,6 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
},
workflow_event::Event::RunSubmitted {

View file

@ -85,7 +85,7 @@ session_sandboxes = true
format!("GET /api/v1/runs/{run_id}/settings"),
)
.await;
assert_eq!(body["project"]["directory"], ".");
assert!(body["project"].get("directory").is_none());
assert_eq!(body["workflow"]["graph"], "workflow.fabro");
assert_eq!(body["run"]["goal"]["type"], "inline");
assert_eq!(body["run"]["goal"]["value"], "Ship it");

View file

@ -4,7 +4,7 @@ use std::time::Duration;
use axum::body::{Body, to_bytes};
use axum::http::{Request, StatusCode};
use fabro_config::{LocalSandboxLayer, RunLayer, RunSandboxLayer, ServerSettingsBuilder};
use fabro_config::{RunLayer, RunSandboxLayer, ServerSettingsBuilder};
use fabro_server::server::{AppState, spawn_scheduler};
use fabro_server::test_support::{
build_test_router, test_app_state as server_test_app_state,
@ -16,7 +16,6 @@ use fabro_test::{
expect_axum_status_in, expect_axum_text,
};
use fabro_types::ServerSettings;
use fabro_types::settings::run::WorktreeMode;
use tokio::time::sleep;
use tower::ServiceExt;
@ -97,9 +96,6 @@ pub(crate) fn test_settings() -> TestAppSettings {
manifest_run_defaults: RunLayer {
sandbox: Some(RunSandboxLayer {
provider: Some("local".to_string()),
local: Some(LocalSandboxLayer {
worktree_mode: Some(WorktreeMode::Never),
}),
..RunSandboxLayer::default()
}),
..RunLayer::default()

View file

@ -67,7 +67,6 @@ impl RunProjectionReducer for RunProjection {
definition_blob: None,
git: props.git.clone(),
fork_source_ref: props.fork_source_ref.clone(),
in_place: props.in_place,
});
self.graph_source.clone_from(&props.workflow_source);
}
@ -538,7 +537,6 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> RunSummary
.spec
.as_ref()
.and_then(|spec| spec.source_directory.clone()),
state.spec.as_ref().is_some_and(|spec| spec.in_place),
state
.spec
.as_ref()
@ -1675,7 +1673,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
});
let summary_json = serde_json::to_value(build_summary(&state, &fixtures::RUN_1)).unwrap();

View file

@ -478,7 +478,6 @@ mod tests {
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
}
}

View file

@ -29,7 +29,6 @@ fn sample_run_spec() -> RunSpec {
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
}
}

View file

@ -98,8 +98,6 @@ pub struct RunSpec {
pub git: Option<GitContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fork_source_ref: Option<ForkSourceRef>,
#[serde(default)]
pub in_place: bool,
}
impl RunSpec {

View file

@ -36,8 +36,6 @@ pub struct RunCreatedProps {
pub git: Option<GitContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fork_source_ref: Option<ForkSourceRef>,
#[serde(default)]
pub in_place: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_url: Option<String>,
}

View file

@ -20,8 +20,6 @@ pub struct RunSummary {
#[serde(default)]
pub source_directory: Option<String>,
#[serde(default)]
pub in_place: bool,
#[serde(default)]
pub repo_origin_url: Option<String>,
pub repository: RepositoryReference,
#[serde(default)]
@ -59,7 +57,6 @@ impl RunSummary {
title: String,
labels: HashMap<String, String>,
source_directory: Option<String>,
in_place: bool,
repo_origin_url: Option<String>,
start_time: Option<DateTime<Utc>>,
last_event_at: Option<DateTime<Utc>>,
@ -85,7 +82,6 @@ impl RunSummary {
title,
labels,
source_directory,
in_place,
repo_origin_url,
repository,
start_time,
@ -172,7 +168,6 @@ mod tests {
"Production title".to_string(),
HashMap::from([("team".to_string(), "core".to_string())]),
Some("/Users/client/local-checkout".to_string()),
false,
Some("https://github.com/fabro-sh/fabro.git".to_string()),
Some(Utc.with_ymd_and_hms(2026, 4, 20, 12, 0, 0).unwrap()),
Some(Utc.with_ymd_and_hms(2026, 4, 20, 12, 5, 0).unwrap()),
@ -290,7 +285,6 @@ mod tests {
"ship it".to_string(),
HashMap::new(),
Some("/Users/client/local-checkout".to_string()),
false,
None,
None,
None,
@ -313,7 +307,6 @@ mod tests {
"ship it".to_string(),
HashMap::new(),
None,
false,
None,
None,
None,

View file

@ -45,12 +45,12 @@ pub use run::{
ScmGitHubSettings, TlsMode,
};
pub use server::{
DiscordIntegrationSettings, GithubIntegrationSettings, IntegrationWebhooksSettings,
IpAllowEntry, LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings,
ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,
GithubIntegrationSettings, IntegrationWebhooksSettings, IpAllowEntry, LogDestination,
ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings,
ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,
ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerListenSettings,
ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings,
ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
};
pub use size::{ParseSizeError, Size};
pub use workflow::WorkflowNamespace;

View file

@ -1,7 +1,4 @@
//! Project domain: first-class project object.
//!
//! `[project]` replaces the old flat `[fabro]` shape. `directory` means the
//! Fabro-managed project directory inside the repo, defaulting to `.`.
use std::collections::HashMap;
@ -12,6 +9,5 @@ use serde::{Deserialize, Serialize};
pub struct ProjectNamespace {
pub name: Option<String>,
pub description: Option<String>,
pub directory: String,
pub metadata: HashMap<String, String>,
}

View file

@ -201,7 +201,6 @@ pub struct RunSandboxSettings {
pub stop_on_terminal: bool,
pub devcontainer: bool,
pub env: HashMap<String, InterpString>,
pub local: LocalSandboxSettings,
pub docker: Option<DockerSettings>,
pub daytona: Option<DaytonaSettings>,
}
@ -218,18 +217,12 @@ impl Default for RunSandboxSettings {
stop_on_terminal: true,
devcontainer: false,
env: HashMap::new(),
local: LocalSandboxSettings::default(),
docker: None,
daytona: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct LocalSandboxSettings {
pub worktree_mode: WorktreeMode,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DockerSettings {
pub image: String,
@ -309,8 +302,6 @@ pub struct NotificationRouteSettings {
pub provider: Option<String>,
pub events: Vec<String>,
pub slack: Option<NotificationProviderSettings>,
pub discord: Option<NotificationProviderSettings>,
pub teams: Option<NotificationProviderSettings>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
@ -322,8 +313,6 @@ pub struct NotificationProviderSettings {
pub struct RunInterviewsSettings {
pub provider: Option<String>,
pub slack: Option<InterviewProviderSettings>,
pub discord: Option<InterviewProviderSettings>,
pub teams: Option<InterviewProviderSettings>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
@ -585,16 +574,6 @@ pub enum ApprovalMode {
Auto,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorktreeMode {
Always,
#[default]
Clean,
Dirty,
Never,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum DaytonaNetworkLayer {

View file

@ -258,10 +258,8 @@ pub struct ServerLoggingSettings {
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerIntegrationsSettings {
pub github: GithubIntegrationSettings,
pub slack: SlackIntegrationSettings,
pub discord: DiscordIntegrationSettings,
pub teams: TeamsIntegrationSettings,
pub github: GithubIntegrationSettings,
pub slack: SlackIntegrationSettings,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
@ -280,16 +278,6 @@ pub struct SlackIntegrationSettings {
pub default_channel: Option<InterpString>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiscordIntegrationSettings {
pub enabled: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TeamsIntegrationSettings {
pub enabled: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct IntegrationWebhooksSettings {
pub strategy: Option<WebhookStrategy>,

View file

@ -39,7 +39,6 @@ fn run_created_props_round_trip_templated_settings() {
source_run_id: fixtures::RUN_2,
checkpoint_sha: "def456".to_string(),
}),
in_place: true,
web_url: Some("http://localhost:3000/runs/01JNQVR7M0EJ5GKAT2SC4ERS1Z".to_string()),
};
@ -54,7 +53,6 @@ fn run_created_props_round_trip_templated_settings() {
assert_eq!(json["git"]["branch"], "main");
assert_eq!(json["git"]["dirty"], "unknown");
assert_eq!(json["git"]["push_outcome"]["type"], "skipped_no_remote");
assert_eq!(json["in_place"], true);
assert_eq!(
json["web_url"],
"http://localhost:3000/runs/01JNQVR7M0EJ5GKAT2SC4ERS1Z"
@ -90,7 +88,6 @@ fn run_created_props_omits_web_url_when_absent() {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
};

View file

@ -26,7 +26,6 @@ fn sample_run_spec() -> RunSpec {
},
}),
fork_source_ref: None,
in_place: false,
}
}

View file

@ -38,7 +38,6 @@ fn run_spec_round_trips_templated_settings() {
source_run_id: fixtures::RUN_2,
checkpoint_sha: "def456".to_string(),
}),
in_place: false,
};
let json = serde_json::to_value(&record).expect("record should serialize");
@ -54,8 +53,6 @@ fn run_spec_round_trips_templated_settings() {
assert_eq!(json["git"]["dirty"], "clean");
assert_eq!(json["git"]["push_outcome"]["type"], "succeeded");
assert_eq!(json["fork_source_ref"]["checkpoint_sha"], "def456");
assert_eq!(json["in_place"], false);
let round_trip: RunSpec =
serde_json::from_value(json.clone()).expect("record should deserialize");

View file

@ -284,7 +284,6 @@ mod tests {
definition_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
}
}
}

View file

@ -52,7 +52,6 @@ fn event_body_from_event(event: &Event) -> EventBody {
manifest_blob,
git,
fork_source_ref,
in_place,
web_url,
..
} => EventBody::RunCreated(fabro_types::RunCreatedProps {
@ -71,7 +70,6 @@ fn event_body_from_event(event: &Event) -> EventBody {
manifest_blob: *manifest_blob,
git: git.clone(),
fork_source_ref: fork_source_ref.clone(),
in_place: *in_place,
web_url: web_url.clone(),
}),
Event::WorkflowRunStarted {
@ -2051,7 +2049,6 @@ mod tests {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
});
let actor = stored.actor.as_ref().expect("actor set");

View file

@ -43,8 +43,6 @@ pub enum Event {
git: Option<GitContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
fork_source_ref: Option<ForkSourceRef>,
#[serde(default)]
in_place: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
web_url: Option<String>,
},

View file

@ -722,7 +722,6 @@ mod tests {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await

View file

@ -45,7 +45,6 @@ pub struct CreateRunInput {
pub title: Option<String>,
pub git: Option<GitContext>,
pub fork_source_ref: Option<ForkSourceRef>,
pub in_place: bool,
pub provenance: Option<RunProvenance>,
pub configured_providers: Vec<Provider>,
/// Public URL where this run can be viewed in the web UI, when the server
@ -71,7 +70,6 @@ struct PersistCreateOptions {
source_directory: Option<String>,
git: Option<GitContext>,
fork_source_ref: Option<ForkSourceRef>,
in_place: bool,
provenance: Option<RunProvenance>,
configured_providers: Vec<Provider>,
}
@ -106,7 +104,6 @@ pub async fn create(
title,
git,
fork_source_ref,
in_place,
provenance,
configured_providers,
web_url,
@ -143,7 +140,6 @@ pub async fn create(
source_directory,
git,
fork_source_ref,
in_place,
provenance,
configured_providers,
},
@ -240,7 +236,6 @@ async fn persist_created_run(
manifest_blob,
git: record.git.clone(),
fork_source_ref: record.fork_source_ref.clone(),
in_place: record.in_place,
web_url,
},
record.run_id.created_at(),
@ -353,7 +348,6 @@ fn persist_validated(
source_directory,
git,
fork_source_ref,
in_place,
provenance,
configured_providers,
} = options;
@ -380,7 +374,6 @@ fn persist_validated(
definition_blob: None,
git,
fork_source_ref,
in_place,
};
pipeline::persist(validated, PersistOptions { run_dir, run_spec })
@ -722,7 +715,6 @@ mod tests {
title: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,
@ -767,7 +759,6 @@ mod tests {
title: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,
@ -834,7 +825,6 @@ mod tests {
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,
@ -944,7 +934,6 @@ mod tests {
title: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,
@ -988,7 +977,6 @@ mod tests {
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,
@ -1054,7 +1042,6 @@ mod tests {
title: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,
@ -1099,7 +1086,6 @@ mod tests {
title: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: Some(fabro_types::RunProvenance {
server: Some(fabro_types::RunServerProvenance {
version: "0.9.0".to_string(),

View file

@ -109,12 +109,6 @@ fn validate_source_spec(
checkpoint_sha: &str,
) -> std::result::Result<(), Error> {
let spec = spec.ok_or_else(|| Error::engine("source run projection has no spec"))?;
if spec.in_place {
return Err(Error::Validation(
"source run was created with --in-place; cannot fork (no git checkpoint history)"
.to_string(),
));
}
if checkpoint_sha.trim().is_empty() {
return Err(Error::Validation(
"target checkpoint has an empty git_commit_sha; cannot fork".to_string(),
@ -184,7 +178,6 @@ async fn persist_forked_run(
manifest_blob: spec.manifest_blob,
git: spec.git.clone(),
fork_source_ref: spec.fork_source_ref.clone(),
in_place: spec.in_place,
web_url: None,
})
.await
@ -365,7 +358,6 @@ mod tests {
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await

View file

@ -8,8 +8,7 @@ use fabro_interview::{AutoApproveInterviewer, Interviewer};
use fabro_mcp::config::{McpServerSettings, McpTransport};
use fabro_model::{Catalog, FallbackTarget, Provider};
use fabro_sandbox::config::{
self as sandbox_config, DaytonaNetwork, DaytonaSnapshotSettings,
DockerfileSource as SandboxDockerfileSource, WorktreeMode, bridge_worktree_mode,
DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource,
};
use fabro_sandbox::daytona::DaytonaConfig;
use fabro_sandbox::{DockerSandboxOptions, SandboxProvider, SandboxSpec};
@ -69,7 +68,6 @@ struct RunSession {
artifact_sink: Option<ArtifactSink>,
git: Option<GitCheckpointOptions>,
github_app: Option<fabro_github::GitHubCredentials>,
worktree_mode: Option<WorktreeMode>,
registry_override: Option<Arc<HandlerRegistry>>,
preserve_sandbox: bool,
stop_on_terminal: bool,
@ -442,7 +440,6 @@ impl RunSession {
artifact_sink: services.artifact_sink,
git,
github_app: services.github_app.clone(),
worktree_mode: Some(resolve_worktree_mode(resolved)),
registry_override: services.registry_override,
preserve_sandbox: resolved.sandbox.preserve,
stop_on_terminal: resolved.sandbox.stop_on_terminal,
@ -496,10 +493,6 @@ fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> Result<SandboxPro
.map_or_else(|| Ok(SandboxProvider::default()), Ok)
}
fn resolve_worktree_mode(settings: &ResolvedRunSettings) -> sandbox_config::WorktreeMode {
bridge_worktree_mode(settings.sandbox.local.worktree_mode)
}
fn resolve_daytona_config(settings: &ResolvedRunSettings) -> Option<DaytonaConfig> {
settings
.sandbox
@ -753,7 +746,6 @@ impl RunSession {
vault: self.vault,
devcontainer: self.devcontainer,
git: self.git,
worktree_mode: self.worktree_mode,
registry_override: self.registry_override,
artifact_sink: self.artifact_sink,
run_control: self.run_control,
@ -1078,7 +1070,6 @@ mod tests {
title: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,
@ -1268,7 +1259,6 @@ mod tests {
title: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
configured_providers: Vec::new(),
web_url: None,

View file

@ -157,7 +157,6 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
},
)
}
@ -232,7 +231,6 @@ async fn execute_test_run_with_options(
vault: None,
devcontainer: None,
git: git_options,
worktree_mode: None,
run_control: None,
registry_override,
artifact_sink: None,
@ -293,7 +291,6 @@ async fn execute_runs_start_to_exit_and_returns_final_context() {
vault: None,
devcontainer: None,
git: None,
worktree_mode: None,
run_control: None,
registry_override: None,
artifact_sink: None,
@ -362,7 +359,6 @@ async fn run_with_lifecycle(
vault: None,
devcontainer: None,
git: None,
worktree_mode: None,
run_control: None,
registry_override: Some(Arc::new(registry)),
artifact_sink: None,

View file

@ -739,7 +739,6 @@ mod tests {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await

View file

@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
@ -8,13 +8,11 @@ use fabro_auth::{
CredentialResolver, CredentialSource, EnvCredentialSource, VaultCredentialSource,
auth_issue_message,
};
use fabro_config::RunScratch;
use fabro_graphviz::graph;
use fabro_hooks::{HookContext, HookDecision, HookEvent, HookRunner};
use fabro_sandbox::config::WorktreeMode;
use fabro_sandbox::{
GitSetupIntent, ReadBeforeWriteSandbox, SandboxEventCallback, SandboxSpec, WorktreeOptions,
WorktreeSandbox, reconnect_for_run_with_callback,
GitSetupIntent, ReadBeforeWriteSandbox, SandboxEventCallback, SandboxSpec,
reconnect_for_run_with_callback,
};
use fabro_static::EnvVars;
use fabro_vault::Vault;
@ -29,72 +27,17 @@ use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec}
use crate::devcontainer_bridge::{devcontainer_to_snapshot_config, run_devcontainer_lifecycle};
use crate::error::Error;
use crate::event::{Event, RunNoticeCode, RunNoticeLevel};
use crate::git::RUN_BRANCH_PREFIX;
use crate::github_token_source::{AppIatMinter, GitHubTokenSource};
use crate::handler::llm::{AgentApiBackend, AgentCliBackend, BackendRouter};
use crate::handler::{HandlerRegistry, default_registry};
use crate::run_metadata::{RunMetadataRuntime, build_metadata_writer, metadata_branch_name};
use crate::run_options::{GitCheckpointOptions, RunOptions};
use crate::sandbox_git::GIT_REMOTE;
use crate::sandbox_git_runtime::SandboxGitRuntime;
use crate::services::{EngineServices, RunServices, WorkflowToolEnvProvider};
use crate::steering_hub::SteeringHub;
struct WorktreePlan {
branch_name: String,
base_sha: Option<String>,
worktree_path: PathBuf,
skip_branch_creation: bool,
}
type BuiltSandboxEnv = (HashMap<String, String>, Option<Arc<GitHubTokenSource>>);
async fn resolve_worktree_base_sha(
sandbox: &dyn Sandbox,
plan: &WorktreePlan,
) -> Result<Option<String>, Error> {
if let Some(base_sha) = plan.base_sha.as_ref() {
return Ok(Some(base_sha.clone()));
}
let result = sandbox
.exec_command(
&format!("{GIT_REMOTE} rev-parse HEAD"),
10_000,
None,
None,
None,
)
.await
.map_err(|err| Error::engine_with_source("git rev-parse HEAD failed", &err))?;
if !result.is_success() {
let output = result.stderr.trim();
let output = if output.is_empty() {
result.stdout.trim()
} else {
output
};
if is_not_git_repository(output) {
return Ok(None);
}
return Err(Error::engine(format!(
"git rev-parse HEAD failed (exit {}): {}",
result.display_exit_code(),
output
)));
}
let base_sha = result.stdout.trim();
if base_sha.is_empty() {
return Err(Error::engine("git rev-parse HEAD returned no commit sha"));
}
Ok(Some(base_sha.to_string()))
}
fn is_not_git_repository(output: &str) -> bool {
output.contains("not a git repository") || output.contains("ambiguous argument 'HEAD'")
}
async fn run_hooks(
hook_runner: Option<&HookRunner>,
hook_context: &HookContext,
@ -107,110 +50,6 @@ async fn run_hooks(
runner.run(hook_context, sandbox, work_dir).await
}
fn resolve_worktree_plan(options: &mut InitOptions) -> Option<WorktreePlan> {
let Some(worktree_mode) = options.worktree_mode else {
options.run_options.display_base_sha = None;
return None;
};
let is_local = matches!(options.sandbox, SandboxSpec::Local { .. });
if options.checkpoint.is_some() && is_local {
if let Some(fork_source) = options.run_options.fork_source_ref.as_ref() {
let base_sha = fork_source.checkpoint_sha.clone();
options.run_options.display_base_sha = Some(base_sha.clone());
return Some(WorktreePlan {
branch_name: format!("{RUN_BRANCH_PREFIX}{}", options.run_id),
base_sha: Some(base_sha),
worktree_path: RunScratch::new(&options.run_options.run_dir).worktree_dir(),
skip_branch_creation: false,
});
}
if let Some(git) = options.run_options.git.as_ref() {
if let (Some(run_branch), Some(base_sha)) = (&git.run_branch, &git.base_sha) {
options.run_options.display_base_sha = Some(base_sha.clone());
return Some(WorktreePlan {
branch_name: run_branch.clone(),
base_sha: Some(base_sha.clone()),
worktree_path: RunScratch::new(&options.run_options.run_dir)
.worktree_dir(),
skip_branch_creation: true,
});
}
}
}
let local_dirty = options
.run_options
.pre_run_git
.as_ref()
.map(|git| git.dirty);
if matches!(local_dirty, Some(fabro_types::DirtyStatus::Dirty)) {
let env_name = if !is_local {
Some("remote sandbox")
} else if worktree_mode == WorktreeMode::Never {
None
} else {
Some("worktree")
};
if let Some(env_name) = env_name {
options.emitter.notice(
RunNoticeLevel::Warn,
RunNoticeCode::DirtyWorktree,
format!("Uncommitted changes will not be included in the {env_name}."),
);
}
}
if !is_local {
options.run_options.display_base_sha = options
.run_options
.pre_run_git
.as_ref()
.and_then(|git| git.sha.clone());
return None;
}
if worktree_mode == WorktreeMode::Never {
options.run_options.display_base_sha = None;
return None;
}
let (branch_name, base_sha) =
if let Some(fork_source) = options.run_options.fork_source_ref.as_ref() {
(
format!("{RUN_BRANCH_PREFIX}{}", options.run_id),
Some(fork_source.checkpoint_sha.clone()),
)
} else {
(
format!("{RUN_BRANCH_PREFIX}{}", options.run_id),
options
.run_options
.pre_run_git
.as_ref()
.and_then(|git| git.sha.clone()),
)
};
options.run_options.display_base_sha.clone_from(&base_sha);
Some(WorktreePlan {
branch_name,
base_sha,
worktree_path: RunScratch::new(&options.run_options.run_dir).worktree_dir(),
skip_branch_creation: false,
})
}
fn worktree_skipped_notice(mode: Option<WorktreeMode>) -> Option<(RunNoticeCode, &'static str)> {
matches!(mode, Some(WorktreeMode::Always)).then_some((
RunNoticeCode::WorktreeSkippedNoGit,
"Worktree mode `always` requested but no Git repository was found; running without a \
worktree.",
))
}
fn git_setup_intent(run_options: &RunOptions) -> GitSetupIntent {
if let Some(source) = run_options.fork_source_ref.as_ref() {
GitSetupIntent::ForkFromCheckpoint {
@ -489,11 +328,28 @@ pub async fn initialize(
resolve_devcontainer(&mut options).await?;
let attach_existing = options.checkpoint.is_some();
let worktree_plan = if attach_existing {
None
} else {
resolve_worktree_plan(&mut options)
};
options.run_options.display_base_sha = options
.run_options
.pre_run_git
.as_ref()
.and_then(|git| git.sha.clone());
if !attach_existing
&& !matches!(options.sandbox, SandboxSpec::Local { .. })
&& matches!(
options
.run_options
.pre_run_git
.as_ref()
.map(|git| git.dirty),
Some(fabro_types::DirtyStatus::Dirty)
)
{
options.emitter.notice(
RunNoticeLevel::Warn,
RunNoticeCode::DirtyWorktree,
"Uncommitted changes will not be included in the remote sandbox.",
);
}
let sandbox_event_callback: SandboxEventCallback = {
let emitter = Arc::clone(&options.emitter);
@ -501,7 +357,6 @@ pub async fn initialize(
emitter.emit(&Event::Sandbox { event });
})
};
let mut worktree_created = false;
let mut sandbox_initialized = true;
let sandbox: Arc<dyn Sandbox> = if attach_existing {
let run_state = options
@ -530,43 +385,6 @@ pub async fn initialize(
.map_err(|err| Error::engine_with_anyhow("Failed to reconnect sandbox for resume", &err))?;
sandbox_initialized = false;
Arc::new(ReadBeforeWriteSandbox::new(Arc::from(sandbox)))
} else if let Some(plan) = worktree_plan.as_ref() {
let inner = options
.sandbox
.build(Some(Arc::clone(&sandbox_event_callback)))
.await
.map_err(|e| Error::engine_with_anyhow("Failed to build sandbox", &e))?;
if let Some(base_sha) = resolve_worktree_base_sha(&*inner, plan).await? {
sandbox_git
.ensure_git_available(&*inner)
.await
.map_err(|err| Error::engine_with_source("sandbox git unavailable", &err))?;
options.run_options.display_base_sha = Some(base_sha.clone());
options.run_options.git = Some(GitCheckpointOptions {
base_sha: Some(base_sha.clone()),
run_branch: Some(plan.branch_name.clone()),
meta_branch: Some(metadata_branch_name(&options.run_id.to_string())),
});
let mut worktree = WorktreeSandbox::new(inner, WorktreeOptions {
branch_name: plan.branch_name.clone(),
base_sha,
worktree_path: plan.worktree_path.to_string_lossy().into_owned(),
skip_branch_creation: plan.skip_branch_creation,
setup_intent: Some(git_setup_intent(&options.run_options)),
});
worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback());
match worktree.initialize().await {
Ok(()) => {
worktree_created = true;
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree)))
}
Err(e) => {
return Err(Error::engine_with_source("Git worktree setup failed", &e));
}
}
} else {
Arc::new(ReadBeforeWriteSandbox::new(inner))
}
} else {
Arc::new(ReadBeforeWriteSandbox::new(
options
@ -576,16 +394,6 @@ pub async fn initialize(
.map_err(|e| Error::engine_with_anyhow("Failed to build sandbox", &e))?,
))
};
if worktree_plan.is_some() && !worktree_created {
if let Some((code, message)) = worktree_skipped_notice(options.worktree_mode) {
tracing::warn!(
worktree_mode = ?options.worktree_mode,
"worktree skipped: cwd is not a git repository"
);
options.emitter.notice(RunNoticeLevel::Warn, code, message);
}
options.run_options.git = None;
}
let cleanup_guard = (!attach_existing).then(|| {
scopeguard::guard(Arc::clone(&sandbox), |sandbox| {
if let Ok(handle) = Handle::try_current() {
@ -856,7 +664,6 @@ mod tests {
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
use fabro_interview::AutoApproveInterviewer;
use fabro_sandbox::SandboxSpec;
use fabro_sandbox::config::WorktreeMode;
use fabro_store::Database;
use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures};
use fabro_vault::{SecretType, Vault};
@ -979,7 +786,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
},
)
}
@ -1038,7 +844,6 @@ mod tests {
vault: None,
devcontainer: None,
git: None,
worktree_mode: None,
run_control: None,
registry_override: None,
artifact_sink: None,
@ -1050,77 +855,6 @@ mod tests {
(result, events)
}
#[tokio::test]
async fn resolve_worktree_plan_uses_local_worktree_without_pre_run_git_context() {
let temp = tempfile::tempdir().unwrap();
let run_dir = temp.path().join("run");
std::fs::create_dir_all(&run_dir).unwrap();
let store = memory_store();
let emitter = Arc::new(crate::event::Emitter::new(test_run_id()));
let mut options = InitOptions {
run_id: test_run_id(),
run_store: {
let inner = store.create_run(&test_run_id()).await.unwrap();
inner.into()
},
dry_run: false,
emitter: emitter.clone(),
sandbox: SandboxSpec::Local {
working_directory: std::env::current_dir().unwrap(),
},
llm: LlmSpec {
model: "test-model".to_string(),
provider: fabro_llm::Provider::Anthropic,
fallback_chain: Vec::new(),
mcp_servers: Vec::new(),
dry_run: true,
},
interviewer: Arc::new(AutoApproveInterviewer::engine()),
steering_hub: Arc::new(crate::steering_hub::SteeringHub::new(emitter.clone())),
lifecycle: crate::run_options::LifecycleOptions {
setup_commands: vec![],
setup_command_timeout_ms: 1_000,
devcontainer_phases: vec![],
},
run_options: test_settings(&run_dir),
workflow_path: None,
workflow_bundle: None,
hooks: fabro_hooks::HookSettings { hooks: vec![] },
sandbox_env: SandboxEnvSpec {
devcontainer_env: HashMap::new(),
toml_env: HashMap::new(),
github_permissions: None,
origin_url: None,
},
vault: None,
devcontainer: None,
git: None,
worktree_mode: Some(WorktreeMode::Always),
run_control: None,
registry_override: None,
artifact_sink: None,
checkpoint: None,
seed_context: None,
};
let plan = resolve_worktree_plan(&mut options);
assert!(plan.is_some());
assert!(options.run_options.display_base_sha.is_none());
assert!(options.run_options.git.is_none());
}
#[test]
fn worktree_skipped_notice_only_warns_for_always() {
assert!(worktree_skipped_notice(None).is_none());
assert!(worktree_skipped_notice(Some(WorktreeMode::Clean)).is_none());
assert!(worktree_skipped_notice(Some(WorktreeMode::Dirty)).is_none());
assert!(worktree_skipped_notice(Some(WorktreeMode::Never)).is_none());
let (code, _) = worktree_skipped_notice(Some(WorktreeMode::Always)).unwrap();
assert_eq!(code, RunNoticeCode::WorktreeSkippedNoGit);
}
#[tokio::test]
async fn initialize_prepares_sandbox_and_uses_persisted_run_dir() {
let temp = tempfile::tempdir().unwrap();
@ -1169,7 +903,6 @@ mod tests {
vault: None,
devcontainer: None,
git: None,
worktree_mode: None,
run_control: None,
registry_override: None,
artifact_sink: None,
@ -1310,7 +1043,6 @@ mod tests {
vault: None,
devcontainer: None,
git: None,
worktree_mode: None,
run_control: None,
registry_override: None,
artifact_sink: None,
@ -1425,7 +1157,6 @@ mod tests {
vault: None,
devcontainer: None,
git: None,
worktree_mode: None,
run_control: None,
registry_override: None,
artifact_sink: None,
@ -1491,7 +1222,6 @@ mod tests {
vault: None,
devcontainer: None,
git: None,
worktree_mode: None,
run_control: None,
registry_override: None,
artifact_sink: None,

View file

@ -152,7 +152,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
}
}
@ -175,7 +174,6 @@ mod tests {
manifest_blob: None,
git: record.git.clone(),
fork_source_ref: record.fork_source_ref.clone(),
in_place: record.in_place,
web_url: None,
})
.await

View file

@ -1040,7 +1040,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
};
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
run_id: fixtures::RUN_1,
@ -1058,7 +1057,6 @@ mod tests {
manifest_blob: None,
git: run_spec.git.clone(),
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await
@ -1107,7 +1105,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
};
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
run_id: fixtures::RUN_1,
@ -1125,7 +1122,6 @@ mod tests {
manifest_blob: None,
git: run_spec.git.clone(),
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await
@ -1457,7 +1453,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
};
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
run_id: fixtures::RUN_1,
@ -1475,7 +1470,6 @@ mod tests {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await
@ -1569,7 +1563,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
};
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
run_id: fixtures::RUN_1,
@ -1587,7 +1580,6 @@ mod tests {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await
@ -1739,7 +1731,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
};
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
run_id: fixtures::RUN_1,
@ -1757,7 +1748,6 @@ mod tests {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await

View file

@ -8,7 +8,6 @@ use fabro_llm::Provider;
use fabro_mcp::config::McpServerSettings;
use fabro_model::FallbackTarget;
use fabro_sandbox::SandboxSpec;
use fabro_sandbox::config::WorktreeMode;
use fabro_types::RunId;
use fabro_types::settings::run::PullRequestSettings;
use fabro_validate::{Diagnostic, Severity};
@ -250,7 +249,6 @@ pub struct InitOptions {
pub vault: Option<Arc<AsyncRwLock<Vault>>>,
pub devcontainer: Option<DevcontainerSpec>,
pub git: Option<GitCheckpointOptions>,
pub worktree_mode: Option<WorktreeMode>,
pub registry_override: Option<Arc<HandlerRegistry>>,
pub artifact_sink: Option<ArtifactSink>,
pub run_control: Option<Arc<RunControlState>>,

View file

@ -451,7 +451,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
}
}
@ -480,7 +479,6 @@ mod tests {
manifest_blob: None,
git: run_spec.git.clone(),
fork_source_ref: run_spec.fork_source_ref.clone(),
in_place: run_spec.in_place,
web_url: None,
})
.await

View file

@ -629,7 +629,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
});
let mut dump = RunDump::from_projection(&projection).unwrap();

View file

@ -150,7 +150,6 @@ mod tests {
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
in_place: false,
}
}
@ -174,7 +173,6 @@ mod tests {
manifest_blob: None,
git: None,
fork_source_ref: None,
in_place: false,
web_url: None,
})
.await

View file

@ -126,7 +126,6 @@ async fn initialized(
manifest_blob: None,
git: run_options.pre_run_git.clone(),
fork_source_ref: run_options.fork_source_ref.clone(),
in_place: false,
web_url: None,
})
.await

View file

@ -77,7 +77,6 @@ models/diff-file.ts
models/diff-stats.ts
models/diff-summary.ts
models/dirty-status.ts
models/discord-integration-settings.ts
models/disk-usage-response.ts
models/disk-usage-run-row.ts
models/disk-usage-summary-row.ts
@ -139,7 +138,6 @@ models/interview-provider-settings.ts
models/interview-question-record.ts
models/ip-allow-entry.ts
models/literal-ip-allow-entry.ts
models/local-sandbox-settings.ts
models/log-destination.ts
models/manifest-args.ts
models/manifest-config.ts
@ -328,7 +326,6 @@ models/system-info-response.ts
models/system-repair-run-issue.ts
models/system-repair-runs-response.ts
models/system-run-counts.ts
models/teams-integration-settings.ts
models/terminal-status.ts
models/timeline-entry-response.ts
models/tls-mode.ts
@ -344,5 +341,4 @@ models/workflow-namespace.ts
models/workflow-reference.ts
models/workflow-schedule-summary.ts
models/workflow-settings.ts
models/worktree-mode.ts
models/write-blob-response.ts

View file

@ -1,20 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface DiscordIntegrationSettings {
'enabled': boolean;
}

View file

@ -55,7 +55,6 @@ export * from './diff-file';
export * from './diff-stats';
export * from './diff-summary';
export * from './dirty-status';
export * from './discord-integration-settings';
export * from './disk-usage-response';
export * from './disk-usage-run-row';
export * from './disk-usage-summary-row';
@ -116,7 +115,6 @@ export * from './interview-provider-settings';
export * from './interview-question-record';
export * from './ip-allow-entry';
export * from './literal-ip-allow-entry';
export * from './local-sandbox-settings';
export * from './log-destination';
export * from './manifest-args';
export * from './manifest-config';
@ -305,7 +303,6 @@ export * from './system-info-response';
export * from './system-repair-run-issue';
export * from './system-repair-runs-response';
export * from './system-run-counts';
export * from './teams-integration-settings';
export * from './terminal-status';
export * from './timeline-entry-response';
export * from './tls-mode';
@ -321,5 +318,4 @@ export * from './workflow-namespace';
export * from './workflow-reference';
export * from './workflow-schedule-summary';
export * from './workflow-settings';
export * from './worktree-mode';
export * from './write-blob-response';

View file

@ -1,25 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { WorktreeMode } from './worktree-mode';
export interface LocalSandboxSettings {
'worktree_mode': WorktreeMode;
}

View file

@ -29,10 +29,6 @@ export interface ManifestArgs {
'dry_run'?: boolean;
'auto_approve'?: boolean;
'preserve_sandbox'?: boolean;
/**
* Override `run.sandbox.local.worktree_mode` (e.g. `never` for `--in-place`).
*/
'worktree_mode'?: string;
'label'?: Array<string>;
/**
* Raw repeated CLI input overrides, each in `KEY=VALUE` form.

View file

@ -22,7 +22,5 @@ export interface NotificationRouteSettings {
'provider': string | null;
'events': Array<string>;
'slack': NotificationProviderSettings | null;
'discord': NotificationProviderSettings | null;
'teams': NotificationProviderSettings | null;
}

View file

@ -17,7 +17,6 @@
export interface ProjectNamespace {
'name': string | null;
'description': string | null;
'directory': string;
'metadata': { [key: string]: string; };
}

View file

@ -20,7 +20,5 @@ import type { InterviewProviderSettings } from './interview-provider-settings';
export interface RunInterviewsSettings {
'provider': string | null;
'slack': InterviewProviderSettings | null;
'discord': InterviewProviderSettings | null;
'teams': InterviewProviderSettings | null;
}

View file

@ -54,7 +54,6 @@ export interface RunListItem {
'status': RunStatus;
'labels': { [key: string]: string; };
'source_directory'?: string | null;
'in_place'?: boolean;
'repo_origin_url'?: string | null;
'start_time'?: string | null;
'pending_control'?: RunControlAction | null;

View file

@ -19,9 +19,6 @@ import type { DaytonaSettings } from './daytona-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { DockerSettings } from './docker-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { LocalSandboxSettings } from './local-sandbox-settings';
export interface RunSandboxSettings {
'provider': string;
@ -29,7 +26,6 @@ export interface RunSandboxSettings {
'stop_on_terminal': boolean;
'devcontainer': boolean;
'env': { [key: string]: string; };
'local': LocalSandboxSettings;
'docker': DockerSettings | null;
'daytona': DaytonaSettings | null;
}

View file

@ -41,6 +41,5 @@ export interface RunSpec {
'definition_blob'?: string | null;
'git'?: GitContext | null;
'fork_source_ref'?: ForkSourceRef | null;
'in_place': boolean;
}

Some files were not shown because too many files have changed in this diff Show more