diff --git a/apps/fabro-web/app/layouts/app-shell.test.tsx b/apps/fabro-web/app/layouts/app-shell.test.tsx index 8cffba752..aa11bbcfa 100644 --- a/apps/fabro-web/app/layouts/app-shell.test.tsx +++ b/apps/fabro-web/app/layouts/app-shell.test.tsx @@ -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"); diff --git a/apps/fabro-web/app/layouts/app-shell.tsx b/apps/fabro-web/app/layouts/app-shell.tsx index 3c1fd0148..9ef31802d 100644 --- a/apps/fabro-web/app/layouts/app-shell.tsx +++ b/apps/fabro-web/app/layouts/app-shell.tsx @@ -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 }, diff --git a/apps/fabro-web/app/routes/run-settings.tsx b/apps/fabro-web/app/routes/run-settings.tsx index 42796d5e5..b641c0547 100644 --- a/apps/fabro-web/app/routes/run-settings.tsx +++ b/apps/fabro-web/app/routes/run-settings.tsx @@ -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 ( @@ -152,11 +150,6 @@ function SandboxPanel({ snapshot }: { snapshot: Snapshot }) { {dockerImage} ) : null} - {provider === "local" && worktreeMode ? ( - - {worktreeMode} - - ) : null} diff --git a/apps/fabro-web/app/routes/workflow-detail.tsx b/apps/fabro-web/app/routes/workflow-detail.tsx index df5959421..c4a7764eb 100644 --- a/apps/fabro-web/app/routes/workflow-detail.tsx +++ b/apps/fabro-web/app/routes/workflow-detail.tsx @@ -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 }, diff --git a/docs/brainstorms/2026-04-08-settings-toml-redesign-requirements.md b/docs/brainstorms/2026-04-08-settings-toml-redesign-requirements.md index 412981a1e..6dd9f4c7f 100644 --- a/docs/brainstorms/2026-04-08-settings-toml-redesign-requirements.md +++ b/docs/brainstorms/2026-04-08-settings-toml-redesign-requirements.md @@ -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.]`; 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. diff --git a/docs/plans/2026-04-08-settings-toml-redesign-implementation-plan.md b/docs/plans/2026-04-08-settings-toml-redesign-implementation-plan.md index c20894061..18da2ae90 100644 --- a/docs/plans/2026-04-08-settings-toml-redesign-implementation-plan.md +++ b/docs/plans/2026-04-08-settings-toml-redesign-implementation-plan.md @@ -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..]` - - 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 diff --git a/docs/plans/2026-04-09-settings-toml-redesign-handoff.md b/docs/plans/2026-04-09-settings-toml-redesign-handoff.md index 4a4206125..f6b92b790 100644 --- a/docs/plans/2026-04-09-settings-toml-redesign-handoff.md +++ b/docs/plans/2026-04-09-settings-toml-redesign-handoff.md @@ -560,7 +560,7 @@ These are lessons learned during Stages 1–5. 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 diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 2127799e3..88e2b1f19 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -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 diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx index 60c2cc102..aceef5f3a 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -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]` diff --git a/docs/public/reference/cli.mdx b/docs/public/reference/cli.mdx index 2729b5902..272320d8d 100644 --- a/docs/public/reference/cli.mdx +++ b/docs/public/reference/cli.mdx @@ -303,7 +303,6 @@ fabro create [OPTIONS] | `--dry-run` | Execute with simulated LLM backend | | `--goal ` | Override the workflow goal (available as {{ goal }} in prompts) | | `--goal-file ` | Read the workflow goal from a file | -| `--in-place` | Run directly in the source checkout without git checkpoints | | `--label ` | Attach a label to this run (repeatable, format: KEY=VALUE) | | `--model ` | Override default LLM model | | `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) | @@ -841,7 +840,6 @@ fabro run [OPTIONS] | `--dry-run` | Execute with simulated LLM backend | | `--goal ` | Override the workflow goal (available as {{ goal }} in prompts) | | `--goal-file ` | Read the workflow goal from a file | -| `--in-place` | Run directly in the source checkout without git checkpoints | | `--label ` | Attach a label to this run (repeatable, format: KEY=VALUE) | | `--model ` | Override default LLM model | | `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) | diff --git a/docs/superpowers/plans/2026-05-09-fixed-project-workflow-directory.md b/docs/superpowers/plans/2026-05-09-fixed-project-workflow-directory.md index 7189ab5f1..06a02953f 100644 --- a/docs/superpowers/plans/2026-05-09-fixed-project-workflow-directory.md +++ b/docs/superpowers/plans/2026-05-09-fixed-project-workflow-directory.md @@ -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 `/.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 `/.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` and use `/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 `/.fabro/workflows//`. **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 ` writes `.fabro/workflows//workflow.{fabro,toml}`. + - Confirm `fabro workflow create ` writes both `.fabro/workflows//workflow.fabro` and `.fabro/workflows//workflow.toml`. - Confirm named workflow resolution reads `.fabro/workflows//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. diff --git a/docs/superpowers/plans/2026-05-09-remove-local-worktree-mode.md b/docs/superpowers/plans/2026-05-09-remove-local-worktree-mode.md index 94731137d..98c0b7e92 100644 --- a/docs/superpowers/plans/2026-05-09-remove-local-worktree-mode.md +++ b/docs/superpowers/plans/2026-05-09-remove-local-worktree-mode.md @@ -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 diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index bb29e8d8b..98018abc2 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -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", diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index 6c393793f..a8d45b141 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -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::{ diff --git a/lib/crates/fabro-api/tests/run_event_round_trip.rs b/lib/crates/fabro-api/tests/run_event_round_trip.rs index b9d150fe3..1b04ef29f 100644 --- a/lib/crates/fabro-api/tests/run_event_round_trip.rs +++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs @@ -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) } }); diff --git a/lib/crates/fabro-api/tests/run_summary_round_trip.rs b/lib/crates/fabro-api/tests/run_summary_round_trip.rs index f510ba1a0..c05762bb5 100644 --- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs +++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs @@ -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" diff --git a/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs b/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs index 4c4212c36..50c05af3a 100644 --- a/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs +++ b/lib/crates/fabro-api/tests/workflow_settings_round_trip.rs @@ -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"); diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 7f359a1f6..810343cf8 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -246,10 +246,6 @@ pub(crate) struct RunArgs { #[arg(long, value_enum)] pub(crate) sandbox: Option, - /// 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, diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 855c1eae4..ba718853d 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -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 { diff --git a/lib/crates/fabro-cli/src/commands/workflow/create.rs b/lib/crates/fabro-cli/src/commands/workflow/create.rs index 3a23280e5..813ce45ef 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/create.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/create.rs @@ -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(); diff --git a/lib/crates/fabro-cli/src/commands/workflow/list.rs b/lib/crates/fabro-cli/src/commands/workflow/list.rs index 16846762d..a65f32360 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/list.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/list.rs @@ -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()); diff --git a/lib/crates/fabro-cli/src/manifest_builder.rs b/lib/crates/fabro-cli/src/manifest_builder.rs index 8c91b1fbd..b2436db77 100644 --- a/lib/crates/fabro-cli/src/manifest_builder.rs +++ b/lib/crates/fabro-cli/src/manifest_builder.rs @@ -182,15 +182,10 @@ pub(crate) fn run_manifest_args(args: &RunArgs) -> Option { 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 bool { && args.docker_image.is_none() && args.input.is_empty() && args.verbose.is_none() - && args.worktree_mode.is_none() } #[cfg(test)] diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/crates/fabro-cli/src/server_runs.rs index c4386420f..e45666a1d 100644 --- a/lib/crates/fabro-cli/src/server_runs.rs +++ b/lib/crates/fabro-cli/src/server_runs.rs @@ -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() } diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index 54a4c36a1..c5d32ffe8 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -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", diff --git a/lib/crates/fabro-cli/tests/it/cmd/create.rs b/lib/crates/fabro-cli/tests/it/cmd/create.rs index e33358b10..d80eb76e0 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/create.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/create.rs @@ -54,7 +54,6 @@ fn help() { --provider Override default LLM provider -v, --verbose Enable verbose output --sandbox Sandbox for agent tools [possible values: local, docker, daytona] - --in-place Run directly in the source checkout without git checkpoints --label 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 diff --git a/lib/crates/fabro-cli/tests/it/cmd/dump.rs b/lib/crates/fabro-cli/tests/it/cmd/dump.rs index f15e7dccb..d7c26eb51 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/dump.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/dump.rs @@ -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 diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index a4210a461..a2a6bcf7e 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -150,7 +150,6 @@ fn help() { --provider Override default LLM provider -v, --verbose Enable verbose output --sandbox Sandbox for agent tools [possible values: local, docker, daytona] - --in-place Run directly in the source checkout without git checkpoints --label 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] diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index 23c8c431b..6fc350161 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -349,8 +349,6 @@ goal = "Exercise sandbox commands" provider = "local" preserve = true -[run.sandbox.local] -worktree_mode = "never" "#, ); diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow.rs b/lib/crates/fabro-cli/tests/it/cmd/workflow.rs index c66c89c9d..b3eb7f0f2 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/workflow.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/workflow.rs @@ -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 "); diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs b/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs index 96bc7bcaf..046471a0e 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs @@ -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" + ); } diff --git a/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs b/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs index c3f8d836c..bb8a50c85 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs @@ -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] diff --git a/lib/crates/fabro-config/src/builders.rs b/lib/crates/fabro-config/src/builders.rs index a18ea8a7c..ab704b1ee 100644 --- a/lib/crates/fabro-config/src/builders.rs +++ b/lib/crates/fabro-config/src/builders.rs @@ -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 { - 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 { diff --git a/lib/crates/fabro-config/src/defaults.toml b/lib/crates/fabro-config/src/defaults.toml index 11041787e..718046eed 100644 --- a/lib/crates/fabro-config/src/defaults.toml +++ b/lib/crates/fabro-config/src/defaults.toml @@ -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" diff --git a/lib/crates/fabro-config/src/layers/combine.rs b/lib/crates/fabro-config/src/layers/combine.rs index 58a0f2a03..072b37fda 100644 --- a/lib/crates/fabro-config/src/layers/combine.rs +++ b/lib/crates/fabro-config/src/layers/combine.rs @@ -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, diff --git a/lib/crates/fabro-config/src/layers/mod.rs b/lib/crates/fabro-config/src/layers/mod.rs index 22fb9b490..9d35995d6 100644 --- a/lib/crates/fabro-config/src/layers/mod.rs +++ b/lib/crates/fabro-config/src/layers/mod.rs @@ -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; diff --git a/lib/crates/fabro-config/src/layers/project.rs b/lib/crates/fabro-config/src/layers/project.rs index 51d2c7110..66123cec9 100644 --- a/lib/crates/fabro-config/src/layers/project.rs +++ b/lib/crates/fabro-config/src/layers/project.rs @@ -12,8 +12,8 @@ pub struct ProjectLayer { pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - /// 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, #[serde(default, skip_serializing_if = "ReplaceMap::is_empty")] diff --git a/lib/crates/fabro-config/src/layers/run.rs b/lib/crates/fabro-config/src/layers/run.rs index 3a2610054..aad3e97dd 100644 --- a/lib/crates/fabro-config/src/layers/run.rs +++ b/lib/crates/fabro-config/src/layers/run.rs @@ -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, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] pub docker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub daytona: Option, } -#[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, -} - #[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, - /// Provider-specific destination subtables. First-pass chat providers. + /// Provider-specific destination subtables. #[serde(default, skip_serializing_if = "Option::is_none")] pub slack: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, } /// A single string array entry that may be the splice marker. @@ -396,10 +382,6 @@ pub struct InterviewsLayer { pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub slack: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] diff --git a/lib/crates/fabro-config/src/layers/server.rs b/lib/crates/fabro-config/src/layers/server.rs index f3a6f9a2c..bc03a1824 100644 --- a/lib/crates/fabro-config/src/layers/server.rs +++ b/lib/crates/fabro-config/src/layers/server.rs @@ -189,21 +189,15 @@ pub struct ServerLoggingLayer { pub destination: Option, } -/// `[server.integrations.]` — 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.]` — 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, + pub github: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, + pub slack: Option, } /// `[server.integrations.github]` — GitHub App, credentials, and inbound @@ -235,22 +229,6 @@ pub struct SlackIntegrationLayer { pub default_channel: Option, } -/// `[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, -} - -/// `[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, -} - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] #[serde(deny_unknown_fields)] pub struct IntegrationWebhooksLayer { diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index 73dc06989..076a5ce7e 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -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}; diff --git a/lib/crates/fabro-config/src/parse.rs b/lib/crates/fabro-config/src/parse.rs index 7fb3becd9..7fd413a58 100644 --- a/lib/crates/fabro-config/src/parse.rs +++ b/lib/crates/fabro-config/src/parse.rs @@ -113,7 +113,7 @@ fn rename_hint(key: &str) -> Option { "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 \ diff --git a/lib/crates/fabro-config/src/project.rs b/lib/crates/fabro-config/src/project.rs index 3c5bf48a7..75c395cff 100644 --- a/lib/crates/fabro-config/src/project.rs +++ b/lib/crates/fabro-config/src/project.rs @@ -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, } -/// 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 { - 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> { @@ -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 { 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::() + .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") ); } diff --git a/lib/crates/fabro-config/src/resolve/project.rs b/lib/crates/fabro-config/src/resolve/project.rs index 2296acc5c..226784854 100644 --- a/lib/crates/fabro-config/src/resolve/project.rs +++ b/lib/crates/fabro-config/src/resolve/project.rs @@ -7,10 +7,6 @@ pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> 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(), } } diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs index aa3b150dc..1a85289f2 100644 --- a/lib/crates/fabro-config/src/resolve/run.rs +++ b/lib/crates/fabro-config/src/resolve/run.rs @@ -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), } } diff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs index 902b1ff3c..f6c22c699 100644 --- a/lib/crates/fabro-config/src/resolve/server.rs +++ b/lib/crates/fabro-config/src/resolve/server.rs @@ -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, ) -> 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(), } } diff --git a/lib/crates/fabro-config/src/tests/combine.rs b/lib/crates/fabro-config/src/tests/combine.rs index a3576dd50..51b0a68fe 100644 --- a/lib/crates/fabro-config/src/tests/combine.rs +++ b/lib/crates/fabro-config/src/tests/combine.rs @@ -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]] diff --git a/lib/crates/fabro-config/src/tests/defaults.rs b/lib/crates/fabro-config/src/tests/defaults.rs index 5aecf5f68..79b652084 100644 --- a/lib/crates/fabro-config/src/tests/defaults.rs +++ b/lib/crates/fabro-config/src/tests/defaults.rs @@ -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 diff --git a/lib/crates/fabro-config/src/tests/resolve_project.rs b/lib/crates/fabro-config/src/tests/resolve_project.rs index 21c62d00c..b23b0971b 100644 --- a/lib/crates/fabro-config/src/tests/resolve_project.rs +++ b/lib/crates/fabro-config/src/tests/resolve_project.rs @@ -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") diff --git a/lib/crates/fabro-config/src/tests/resolve_root.rs b/lib/crates/fabro-config/src/tests/resolve_root.rs index 1d75d6221..99fe0fef6 100644 --- a/lib/crates/fabro-config/src/tests/resolve_root.rs +++ b/lib/crates/fabro-config/src/tests/resolve_root.rs @@ -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); } diff --git a/lib/crates/fabro-config/src/tests/resolve_run.rs b/lib/crates/fabro-config/src/tests/resolve_run.rs index f63814e91..7a0501082 100644 --- a/lib/crates/fabro-config/src/tests/resolve_run.rs +++ b/lib/crates/fabro-config/src/tests/resolve_run.rs @@ -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::() + .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::() + .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( diff --git a/lib/crates/fabro-config/src/tests/resolve_server.rs b/lib/crates/fabro-config/src/tests/resolve_server.rs index a306a9800..7edb327c3 100644 --- a/lib/crates/fabro-config/src/tests/resolve_server.rs +++ b/lib/crates/fabro-config/src/tests/resolve_server.rs @@ -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::() + .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( diff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs index 560f23043..eb95214b9 100644 --- a/lib/crates/fabro-dump/src/lib.rs +++ b/lib/crates/fabro-dump/src/lib.rs @@ -498,7 +498,6 @@ mod tests { manifest_blob: None, definition_blob: None, fork_source_ref: None, - in_place: false, } } diff --git a/lib/crates/fabro-redact/data/gitleaks.toml b/lib/crates/fabro-redact/data/gitleaks.toml index d4115601b..055e09305 100644 --- a/lib/crates/fabro-redact/data/gitleaks.toml +++ b/lib/crates/fabro-redact/data/gitleaks.toml @@ -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"] - diff --git a/lib/crates/fabro-redact/src/jsonl.rs b/lib/crates/fabro-redact/src/jsonl.rs index 2a14131e4..e466f7002 100644 --- a/lib/crates/fabro-redact/src/jsonl.rs +++ b/lib/crates/fabro-redact/src/jsonl.rs @@ -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 } diff --git a/lib/crates/fabro-sandbox/src/config.rs b/lib/crates/fabro-sandbox/src/config.rs index 4f5ed268c..351865808 100644 --- a/lib/crates/fabro-sandbox/src/config.rs +++ b/lib/crates/fabro-sandbox/src/config.rs @@ -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, pub dockerfile: Option, } - -#[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, - } -} diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index d14307c2a..651067d42 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -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), diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index 839c2fd8f..0bced72f6 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -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 { - 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 { 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(); diff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs index dc0dadfc4..5b6002bb4 100644 --- a/lib/crates/fabro-server/src/server/tests.rs +++ b/lib/crates/fabro-server/src/server/tests.rs @@ -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 { diff --git a/lib/crates/fabro-server/tests/it/api/runs.rs b/lib/crates/fabro-server/tests/it/api/runs.rs index 08909fe46..c8fb2f609 100644 --- a/lib/crates/fabro-server/tests/it/api/runs.rs +++ b/lib/crates/fabro-server/tests/it/api/runs.rs @@ -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"); diff --git a/lib/crates/fabro-server/tests/it/helpers.rs b/lib/crates/fabro-server/tests/it/helpers.rs index 41f43d7a9..32927c684 100644 --- a/lib/crates/fabro-server/tests/it/helpers.rs +++ b/lib/crates/fabro-server/tests/it/helpers.rs @@ -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() diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs index 9ef5dc6a0..e184bba6b 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -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(); diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs index 8cde4e350..be0d9c651 100644 --- a/lib/crates/fabro-store/src/slate/mod.rs +++ b/lib/crates/fabro-store/src/slate/mod.rs @@ -478,7 +478,6 @@ mod tests { push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, }), fork_source_ref: None, - in_place: false, } } diff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs index 85565c402..5fed2cf4a 100644 --- a/lib/crates/fabro-store/tests/serializable_projection.rs +++ b/lib/crates/fabro-store/tests/serializable_projection.rs @@ -29,7 +29,6 @@ fn sample_run_spec() -> RunSpec { push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, }), fork_source_ref: None, - in_place: false, } } diff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs index 79dd74923..3f3aef84c 100644 --- a/lib/crates/fabro-types/src/run.rs +++ b/lib/crates/fabro-types/src/run.rs @@ -98,8 +98,6 @@ pub struct RunSpec { pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub fork_source_ref: Option, - #[serde(default)] - pub in_place: bool, } impl RunSpec { diff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs index 03db57594..3b932883e 100644 --- a/lib/crates/fabro-types/src/run_event/run.rs +++ b/lib/crates/fabro-types/src/run_event/run.rs @@ -36,8 +36,6 @@ pub struct RunCreatedProps { pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub fork_source_ref: Option, - #[serde(default)] - pub in_place: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub web_url: Option, } diff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs index 5e99e141b..1d76967b5 100644 --- a/lib/crates/fabro-types/src/run_summary.rs +++ b/lib/crates/fabro-types/src/run_summary.rs @@ -20,8 +20,6 @@ pub struct RunSummary { #[serde(default)] pub source_directory: Option, #[serde(default)] - pub in_place: bool, - #[serde(default)] pub repo_origin_url: Option, pub repository: RepositoryReference, #[serde(default)] @@ -59,7 +57,6 @@ impl RunSummary { title: String, labels: HashMap, source_directory: Option, - in_place: bool, repo_origin_url: Option, start_time: Option>, last_event_at: Option>, @@ -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, diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index 54b8c6aa1..388f3f10c 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -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; diff --git a/lib/crates/fabro-types/src/settings/project.rs b/lib/crates/fabro-types/src/settings/project.rs index 7e141dbcb..e6ae3ddf3 100644 --- a/lib/crates/fabro-types/src/settings/project.rs +++ b/lib/crates/fabro-types/src/settings/project.rs @@ -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, pub description: Option, - pub directory: String, pub metadata: HashMap, } diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index a816c7d19..9148d986c 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -201,7 +201,6 @@ pub struct RunSandboxSettings { pub stop_on_terminal: bool, pub devcontainer: bool, pub env: HashMap, - pub local: LocalSandboxSettings, pub docker: Option, pub daytona: Option, } @@ -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, pub events: Vec, pub slack: Option, - pub discord: Option, - pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -322,8 +313,6 @@ pub struct NotificationProviderSettings { pub struct RunInterviewsSettings { pub provider: Option, pub slack: Option, - pub discord: Option, - pub teams: Option, } #[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 { diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index 16a5718f9..fa92d3f64 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -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, } -#[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, diff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs index 336713c63..fccb59eae 100644 --- a/lib/crates/fabro-types/tests/run_event_serde.rs +++ b/lib/crates/fabro-types/tests/run_event_serde.rs @@ -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, }; diff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs index 96b37b909..e8353aaf1 100644 --- a/lib/crates/fabro-types/tests/run_spec_methods.rs +++ b/lib/crates/fabro-types/tests/run_spec_methods.rs @@ -26,7 +26,6 @@ fn sample_run_spec() -> RunSpec { }, }), fork_source_ref: None, - in_place: false, } } diff --git a/lib/crates/fabro-types/tests/run_spec_serde.rs b/lib/crates/fabro-types/tests/run_spec_serde.rs index e00e04210..2c776645e 100644 --- a/lib/crates/fabro-types/tests/run_spec_serde.rs +++ b/lib/crates/fabro-types/tests/run_spec_serde.rs @@ -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"); diff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs index 08d66e5f6..1f116c34f 100644 --- a/lib/crates/fabro-workflow/src/billing_rollup.rs +++ b/lib/crates/fabro-workflow/src/billing_rollup.rs @@ -284,7 +284,6 @@ mod tests { definition_blob: None, git: None, fork_source_ref: None, - in_place: false, } } } diff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs index 966956f88..1dacbb917 100644 --- a/lib/crates/fabro-workflow/src/event/convert.rs +++ b/lib/crates/fabro-workflow/src/event/convert.rs @@ -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"); diff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs index f270c82df..47ee23b2c 100644 --- a/lib/crates/fabro-workflow/src/event/events.rs +++ b/lib/crates/fabro-workflow/src/event/events.rs @@ -43,8 +43,6 @@ pub enum Event { git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] fork_source_ref: Option, - #[serde(default)] - in_place: bool, #[serde(default, skip_serializing_if = "Option::is_none")] web_url: Option, }, diff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs index ae0740aaa..b645e6604 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/git.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs @@ -722,7 +722,6 @@ mod tests { manifest_blob: None, git: None, fork_source_ref: None, - in_place: false, web_url: None, }) .await diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs index cb8dca7c0..f39371b8f 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -45,7 +45,6 @@ pub struct CreateRunInput { pub title: Option, pub git: Option, pub fork_source_ref: Option, - pub in_place: bool, pub provenance: Option, pub configured_providers: Vec, /// Public URL where this run can be viewed in the web UI, when the server @@ -71,7 +70,6 @@ struct PersistCreateOptions { source_directory: Option, git: Option, fork_source_ref: Option, - in_place: bool, provenance: Option, configured_providers: Vec, } @@ -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(), diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs index 9be3b703b..d395d871f 100644 --- a/lib/crates/fabro-workflow/src/operations/fork.rs +++ b/lib/crates/fabro-workflow/src/operations/fork.rs @@ -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 diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index 7973d6018..5378ede6c 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -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, git: Option, github_app: Option, - worktree_mode: Option, registry_override: Option>, 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 sandbox_config::WorktreeMode { - bridge_worktree_mode(settings.sandbox.local.worktree_mode) -} - fn resolve_daytona_config(settings: &ResolvedRunSettings) -> Option { 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, diff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs index e7b81bcd3..36529cc0c 100644 --- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs @@ -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, diff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs index c1ebd48a5..be68d208c 100644 --- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs @@ -739,7 +739,6 @@ mod tests { manifest_blob: None, git: None, fork_source_ref: None, - in_place: false, web_url: None, }) .await diff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs index b12288dd5..9b8c3cb27 100644 --- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs @@ -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, - worktree_path: PathBuf, - skip_branch_creation: bool, -} - type BuiltSandboxEnv = (HashMap, Option>); -async fn resolve_worktree_base_sha( - sandbox: &dyn Sandbox, - plan: &WorktreePlan, -) -> Result, 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 { - 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) -> 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 = 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, diff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs index 30a7ac66b..0518a9f27 100644 --- a/lib/crates/fabro-workflow/src/pipeline/persist.rs +++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs @@ -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 diff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs index e828f2910..18eb69f8d 100644 --- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs @@ -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 diff --git a/lib/crates/fabro-workflow/src/pipeline/types.rs b/lib/crates/fabro-workflow/src/pipeline/types.rs index a09d842c3..6bf06fbf0 100644 --- a/lib/crates/fabro-workflow/src/pipeline/types.rs +++ b/lib/crates/fabro-workflow/src/pipeline/types.rs @@ -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>>, pub devcontainer: Option, pub git: Option, - pub worktree_mode: Option, pub registry_override: Option>, pub artifact_sink: Option, pub run_control: Option>, diff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs index 94a154e6a..ed6e688ff 100644 --- a/lib/crates/fabro-workflow/src/run_lookup.rs +++ b/lib/crates/fabro-workflow/src/run_lookup.rs @@ -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 diff --git a/lib/crates/fabro-workflow/src/run_metadata.rs b/lib/crates/fabro-workflow/src/run_metadata.rs index c508a892c..7c54bd973 100644 --- a/lib/crates/fabro-workflow/src/run_metadata.rs +++ b/lib/crates/fabro-workflow/src/run_metadata.rs @@ -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(); diff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs index 05245a64a..e2994b991 100644 --- a/lib/crates/fabro-workflow/src/runtime_store.rs +++ b/lib/crates/fabro-workflow/src/runtime_store.rs @@ -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 diff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs index c85dab7ca..f4cfd0960 100644 --- a/lib/crates/fabro-workflow/src/test_support.rs +++ b/lib/crates/fabro-workflow/src/test_support.rs @@ -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 diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 8661092f5..47f7a94c5 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -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 diff --git a/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts b/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts deleted file mode 100644 index 5ea86fd09..000000000 --- a/lib/packages/fabro-api-client/src/models/discord-integration-settings.ts +++ /dev/null @@ -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; -} - diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index d9b318057..445cdf4e7 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -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'; diff --git a/lib/packages/fabro-api-client/src/models/local-sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/local-sandbox-settings.ts deleted file mode 100644 index dd9f96f60..000000000 --- a/lib/packages/fabro-api-client/src/models/local-sandbox-settings.ts +++ /dev/null @@ -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; -} - - - diff --git a/lib/packages/fabro-api-client/src/models/manifest-args.ts b/lib/packages/fabro-api-client/src/models/manifest-args.ts index 01e109f4d..c448e0133 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-args.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-args.ts @@ -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; /** * Raw repeated CLI input overrides, each in `KEY=VALUE` form. diff --git a/lib/packages/fabro-api-client/src/models/notification-route-settings.ts b/lib/packages/fabro-api-client/src/models/notification-route-settings.ts index 7acc95c38..85de1af5e 100644 --- a/lib/packages/fabro-api-client/src/models/notification-route-settings.ts +++ b/lib/packages/fabro-api-client/src/models/notification-route-settings.ts @@ -22,7 +22,5 @@ export interface NotificationRouteSettings { 'provider': string | null; 'events': Array; 'slack': NotificationProviderSettings | null; - 'discord': NotificationProviderSettings | null; - 'teams': NotificationProviderSettings | null; } diff --git a/lib/packages/fabro-api-client/src/models/project-namespace.ts b/lib/packages/fabro-api-client/src/models/project-namespace.ts index c9a062096..d5fc1b7ef 100644 --- a/lib/packages/fabro-api-client/src/models/project-namespace.ts +++ b/lib/packages/fabro-api-client/src/models/project-namespace.ts @@ -17,7 +17,6 @@ export interface ProjectNamespace { 'name': string | null; 'description': string | null; - 'directory': string; 'metadata': { [key: string]: string; }; } diff --git a/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts b/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts index 2e935d62e..0caac4b57 100644 --- a/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts @@ -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; } diff --git a/lib/packages/fabro-api-client/src/models/run-list-item.ts b/lib/packages/fabro-api-client/src/models/run-list-item.ts index ecb7b89bd..cb7a3222a 100644 --- a/lib/packages/fabro-api-client/src/models/run-list-item.ts +++ b/lib/packages/fabro-api-client/src/models/run-list-item.ts @@ -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; diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts index ddfede4ee..2746c6ef3 100644 --- a/lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-sandbox-settings.ts @@ -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; } diff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts index 2823de5e5..adc11b685 100644 --- a/lib/packages/fabro-api-client/src/models/run-spec.ts +++ b/lib/packages/fabro-api-client/src/models/run-spec.ts @@ -41,6 +41,5 @@ export interface RunSpec { 'definition_blob'?: string | null; 'git'?: GitContext | null; 'fork_source_ref'?: ForkSourceRef | null; - 'in_place': boolean; } diff --git a/lib/packages/fabro-api-client/src/models/run-summary.ts b/lib/packages/fabro-api-client/src/models/run-summary.ts index eab9c7990..44b7bbd7f 100644 --- a/lib/packages/fabro-api-client/src/models/run-summary.ts +++ b/lib/packages/fabro-api-client/src/models/run-summary.ts @@ -43,7 +43,6 @@ export interface RunSummary { 'title': string; 'labels': { [key: string]: string; }; 'source_directory'?: string | null; - 'in_place'?: boolean; 'repo_origin_url'?: string | null; 'repository': RepositoryReference; 'start_time'?: string | null; diff --git a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts index bee2ea73c..500877d6f 100644 --- a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts @@ -13,23 +13,15 @@ */ -// May contain unused imports in some cases -// @ts-ignore -import type { DiscordIntegrationSettings } from './discord-integration-settings'; // May contain unused imports in some cases // @ts-ignore import type { GithubIntegrationSettings } from './github-integration-settings'; // May contain unused imports in some cases // @ts-ignore import type { SlackIntegrationSettings } from './slack-integration-settings'; -// May contain unused imports in some cases -// @ts-ignore -import type { TeamsIntegrationSettings } from './teams-integration-settings'; export interface ServerIntegrationsSettings { 'github': GithubIntegrationSettings; 'slack': SlackIntegrationSettings; - 'discord': DiscordIntegrationSettings; - 'teams': TeamsIntegrationSettings; } diff --git a/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts b/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts deleted file mode 100644 index be2a971cb..000000000 --- a/lib/packages/fabro-api-client/src/models/teams-integration-settings.ts +++ /dev/null @@ -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 TeamsIntegrationSettings { - 'enabled': boolean; -} - diff --git a/lib/packages/fabro-api-client/src/models/worktree-mode.ts b/lib/packages/fabro-api-client/src/models/worktree-mode.ts deleted file mode 100644 index 96589a739..000000000 --- a/lib/packages/fabro-api-client/src/models/worktree-mode.ts +++ /dev/null @@ -1,28 +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 const WorktreeMode = { - ALWAYS: 'always', - CLEAN: 'clean', - DIRTY: 'dirty', - NEVER: 'never' -} as const; - -export type WorktreeMode = typeof WorktreeMode[keyof typeof WorktreeMode]; - - -