mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
48545f4a7d
104 changed files with 403 additions and 1232 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -140,8 +140,6 @@ function SandboxPanel({ snapshot }: { snapshot: Snapshot }) {
|
|||
const provider = getString(sandbox, "provider");
|
||||
const docker = getObject(sandbox, "docker");
|
||||
const dockerImage = getString(docker, "image");
|
||||
const local = getObject(sandbox, "local");
|
||||
const worktreeMode = getString(local, "worktree_mode");
|
||||
return (
|
||||
<Panel title="Sandbox">
|
||||
<Row title="Provider" help="Execution environment for this run.">
|
||||
|
|
@ -152,11 +150,6 @@ function SandboxPanel({ snapshot }: { snapshot: Snapshot }) {
|
|||
<Mono>{dockerImage}</Mono>
|
||||
</Row>
|
||||
) : null}
|
||||
{provider === "local" && worktreeMode ? (
|
||||
<Row title="Worktree mode" help="How the local provider materializes the workspace.">
|
||||
<Badge>{worktreeMode}</Badge>
|
||||
</Row>
|
||||
) : null}
|
||||
<Row title="Devcontainer" help="Whether .devcontainer setup is honored.">
|
||||
<Toggle on={getBool(sandbox, "devcontainer") ?? false} />
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ The new design must optimize for:
|
|||
- R62. Sandbox config must remain provider-specific because provider differences are too large to hide behind one flat abstraction.
|
||||
- R63. Model config must remain intentionally provider-neutral. It should not grow provider-specific subtables. `run.model.fallbacks` is a single ordered array of model references. Each entry may be a bare provider token such as `openai`, a bare model alias or model id such as `gpt-5.4`, or a qualified reference such as `gemini/gemini-flash`. Bare references are allowed only when unambiguous. Ambiguous bare references must hard-error and require qualification. A bare provider token means “choose the best matching model from that provider.”
|
||||
- R64. SCM config must be provider-neutral at the core (`[run.scm]`) with room for provider-specific nested tables such as `[run.scm.github]` only where necessary.
|
||||
- R65. Chat platforms such as Slack, Discord, and Teams are integrations. Their server-owned setup lives under `[server.integrations.<provider>]`; run behavior lives under `[run.notifications.*]` and `[run.interviews]`.
|
||||
- R65. Slack is the chat integration. Its server-owned setup lives under `[server.integrations.slack]`; run behavior lives under `[run.notifications.*]` and `[run.interviews]`.
|
||||
- R66. Object-store-backed domains must use a shared pattern: a small provider-neutral envelope plus provider-specific nested tables.
|
||||
- R67. For local object-store providers, default to `server.storage.root`, but allow explicit local override roots when needed.
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ This refactor is centered on four seams:
|
|||
- notification route surface first pass:
|
||||
- route envelope fields are `enabled`, `provider`, and `events`
|
||||
- provider-specific destination fields live under `[run.notifications.<name>.<provider>]`
|
||||
- first-pass chat destinations for Slack, Discord, and Teams use `channel`
|
||||
- first-pass Slack destinations use `channel`
|
||||
- duration parser first pass:
|
||||
- one shared parser accepts a single unit suffix per value: `ms`, `s`, `m`, `h`, or `d`
|
||||
- composed values like `1h30m` are not supported in first pass; use the smallest needed unit instead
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]`
|
||||
|
||||
|
|
|
|||
|
|
@ -303,7 +303,6 @@ fabro create [OPTIONS] <WORKFLOW>
|
|||
| `--dry-run` | Execute with simulated LLM backend |
|
||||
| `--goal <goal>` | Override the workflow goal (available as {{ goal }} in prompts) |
|
||||
| `--goal-file <goal_file>` | Read the workflow goal from a file |
|
||||
| `--in-place` | Run directly in the source checkout without git checkpoints |
|
||||
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
|
||||
| `--model <model>` | Override default LLM model |
|
||||
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
|
||||
|
|
@ -841,7 +840,6 @@ fabro run [OPTIONS] <WORKFLOW>
|
|||
| `--dry-run` | Execute with simulated LLM backend |
|
||||
| `--goal <goal>` | Override the workflow goal (available as {{ goal }} in prompts) |
|
||||
| `--goal-file <goal_file>` | Read the workflow goal from a file |
|
||||
| `--in-place` | Run directly in the source checkout without git checkpoints |
|
||||
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
|
||||
| `--model <model>` | Override default LLM model |
|
||||
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
|
||||
|
|
|
|||
|
|
@ -1,33 +1,31 @@
|
|||
# Fixed Project Workflow Directory Plan
|
||||
|
||||
**Summary**
|
||||
Remove the `[project].directory` feature entirely so project workflows are always discovered, created, listed, and resolved from `<repo_root>/.fabro/workflows/*`. Because this is greenfield, configs that still contain `[project] directory = ...` should fail schema validation instead of being ignored or migrated. User-level workflows under `~/.fabro/workflows` remain unchanged as an additional fallback/list section.
|
||||
Remove the `[project].directory` feature as an effective setting so project workflows are always discovered, created, listed, and resolved from `<repo_root>/.fabro/workflows/*`. Configs that still contain `[project] directory = ...` should continue parsing, but the field is deprecated and ignored. User-level workflows under `~/.fabro/workflows` remain unchanged as an additional fallback/list section.
|
||||
|
||||
**Key Changes**
|
||||
- Remove `directory` from the project config schema and resolved settings type:
|
||||
- Delete `ProjectLayer::directory` and `ProjectNamespace::directory`.
|
||||
- Remove `directory` from the resolved settings/API shape while tolerating old config files:
|
||||
- Keep `ProjectLayer::directory` as a deprecated parse-only field so existing `project.toml` files with the field do not fail schema validation.
|
||||
- Delete `ProjectNamespace::directory`.
|
||||
- Remove `[project] directory = "."` from built-in defaults.
|
||||
- Update project resolver/tests so `[project]` only carries `name`, `description`, and `metadata`.
|
||||
- Update project resolver, `fabro-types` fixtures, OpenAPI schema, and generated TypeScript client so resolved `[project]` settings only carry `name`, `description`, and `metadata`.
|
||||
- Make project Fabro root fixed:
|
||||
- Change `resolve_fabro_root` to validate `.fabro/project.toml`, then return `config_path.parent()` directly.
|
||||
- Update callers to handle `Result<PathBuf>` and use `<discovered .fabro>/workflows`.
|
||||
- Delete now-unused path normalization code for joining `project.directory`.
|
||||
- Update public API shape:
|
||||
- Remove `directory` from `ProjectNamespace` in `docs/public/api-reference/fabro-api.yaml`.
|
||||
- Update `workflow_settings_round_trip` expectations.
|
||||
- Regenerate `lib/packages/fabro-api-client` so TypeScript `ProjectNamespace` no longer has `directory`.
|
||||
- Delete `resolve_fabro_root`; it only exists to apply `project.directory`.
|
||||
- Delete `load_project_config` and project-root path normalization code if they have no remaining callers after `resolve_fabro_root` is removed.
|
||||
- At workflow discovery/create/list call sites, use the discovered config path's parent directory as the Fabro root and append `workflows`.
|
||||
- Do not add a new project-config validation step for workflow directory discovery; settings validation remains the responsibility of settings-loading paths.
|
||||
- Update docs and hints:
|
||||
- Remove references to `project.directory` and the old `fabro.root -> project.directory` rename hint.
|
||||
- Remove public references to `project.directory` and replace the old `fabro.root -> project.directory` rename hint with guidance that project workflows now live under `.fabro/workflows`.
|
||||
- Add/keep explicit docs that project workflows live at `<repo_root>/.fabro/workflows/<name>/`.
|
||||
|
||||
**Test Plan**
|
||||
- Config tests:
|
||||
- Empty settings still resolve project metadata defaults.
|
||||
- `[project] directory = "..."` is rejected as an unknown field.
|
||||
- `resolve_fabro_root` always returns the `.fabro` directory containing `project.toml`.
|
||||
- `[project] directory = "..."` still parses but is ignored in resolved settings.
|
||||
- Project workflow root calculation uses the `.fabro` directory containing `project.toml`, regardless of any deprecated `project.directory` value.
|
||||
- CLI integration tests:
|
||||
- Replace custom-root workflow create/list tests with fixed-root assertions.
|
||||
- Confirm `fabro workflow create <name>` writes `.fabro/workflows/<name>/workflow.{fabro,toml}`.
|
||||
- Confirm `fabro workflow create <name>` writes both `.fabro/workflows/<name>/workflow.fabro` and `.fabro/workflows/<name>/workflow.toml`.
|
||||
- Confirm named workflow resolution reads `.fabro/workflows/<name>/workflow.toml`.
|
||||
- API/client checks:
|
||||
- `cargo build -p fabro-api`
|
||||
|
|
@ -36,6 +34,6 @@ Remove the `[project].directory` feature entirely so project workflows are alway
|
|||
- `cd apps/fabro-web && bun run typecheck`
|
||||
|
||||
**Assumptions**
|
||||
- No backwards compatibility or migration period: old configs with `[project].directory` should fail.
|
||||
- Backwards compatibility is parse-only: old configs with `[project].directory` should not fail, but the value has no effect and is not exposed in resolved settings or the API.
|
||||
- This change only fixes the project workflow directory. User workflows remain available unless a separate decision removes them later.
|
||||
- GitHub retrieval itself is out of scope here; this refactor makes the repo path deterministic for that future work.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -246,10 +246,6 @@ pub(crate) struct RunArgs {
|
|||
#[arg(long, value_enum)]
|
||||
pub(crate) sandbox: Option<CliSandboxProvider>,
|
||||
|
||||
/// Run directly in the source checkout without git checkpoints
|
||||
#[arg(long, conflicts_with = "sandbox")]
|
||||
pub(crate) in_place: bool,
|
||||
|
||||
/// Attach a label to this run (repeatable, format: KEY=VALUE)
|
||||
#[arg(long = "label", value_name = "KEY=VALUE")]
|
||||
pub(crate) label: Vec<String>,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -182,15 +182,10 @@ pub(crate) fn run_manifest_args(args: &RunArgs) -> Option<types::ManifestArgs> {
|
|||
provider: args.provider.clone(),
|
||||
sandbox: args
|
||||
.sandbox
|
||||
.map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string())
|
||||
.or_else(|| {
|
||||
args.in_place
|
||||
.then(|| fabro_sandbox::SandboxProvider::Local.to_string())
|
||||
}),
|
||||
.map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()),
|
||||
docker_image: None,
|
||||
input: args.inputs.values.clone(),
|
||||
verbose: args.verbose.then_some(true),
|
||||
worktree_mode: args.in_place.then(|| "never".to_string()),
|
||||
};
|
||||
(!manifest_args_is_empty(&payload)).then_some(payload)
|
||||
}
|
||||
|
|
@ -209,7 +204,6 @@ pub(crate) fn preflight_manifest_args(args: &PreflightArgs) -> Option<types::Man
|
|||
docker_image: None,
|
||||
input: args.inputs.values.clone(),
|
||||
verbose: args.verbose.then_some(true),
|
||||
worktree_mode: None,
|
||||
};
|
||||
(!manifest_args_is_empty(&payload)).then_some(payload)
|
||||
}
|
||||
|
|
@ -667,7 +661,6 @@ fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool {
|
|||
&& args.docker_image.is_none()
|
||||
&& args.input.is_empty()
|
||||
&& args.verbose.is_none()
|
||||
&& args.worktree_mode.is_none()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ fn help() {
|
|||
--provider <PROVIDER> Override default LLM provider
|
||||
-v, --verbose Enable verbose output
|
||||
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
|
||||
--in-place Run directly in the source checkout without git checkpoints
|
||||
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)
|
||||
--preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)
|
||||
-d, --detach Run the workflow in the background and print the run ID
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -150,7 +150,6 @@ fn help() {
|
|||
--provider <PROVIDER> Override default LLM provider
|
||||
-v, --verbose Enable verbose output
|
||||
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
|
||||
--in-place Run directly in the source checkout without git checkpoints
|
||||
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)
|
||||
--preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)
|
||||
-d, --detach Run the workflow in the background and print the run ID
|
||||
|
|
@ -670,9 +669,6 @@ goal = "Show stored artifacts"
|
|||
provider = "local"
|
||||
preserve = true
|
||||
|
||||
[run.sandbox.local]
|
||||
worktree_mode = "never"
|
||||
|
||||
[run.artifacts]
|
||||
include = ["assets/**"]
|
||||
"#,
|
||||
|
|
@ -728,7 +724,6 @@ fn dry_run_simple() {
|
|||
|
||||
Run: [ULID]
|
||||
Web UI: http://localhost:3000/runs/[ULID]
|
||||
Warning: Worktree mode `always` requested but no Git repository was found; running without a worktree. [worktree_skipped_no_git]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Run Tests [TIME]
|
||||
|
|
|
|||
|
|
@ -349,8 +349,6 @@ goal = "Exercise sandbox commands"
|
|||
provider = "local"
|
||||
preserve = true
|
||||
|
||||
[run.sandbox.local]
|
||||
worktree_mode = "never"
|
||||
"#,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
");
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use fabro_types::settings::{ProjectNamespace, RunNamespace, WorkflowNamespace};
|
||||
use fabro_types::settings::{RunNamespace, WorkflowNamespace};
|
||||
use fabro_types::{ServerSettings, UserSettings, WorkflowSettings};
|
||||
use fabro_util::error::SharedError;
|
||||
|
||||
|
|
@ -419,15 +419,6 @@ impl WorkflowSettingsBuilder {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn project_from_layer(
|
||||
layer: &SettingsLayer,
|
||||
) -> std::result::Result<ProjectNamespace, ResolveErrors> {
|
||||
let layer = layer.clone().combine(DEFAULTS_LAYER.clone());
|
||||
let mut errors = Vec::new();
|
||||
let project = resolve_project(&layer.project.clone().unwrap_or_default(), &mut errors);
|
||||
finish_dense_result(project, errors)
|
||||
}
|
||||
|
||||
pub(crate) fn workflow_from_layer(
|
||||
layer: &SettingsLayer,
|
||||
) -> std::result::Result<WorkflowNamespace, ResolveErrors> {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ pub struct ProjectLayer {
|
|||
pub name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// The Fabro-managed project directory inside the repo. Defaults to
|
||||
/// `.` after layering when unspecified.
|
||||
/// Deprecated parse-only field. Project workflows always live under the
|
||||
/// `.fabro` directory that contains `project.toml`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub directory: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "ReplaceMap::is_empty")]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use std::collections::HashMap;
|
|||
|
||||
use fabro_types::settings::run::{
|
||||
AgentPermissions, ApprovalMode, DaytonaNetworkLayer, HookEvent, MergeStrategy, RunMode,
|
||||
WorktreeMode,
|
||||
};
|
||||
use fabro_types::settings::{Duration, InterpString, ModelRef, Size};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -264,20 +263,11 @@ pub struct RunSandboxLayer {
|
|||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub env: StickyMap<InterpString>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub local: Option<LocalSandboxLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub docker: Option<DockerSandboxLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub daytona: Option<DaytonaSandboxLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LocalSandboxLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub worktree_mode: Option<WorktreeMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DockerSandboxLayer {
|
||||
|
|
@ -344,13 +334,9 @@ pub struct NotificationRouteLayer {
|
|||
/// Raw Fabro event names. Splice marker supported at layering time.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub events: Vec<StringOrSplice>,
|
||||
/// Provider-specific destination subtables. First-pass chat providers.
|
||||
/// Provider-specific destination subtables.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slack: Option<NotificationProviderLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub discord: Option<NotificationProviderLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub teams: Option<NotificationProviderLayer>,
|
||||
}
|
||||
|
||||
/// A single string array entry that may be the splice marker.
|
||||
|
|
@ -396,10 +382,6 @@ pub struct InterviewsLayer {
|
|||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slack: Option<InterviewProviderLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub discord: Option<InterviewProviderLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub teams: Option<InterviewProviderLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -189,21 +189,15 @@ pub struct ServerLoggingLayer {
|
|||
pub destination: Option<LogDestination>,
|
||||
}
|
||||
|
||||
/// `[server.integrations.<provider>]` — cohesive integration surface for chat
|
||||
/// platforms and git providers (GitHub App, webhooks, etc.). First-pass
|
||||
/// integrations enumerate known providers rather than using a flatten-HashMap
|
||||
/// shape so strict unknown-field validation still holds.
|
||||
/// `[server.integrations.<provider>]` — cohesive integration surface for Slack
|
||||
/// and git providers (GitHub App, webhooks, etc.).
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ServerIntegrationsLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub github: Option<GithubIntegrationLayer>,
|
||||
pub github: Option<GithubIntegrationLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slack: Option<SlackIntegrationLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub discord: Option<DiscordIntegrationLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub teams: Option<TeamsIntegrationLayer>,
|
||||
pub slack: Option<SlackIntegrationLayer>,
|
||||
}
|
||||
|
||||
/// `[server.integrations.github]` — GitHub App, credentials, and inbound
|
||||
|
|
@ -235,22 +229,6 @@ pub struct SlackIntegrationLayer {
|
|||
pub default_channel: Option<InterpString>,
|
||||
}
|
||||
|
||||
/// `[server.integrations.discord]` — Discord workspace configuration.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DiscordIntegrationLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// `[server.integrations.teams]` — Microsoft Teams configuration.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TeamsIntegrationLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IntegrationWebhooksLayer {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ fn rename_hint(key: &str) -> Option<String> {
|
|||
"artifact_storage" => "rename to `[server.artifacts]`",
|
||||
"storage_dir" | "data_dir" => "rename to `[server.storage] root`",
|
||||
"max_concurrent_runs" => "rename to `[server.scheduler]` field",
|
||||
"fabro" => "rename to `[project]`; `fabro.root` becomes `project.directory`",
|
||||
"fabro" => "rename to `[project]`; project workflows now live under `.fabro/workflows`",
|
||||
"git" => "split into `[run.git]` (local git behavior) and `[server.integrations.github]`",
|
||||
"github" => {
|
||||
"split into `[server.integrations.github]` (App identity/auth) and \
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@
|
|||
)]
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use fabro_types::settings::{InterpString, RunNamespace};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::load::load_settings_path;
|
||||
use crate::{Error, Result, SettingsLayer, WorkflowSettingsBuilder, run};
|
||||
use crate::{Error, Result, WorkflowSettingsBuilder, run};
|
||||
|
||||
const CONFIG_FILENAME: &str = ".fabro/project.toml";
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -27,19 +26,6 @@ pub struct WorkflowPathResolution {
|
|||
pub workflow_slug: Option<String>,
|
||||
}
|
||||
|
||||
/// Load a project config from a file path.
|
||||
///
|
||||
/// Goes through [`load_settings_path`] so that relative `run.goal.file`
|
||||
/// paths are anchored at the directory of `path` at load time.
|
||||
fn load_project_config(path: &Path) -> Result<SettingsLayer> {
|
||||
let config = load_settings_path(path)?;
|
||||
let root = WorkflowSettingsBuilder::project_from_layer(&config)
|
||||
.map_err(|errors| Error::resolve("Failed to resolve project settings", errors.into()))?
|
||||
.directory;
|
||||
tracing::debug!(path = %path.display(), root = %root, "Loaded project config");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Walk ancestor directories from `start` looking for `.fabro/project.toml`.
|
||||
/// Returns the config file path, or `None` if not found.
|
||||
pub fn discover_project_config(start: &Path) -> Result<Option<PathBuf>> {
|
||||
|
|
@ -146,7 +132,9 @@ fn resolve_workflow_arg_impl(
|
|||
let name = arg.to_string_lossy();
|
||||
match discover_project_config(start_dir) {
|
||||
Ok(Some(config_path)) => {
|
||||
let fabro_root = resolve_fabro_root(&config_path);
|
||||
let fabro_root = config_path
|
||||
.parent()
|
||||
.expect("project config should have a parent directory");
|
||||
let project_candidate = fabro_root
|
||||
.join("workflows")
|
||||
.join(&*name)
|
||||
|
|
@ -334,40 +322,6 @@ pub fn resolve_workflow(arg: &Path) -> Result<PathBuf> {
|
|||
Ok(resolution.dot_path)
|
||||
}
|
||||
|
||||
fn normalize_joined_path(base_dir: &Path, reference: &Path) -> PathBuf {
|
||||
if reference.is_absolute() {
|
||||
return reference.to_path_buf();
|
||||
}
|
||||
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in base_dir.join(reference).components() {
|
||||
match component {
|
||||
Component::CurDir => {}
|
||||
Component::Normal(part) => normalized.push(part),
|
||||
Component::ParentDir => {
|
||||
normalized.pop();
|
||||
}
|
||||
Component::RootDir => normalized.push(Path::new("/")),
|
||||
Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
/// Resolve the fabro root directory from a config file path and its config.
|
||||
/// The returned path is the config file's parent directory joined with the
|
||||
/// `project.directory` value (default: `.`).
|
||||
pub fn resolve_fabro_root(config_path: &Path) -> PathBuf {
|
||||
let project_dir = config_path
|
||||
.parent()
|
||||
.expect("config_path should have a parent directory");
|
||||
let config = load_project_config(config_path).expect("project config should load");
|
||||
let root = WorkflowSettingsBuilder::project_from_layer(&config)
|
||||
.expect("project settings should resolve")
|
||||
.directory;
|
||||
normalize_joined_path(project_dir, Path::new(&root))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
|
@ -386,19 +340,31 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_with_project_directory() {
|
||||
assert_eq!(
|
||||
WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[project]
|
||||
directory = "custom/"
|
||||
"#
|
||||
.parse::<crate::SettingsLayer>()
|
||||
.unwrap()
|
||||
.project
|
||||
.and_then(|project| project.directory),
|
||||
Some("custom/".to_string())
|
||||
);
|
||||
|
||||
let project = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[project]
|
||||
directory = "custom/"
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.project
|
||||
.directory,
|
||||
"custom/"
|
||||
);
|
||||
)
|
||||
.unwrap()
|
||||
.project;
|
||||
let json = serde_json::to_value(&project).expect("project settings should serialize");
|
||||
assert!(json.get("directory").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -425,17 +391,6 @@ directory = "custom/"
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_disk() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config_dir = tmp.path().join(".fabro");
|
||||
fs::create_dir_all(&config_dir).unwrap();
|
||||
let path = config_dir.join("project.toml");
|
||||
fs::write(&path, "_version = 1\n").unwrap();
|
||||
let config = load_project_config(&path).unwrap();
|
||||
assert_eq!(config.version, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_walks_ancestors() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
|
@ -450,50 +405,14 @@ directory = "custom/"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn load_project_config_rewrites_relative_goal_file_path() {
|
||||
use crate::RunGoalLayer;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config_dir = tmp.path().join(".fabro");
|
||||
fs::create_dir_all(&config_dir).unwrap();
|
||||
let path = config_dir.join("project.toml");
|
||||
fs::write(
|
||||
&path,
|
||||
r#"_version = 1
|
||||
|
||||
[run.goal]
|
||||
file = "prompts/goal.md"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_project_config(&path).unwrap();
|
||||
let Some(RunGoalLayer::File { file }) =
|
||||
config.run.as_ref().and_then(|run| run.goal.as_ref())
|
||||
else {
|
||||
panic!("expected file variant");
|
||||
};
|
||||
let expected = config_dir.join("prompts").join("goal.md");
|
||||
assert_eq!(file.as_source(), expected.to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_directory_resolves_to_config_parent() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config_dir = tmp.path().join(".fabro");
|
||||
fs::create_dir_all(&config_dir).unwrap();
|
||||
let config_path = config_dir.join("project.toml");
|
||||
fs::write(&config_path, "_version = 1\n").unwrap();
|
||||
|
||||
assert_eq!(resolve_fabro_root(&config_path), config_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_relative_directory_resolves_from_config_parent() {
|
||||
fn deprecated_project_directory_does_not_change_fabro_root() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config_dir = tmp.path().join(".fabro");
|
||||
fs::create_dir_all(&config_dir).unwrap();
|
||||
let config_path = config_dir.join("project.toml");
|
||||
let workflow_dir = config_dir.join("workflows/demo");
|
||||
fs::create_dir_all(&workflow_dir).unwrap();
|
||||
fs::write(workflow_dir.join("workflow.toml"), "_version = 1\n").unwrap();
|
||||
fs::write(
|
||||
&config_path,
|
||||
r#"_version = 1
|
||||
|
|
@ -504,37 +423,9 @@ directory = "../custom"
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resolve_fabro_root(&config_path), tmp.path().join("custom"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_goal_file_resolves_from_config_dir() {
|
||||
use crate::RunGoalLayer;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config_dir = tmp.path().join(".fabro");
|
||||
fs::create_dir_all(&config_dir).unwrap();
|
||||
let config_path = config_dir.join("project.toml");
|
||||
fs::write(
|
||||
&config_path,
|
||||
r#"_version = 1
|
||||
|
||||
[run.goal]
|
||||
file = "prompts/goal.md"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_project_config(&config_path).unwrap();
|
||||
let Some(RunGoalLayer::File { file }) =
|
||||
config.run.as_ref().and_then(|run| run.goal.as_ref())
|
||||
else {
|
||||
panic!("expected file variant");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
file.as_source(),
|
||||
config_dir.join("prompts").join("goal.md").to_string_lossy()
|
||||
resolve_workflow_arg_impl(Path::new("demo"), tmp.path(), None).unwrap(),
|
||||
config_dir.join("workflows/demo/workflow.toml")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,6 @@ pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) ->
|
|||
ProjectNamespace {
|
||||
name: layer.name.clone(),
|
||||
description: layer.description.clone(),
|
||||
directory: layer
|
||||
.directory
|
||||
.clone()
|
||||
.expect("defaults.toml should provide project.directory"),
|
||||
metadata: layer.metadata.clone().into_inner(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::server::{
|
||||
DiscordIntegrationSettings, GithubIntegrationSettings, GithubIntegrationStrategy,
|
||||
IntegrationWebhooksSettings, IpAllowEntry, ObjectStoreProvider, ObjectStoreSettings,
|
||||
ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod,
|
||||
ServerAuthSettings, ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings,
|
||||
ServerIpAllowlistSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace,
|
||||
ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings,
|
||||
SlackIntegrationSettings, TeamsIntegrationSettings, WebhookStrategy,
|
||||
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
|
||||
IpAllowEntry, ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings,
|
||||
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
|
||||
ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,
|
||||
ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings,
|
||||
ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
|
||||
WebhookStrategy,
|
||||
};
|
||||
use fabro_util::Home;
|
||||
|
||||
|
|
@ -454,7 +454,7 @@ fn resolve_integrations(
|
|||
errors: &mut Vec<ResolveError>,
|
||||
) -> ServerIntegrationsSettings {
|
||||
ServerIntegrationsSettings {
|
||||
github: layer
|
||||
github: layer
|
||||
.and_then(|integrations| integrations.github.as_ref())
|
||||
.map(|github| GithubIntegrationSettings {
|
||||
enabled: github.enabled.unwrap_or(true),
|
||||
|
|
@ -467,25 +467,13 @@ fn resolve_integrations(
|
|||
}),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
slack: layer
|
||||
slack: layer
|
||||
.and_then(|integrations| integrations.slack.as_ref())
|
||||
.map(|slack| SlackIntegrationSettings {
|
||||
enabled: slack.enabled.unwrap_or(true),
|
||||
default_channel: slack.default_channel.clone(),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
discord: layer
|
||||
.and_then(|integrations| integrations.discord.as_ref())
|
||||
.map(|discord| DiscordIntegrationSettings {
|
||||
enabled: discord.enabled.unwrap_or(true),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
teams: layer
|
||||
.and_then(|integrations| integrations.teams.as_ref())
|
||||
.map(|teams| TeamsIntegrationSettings {
|
||||
enabled: teams.enabled.unwrap_or(true),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::{ApprovalMode, RunGoal, RunMode, WorktreeMode};
|
||||
use fabro_types::settings::run::{ApprovalMode, RunGoal, RunMode};
|
||||
|
||||
use crate::{SettingsLayer, WorkflowSettingsBuilder};
|
||||
|
||||
|
|
@ -14,7 +14,6 @@ fn resolves_run_defaults_from_empty_settings() {
|
|||
assert_eq!(settings.prepare.timeout_ms, 300_000);
|
||||
assert_eq!(settings.sandbox.provider, "docker");
|
||||
assert!(settings.sandbox.stop_on_terminal);
|
||||
assert_eq!(settings.sandbox.local.worktree_mode, WorktreeMode::Always);
|
||||
let docker = settings
|
||||
.sandbox
|
||||
.docker
|
||||
|
|
@ -27,6 +26,92 @@ fn resolves_run_defaults_from_empty_settings() {
|
|||
assert!(settings.pull_request.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_run_chat_surfaces_are_slack_only() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
r##"
|
||||
_version = 1
|
||||
|
||||
[run.notifications.ops]
|
||||
enabled = true
|
||||
provider = "slack"
|
||||
events = ["run.completed"]
|
||||
|
||||
[run.notifications.ops.slack]
|
||||
channel = "#ops"
|
||||
|
||||
[run.interviews]
|
||||
provider = "slack"
|
||||
|
||||
[run.interviews.slack]
|
||||
channel = "#ops"
|
||||
"##,
|
||||
)
|
||||
.expect("slack-only chat settings should resolve")
|
||||
.run;
|
||||
|
||||
let route = settings
|
||||
.notifications
|
||||
.get("ops")
|
||||
.expect("notification route should resolve");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(route).expect("route should serialize"),
|
||||
serde_json::json!({
|
||||
"enabled": true,
|
||||
"provider": "slack",
|
||||
"events": ["run.completed"],
|
||||
"slack": {
|
||||
"channel": "#ops",
|
||||
},
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&settings.interviews).expect("interviews should serialize"),
|
||||
serde_json::json!({
|
||||
"provider": "slack",
|
||||
"slack": {
|
||||
"channel": "#ops",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_rejects_unknown_run_chat_destinations() {
|
||||
let notifications = r##"
|
||||
_version = 1
|
||||
|
||||
[run.notifications.ops.chatapp]
|
||||
channel = "#ops"
|
||||
"##;
|
||||
|
||||
let err = notifications
|
||||
.parse::<SettingsLayer>()
|
||||
.expect_err("unknown notification destination should be rejected");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("chatapp") || message.contains("unknown field"),
|
||||
"expected notification parse error for unknown chat provider, got: {message}"
|
||||
);
|
||||
|
||||
let interviews = r##"
|
||||
_version = 1
|
||||
|
||||
[run.interviews.chatapp]
|
||||
channel = "#ops"
|
||||
"##;
|
||||
|
||||
let err = interviews
|
||||
.parse::<SettingsLayer>()
|
||||
.expect_err("unknown interview destination should be rejected");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("chatapp") || message.contains("unknown field"),
|
||||
"expected interview parse error for unknown chat provider, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_explicit_stop_on_terminal_false() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
|
|
|
|||
|
|
@ -109,6 +109,51 @@ fn resolves_server_defaults_from_empty_settings() {
|
|||
assert!(!settings.slatedb.disk_cache);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_server_integrations_are_slack_only_for_chat() {
|
||||
let settings = resolve_server(&empty_settings_with_auth_methods());
|
||||
|
||||
let integrations =
|
||||
serde_json::to_value(&settings.integrations).expect("integrations should serialize");
|
||||
|
||||
assert_eq!(
|
||||
integrations,
|
||||
serde_json::json!({
|
||||
"github": {
|
||||
"enabled": false,
|
||||
"strategy": "token",
|
||||
"app_id": null,
|
||||
"client_id": null,
|
||||
"slug": null,
|
||||
"webhooks": null,
|
||||
},
|
||||
"slack": {
|
||||
"enabled": false,
|
||||
"default_channel": null,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_rejects_unknown_server_integrations() {
|
||||
let source = r#"
|
||||
_version = 1
|
||||
|
||||
[server.integrations.chatapp]
|
||||
enabled = true
|
||||
"#;
|
||||
|
||||
let err = source
|
||||
.parse::<SettingsLayer>()
|
||||
.expect_err("unknown chat integration should be rejected");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("chatapp") || message.contains("unknown field"),
|
||||
"expected parse error for unknown chat provider, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_server_logging_destination_from_settings() {
|
||||
let file = parse(
|
||||
|
|
|
|||
|
|
@ -498,7 +498,6 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use fabro_types::settings::run::WorktreeMode as V2WorktreeMode;
|
||||
use serde::de::{self, MapAccess, Visitor};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -123,24 +122,3 @@ pub struct DaytonaSnapshotSettings {
|
|||
pub disk: Option<i32>,
|
||||
pub dockerfile: Option<DockerfileSource>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorktreeMode {
|
||||
Always,
|
||||
#[default]
|
||||
Clean,
|
||||
Dirty,
|
||||
Never,
|
||||
}
|
||||
|
||||
/// Convert a v2 [`V2WorktreeMode`] into the runtime [`WorktreeMode`].
|
||||
#[must_use]
|
||||
pub fn bridge_worktree_mode(m: V2WorktreeMode) -> WorktreeMode {
|
||||
match m {
|
||||
V2WorktreeMode::Always => WorktreeMode::Always,
|
||||
V2WorktreeMode::Clean => WorktreeMode::Clean,
|
||||
V2WorktreeMode::Dirty => WorktreeMode::Dirty,
|
||||
V2WorktreeMode::Never => WorktreeMode::Never,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ use fabro_api::types;
|
|||
use fabro_auth::auth_issue_message;
|
||||
use fabro_config::run::parse_run_layer_from_settings_toml;
|
||||
use fabro_config::{
|
||||
CliLayer, CliOutputLayer, DaytonaDockerfileLayer, DockerSandboxLayer, LocalSandboxLayer,
|
||||
ReplaceMap, RunExecutionLayer, RunLayer, RunModelLayer, RunSandboxLayer,
|
||||
WorkflowSettingsBuilder, parse_input_overrides,
|
||||
CliLayer, CliOutputLayer, DaytonaDockerfileLayer, DockerSandboxLayer, ReplaceMap,
|
||||
RunExecutionLayer, RunLayer, RunModelLayer, RunSandboxLayer, WorkflowSettingsBuilder,
|
||||
parse_input_overrides,
|
||||
};
|
||||
use fabro_graphviz::graph::{Graph, is_llm_handler_type};
|
||||
use fabro_graphviz::render::apply_direction;
|
||||
|
|
@ -29,7 +29,7 @@ use fabro_types::settings::cli::OutputVerbosity;
|
|||
use fabro_types::settings::interp::InterpString;
|
||||
use fabro_types::settings::run::{
|
||||
ApprovalMode, DaytonaNetworkLayer, DaytonaSettings, DockerSettings, DockerfileSource, RunGoal,
|
||||
RunMode, RunNamespace, WorktreeMode,
|
||||
RunMode, RunNamespace,
|
||||
};
|
||||
use fabro_types::{RunId, WorkflowSettings};
|
||||
use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};
|
||||
|
|
@ -57,7 +57,6 @@ pub(crate) struct PreparedManifest {
|
|||
pub workflow_bundle: WorkflowBundle,
|
||||
pub workflow_input: BundledWorkflow,
|
||||
pub source_directory: PathBuf,
|
||||
pub in_place: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
|
@ -136,9 +135,6 @@ pub(crate) fn prepare_manifest(
|
|||
.map(|title| fabro_types::normalize_explicit_run_title(title.as_str()))
|
||||
.transpose()?;
|
||||
|
||||
let in_place = settings.run.sandbox.provider == "local"
|
||||
&& settings.run.sandbox.local.worktree_mode == WorktreeMode::Never;
|
||||
|
||||
Ok(PreparedManifest {
|
||||
cwd: cwd.clone(),
|
||||
git: manifest.git.clone(),
|
||||
|
|
@ -155,7 +151,6 @@ pub(crate) fn prepare_manifest(
|
|||
workflow_bundle,
|
||||
workflow_input,
|
||||
source_directory: resolve_working_directory(&settings, &cwd),
|
||||
in_place,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +182,6 @@ pub(crate) fn create_run_input(
|
|||
title: prepared.title,
|
||||
git: prepared.git,
|
||||
fork_source_ref: None,
|
||||
in_place: prepared.in_place,
|
||||
provenance: None,
|
||||
configured_providers,
|
||||
web_url,
|
||||
|
|
@ -327,27 +321,17 @@ fn manifest_args_overrides(
|
|||
name: args.model.as_deref().map(InterpString::parse),
|
||||
fallbacks: Vec::new(),
|
||||
});
|
||||
let local_worktree = args
|
||||
.worktree_mode
|
||||
.as_deref()
|
||||
.and_then(parse_worktree_mode_arg)
|
||||
.map(|mode| LocalSandboxLayer {
|
||||
worktree_mode: Some(mode),
|
||||
});
|
||||
let sandbox = (args.sandbox.is_some()
|
||||
|| args.preserve_sandbox.is_some()
|
||||
|| args.docker_image.is_some()
|
||||
|| local_worktree.is_some())
|
||||
.then(|| RunSandboxLayer {
|
||||
provider: args.sandbox.clone(),
|
||||
preserve: args.preserve_sandbox,
|
||||
local: local_worktree,
|
||||
docker: args.docker_image.as_ref().map(|image| DockerSandboxLayer {
|
||||
image: Some(image.clone()),
|
||||
..DockerSandboxLayer::default()
|
||||
}),
|
||||
..RunSandboxLayer::default()
|
||||
});
|
||||
let sandbox =
|
||||
(args.sandbox.is_some() || args.preserve_sandbox.is_some() || args.docker_image.is_some())
|
||||
.then(|| RunSandboxLayer {
|
||||
provider: args.sandbox.clone(),
|
||||
preserve: args.preserve_sandbox,
|
||||
docker: args.docker_image.as_ref().map(|image| DockerSandboxLayer {
|
||||
image: Some(image.clone()),
|
||||
..DockerSandboxLayer::default()
|
||||
}),
|
||||
..RunSandboxLayer::default()
|
||||
});
|
||||
|
||||
let execution_has_any = args.dry_run.is_some() || args.auto_approve.is_some();
|
||||
let execution = execution_has_any.then(|| RunExecutionLayer {
|
||||
|
|
@ -392,16 +376,6 @@ fn manifest_args_overrides(
|
|||
})
|
||||
}
|
||||
|
||||
fn parse_worktree_mode_arg(value: &str) -> Option<WorktreeMode> {
|
||||
match value {
|
||||
"always" => Some(WorktreeMode::Always),
|
||||
"clean" => Some(WorktreeMode::Clean),
|
||||
"dirty" => Some(WorktreeMode::Dirty),
|
||||
"never" => Some(WorktreeMode::Never),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_labels(labels: &[String]) -> HashMap<String, String> {
|
||||
labels
|
||||
.iter()
|
||||
|
|
@ -1724,7 +1698,6 @@ root = "/srv/fabro"
|
|||
docker_image: None,
|
||||
input: Vec::new(),
|
||||
verbose: None,
|
||||
worktree_mode: None,
|
||||
});
|
||||
|
||||
let prepared = prepare_manifest(&server_settings, &manifest).unwrap();
|
||||
|
|
@ -1758,7 +1731,6 @@ override = "server"
|
|||
docker_image: None,
|
||||
input: vec!["override=cli".to_string()],
|
||||
verbose: None,
|
||||
worktree_mode: None,
|
||||
});
|
||||
|
||||
let prepared = prepare_manifest(&server_settings, &manifest).unwrap();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -478,7 +478,6 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ fn sample_run_spec() -> RunSpec {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,8 +98,6 @@ pub struct RunSpec {
|
|||
pub git: Option<GitContext>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default)]
|
||||
pub in_place: bool,
|
||||
}
|
||||
|
||||
impl RunSpec {
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ pub struct RunCreatedProps {
|
|||
pub git: Option<GitContext>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default)]
|
||||
pub in_place: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub web_url: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ pub struct RunSummary {
|
|||
#[serde(default)]
|
||||
pub source_directory: Option<String>,
|
||||
#[serde(default)]
|
||||
pub in_place: bool,
|
||||
#[serde(default)]
|
||||
pub repo_origin_url: Option<String>,
|
||||
pub repository: RepositoryReference,
|
||||
#[serde(default)]
|
||||
|
|
@ -59,7 +57,6 @@ impl RunSummary {
|
|||
title: String,
|
||||
labels: HashMap<String, String>,
|
||||
source_directory: Option<String>,
|
||||
in_place: bool,
|
||||
repo_origin_url: Option<String>,
|
||||
start_time: Option<DateTime<Utc>>,
|
||||
last_event_at: Option<DateTime<Utc>>,
|
||||
|
|
@ -85,7 +82,6 @@ impl RunSummary {
|
|||
title,
|
||||
labels,
|
||||
source_directory,
|
||||
in_place,
|
||||
repo_origin_url,
|
||||
repository,
|
||||
start_time,
|
||||
|
|
@ -172,7 +168,6 @@ mod tests {
|
|||
"Production title".to_string(),
|
||||
HashMap::from([("team".to_string(), "core".to_string())]),
|
||||
Some("/Users/client/local-checkout".to_string()),
|
||||
false,
|
||||
Some("https://github.com/fabro-sh/fabro.git".to_string()),
|
||||
Some(Utc.with_ymd_and_hms(2026, 4, 20, 12, 0, 0).unwrap()),
|
||||
Some(Utc.with_ymd_and_hms(2026, 4, 20, 12, 5, 0).unwrap()),
|
||||
|
|
@ -290,7 +285,6 @@ mod tests {
|
|||
"ship it".to_string(),
|
||||
HashMap::new(),
|
||||
Some("/Users/client/local-checkout".to_string()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -313,7 +307,6 @@ mod tests {
|
|||
"ship it".to_string(),
|
||||
HashMap::new(),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
//! Project domain: first-class project object.
|
||||
//!
|
||||
//! `[project]` replaces the old flat `[fabro]` shape. `directory` means the
|
||||
//! Fabro-managed project directory inside the repo, defaulting to `.`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -12,6 +9,5 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct ProjectNamespace {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub directory: String,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,6 @@ pub struct RunSandboxSettings {
|
|||
pub stop_on_terminal: bool,
|
||||
pub devcontainer: bool,
|
||||
pub env: HashMap<String, InterpString>,
|
||||
pub local: LocalSandboxSettings,
|
||||
pub docker: Option<DockerSettings>,
|
||||
pub daytona: Option<DaytonaSettings>,
|
||||
}
|
||||
|
|
@ -218,18 +217,12 @@ impl Default for RunSandboxSettings {
|
|||
stop_on_terminal: true,
|
||||
devcontainer: false,
|
||||
env: HashMap::new(),
|
||||
local: LocalSandboxSettings::default(),
|
||||
docker: None,
|
||||
daytona: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LocalSandboxSettings {
|
||||
pub worktree_mode: WorktreeMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DockerSettings {
|
||||
pub image: String,
|
||||
|
|
@ -309,8 +302,6 @@ pub struct NotificationRouteSettings {
|
|||
pub provider: Option<String>,
|
||||
pub events: Vec<String>,
|
||||
pub slack: Option<NotificationProviderSettings>,
|
||||
pub discord: Option<NotificationProviderSettings>,
|
||||
pub teams: Option<NotificationProviderSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -322,8 +313,6 @@ pub struct NotificationProviderSettings {
|
|||
pub struct RunInterviewsSettings {
|
||||
pub provider: Option<String>,
|
||||
pub slack: Option<InterviewProviderSettings>,
|
||||
pub discord: Option<InterviewProviderSettings>,
|
||||
pub teams: Option<InterviewProviderSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -585,16 +574,6 @@ pub enum ApprovalMode {
|
|||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorktreeMode {
|
||||
Always,
|
||||
#[default]
|
||||
Clean,
|
||||
Dirty,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum DaytonaNetworkLayer {
|
||||
|
|
|
|||
|
|
@ -258,10 +258,8 @@ pub struct ServerLoggingSettings {
|
|||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ServerIntegrationsSettings {
|
||||
pub github: GithubIntegrationSettings,
|
||||
pub slack: SlackIntegrationSettings,
|
||||
pub discord: DiscordIntegrationSettings,
|
||||
pub teams: TeamsIntegrationSettings,
|
||||
pub github: GithubIntegrationSettings,
|
||||
pub slack: SlackIntegrationSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -280,16 +278,6 @@ pub struct SlackIntegrationSettings {
|
|||
pub default_channel: Option<InterpString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DiscordIntegrationSettings {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TeamsIntegrationSettings {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct IntegrationWebhooksSettings {
|
||||
pub strategy: Option<WebhookStrategy>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ fn sample_run_spec() -> RunSpec {
|
|||
},
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -284,7 +284,6 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ pub enum Event {
|
|||
git: Option<GitContext>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default)]
|
||||
in_place: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
web_url: Option<String>,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -722,7 +722,6 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ pub struct CreateRunInput {
|
|||
pub title: Option<String>,
|
||||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub in_place: bool,
|
||||
pub provenance: Option<RunProvenance>,
|
||||
pub configured_providers: Vec<Provider>,
|
||||
/// Public URL where this run can be viewed in the web UI, when the server
|
||||
|
|
@ -71,7 +70,6 @@ struct PersistCreateOptions {
|
|||
source_directory: Option<String>,
|
||||
git: Option<GitContext>,
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
in_place: bool,
|
||||
provenance: Option<RunProvenance>,
|
||||
configured_providers: Vec<Provider>,
|
||||
}
|
||||
|
|
@ -106,7 +104,6 @@ pub async fn create(
|
|||
title,
|
||||
git,
|
||||
fork_source_ref,
|
||||
in_place,
|
||||
provenance,
|
||||
configured_providers,
|
||||
web_url,
|
||||
|
|
@ -143,7 +140,6 @@ pub async fn create(
|
|||
source_directory,
|
||||
git,
|
||||
fork_source_ref,
|
||||
in_place,
|
||||
provenance,
|
||||
configured_providers,
|
||||
},
|
||||
|
|
@ -240,7 +236,6 @@ async fn persist_created_run(
|
|||
manifest_blob,
|
||||
git: record.git.clone(),
|
||||
fork_source_ref: record.fork_source_ref.clone(),
|
||||
in_place: record.in_place,
|
||||
web_url,
|
||||
},
|
||||
record.run_id.created_at(),
|
||||
|
|
@ -353,7 +348,6 @@ fn persist_validated(
|
|||
source_directory,
|
||||
git,
|
||||
fork_source_ref,
|
||||
in_place,
|
||||
provenance,
|
||||
configured_providers,
|
||||
} = options;
|
||||
|
|
@ -380,7 +374,6 @@ fn persist_validated(
|
|||
definition_blob: None,
|
||||
git,
|
||||
fork_source_ref,
|
||||
in_place,
|
||||
};
|
||||
|
||||
pipeline::persist(validated, PersistOptions { run_dir, run_spec })
|
||||
|
|
@ -722,7 +715,6 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -767,7 +759,6 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -834,7 +825,6 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -944,7 +934,6 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -988,7 +977,6 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1054,7 +1042,6 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1099,7 +1086,6 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: Some(fabro_types::RunProvenance {
|
||||
server: Some(fabro_types::RunServerProvenance {
|
||||
version: "0.9.0".to_string(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ use fabro_interview::{AutoApproveInterviewer, Interviewer};
|
|||
use fabro_mcp::config::{McpServerSettings, McpTransport};
|
||||
use fabro_model::{Catalog, FallbackTarget, Provider};
|
||||
use fabro_sandbox::config::{
|
||||
self as sandbox_config, DaytonaNetwork, DaytonaSnapshotSettings,
|
||||
DockerfileSource as SandboxDockerfileSource, WorktreeMode, bridge_worktree_mode,
|
||||
DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource,
|
||||
};
|
||||
use fabro_sandbox::daytona::DaytonaConfig;
|
||||
use fabro_sandbox::{DockerSandboxOptions, SandboxProvider, SandboxSpec};
|
||||
|
|
@ -69,7 +68,6 @@ struct RunSession {
|
|||
artifact_sink: Option<ArtifactSink>,
|
||||
git: Option<GitCheckpointOptions>,
|
||||
github_app: Option<fabro_github::GitHubCredentials>,
|
||||
worktree_mode: Option<WorktreeMode>,
|
||||
registry_override: Option<Arc<HandlerRegistry>>,
|
||||
preserve_sandbox: bool,
|
||||
stop_on_terminal: bool,
|
||||
|
|
@ -442,7 +440,6 @@ impl RunSession {
|
|||
artifact_sink: services.artifact_sink,
|
||||
git,
|
||||
github_app: services.github_app.clone(),
|
||||
worktree_mode: Some(resolve_worktree_mode(resolved)),
|
||||
registry_override: services.registry_override,
|
||||
preserve_sandbox: resolved.sandbox.preserve,
|
||||
stop_on_terminal: resolved.sandbox.stop_on_terminal,
|
||||
|
|
@ -496,10 +493,6 @@ fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> Result<SandboxPro
|
|||
.map_or_else(|| Ok(SandboxProvider::default()), Ok)
|
||||
}
|
||||
|
||||
fn resolve_worktree_mode(settings: &ResolvedRunSettings) -> sandbox_config::WorktreeMode {
|
||||
bridge_worktree_mode(settings.sandbox.local.worktree_mode)
|
||||
}
|
||||
|
||||
fn resolve_daytona_config(settings: &ResolvedRunSettings) -> Option<DaytonaConfig> {
|
||||
settings
|
||||
.sandbox
|
||||
|
|
@ -753,7 +746,6 @@ impl RunSession {
|
|||
vault: self.vault,
|
||||
devcontainer: self.devcontainer,
|
||||
git: self.git,
|
||||
worktree_mode: self.worktree_mode,
|
||||
registry_override: self.registry_override,
|
||||
artifact_sink: self.artifact_sink,
|
||||
run_control: self.run_control,
|
||||
|
|
@ -1078,7 +1070,6 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1268,7 +1259,6 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -739,7 +739,6 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
|
|
@ -8,13 +8,11 @@ use fabro_auth::{
|
|||
CredentialResolver, CredentialSource, EnvCredentialSource, VaultCredentialSource,
|
||||
auth_issue_message,
|
||||
};
|
||||
use fabro_config::RunScratch;
|
||||
use fabro_graphviz::graph;
|
||||
use fabro_hooks::{HookContext, HookDecision, HookEvent, HookRunner};
|
||||
use fabro_sandbox::config::WorktreeMode;
|
||||
use fabro_sandbox::{
|
||||
GitSetupIntent, ReadBeforeWriteSandbox, SandboxEventCallback, SandboxSpec, WorktreeOptions,
|
||||
WorktreeSandbox, reconnect_for_run_with_callback,
|
||||
GitSetupIntent, ReadBeforeWriteSandbox, SandboxEventCallback, SandboxSpec,
|
||||
reconnect_for_run_with_callback,
|
||||
};
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_vault::Vault;
|
||||
|
|
@ -29,72 +27,17 @@ use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec}
|
|||
use crate::devcontainer_bridge::{devcontainer_to_snapshot_config, run_devcontainer_lifecycle};
|
||||
use crate::error::Error;
|
||||
use crate::event::{Event, RunNoticeCode, RunNoticeLevel};
|
||||
use crate::git::RUN_BRANCH_PREFIX;
|
||||
use crate::github_token_source::{AppIatMinter, GitHubTokenSource};
|
||||
use crate::handler::llm::{AgentApiBackend, AgentCliBackend, BackendRouter};
|
||||
use crate::handler::{HandlerRegistry, default_registry};
|
||||
use crate::run_metadata::{RunMetadataRuntime, build_metadata_writer, metadata_branch_name};
|
||||
use crate::run_options::{GitCheckpointOptions, RunOptions};
|
||||
use crate::sandbox_git::GIT_REMOTE;
|
||||
use crate::sandbox_git_runtime::SandboxGitRuntime;
|
||||
use crate::services::{EngineServices, RunServices, WorkflowToolEnvProvider};
|
||||
use crate::steering_hub::SteeringHub;
|
||||
|
||||
struct WorktreePlan {
|
||||
branch_name: String,
|
||||
base_sha: Option<String>,
|
||||
worktree_path: PathBuf,
|
||||
skip_branch_creation: bool,
|
||||
}
|
||||
|
||||
type BuiltSandboxEnv = (HashMap<String, String>, Option<Arc<GitHubTokenSource>>);
|
||||
|
||||
async fn resolve_worktree_base_sha(
|
||||
sandbox: &dyn Sandbox,
|
||||
plan: &WorktreePlan,
|
||||
) -> Result<Option<String>, Error> {
|
||||
if let Some(base_sha) = plan.base_sha.as_ref() {
|
||||
return Ok(Some(base_sha.clone()));
|
||||
}
|
||||
|
||||
let result = sandbox
|
||||
.exec_command(
|
||||
&format!("{GIT_REMOTE} rev-parse HEAD"),
|
||||
10_000,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| Error::engine_with_source("git rev-parse HEAD failed", &err))?;
|
||||
if !result.is_success() {
|
||||
let output = result.stderr.trim();
|
||||
let output = if output.is_empty() {
|
||||
result.stdout.trim()
|
||||
} else {
|
||||
output
|
||||
};
|
||||
if is_not_git_repository(output) {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(Error::engine(format!(
|
||||
"git rev-parse HEAD failed (exit {}): {}",
|
||||
result.display_exit_code(),
|
||||
output
|
||||
)));
|
||||
}
|
||||
|
||||
let base_sha = result.stdout.trim();
|
||||
if base_sha.is_empty() {
|
||||
return Err(Error::engine("git rev-parse HEAD returned no commit sha"));
|
||||
}
|
||||
Ok(Some(base_sha.to_string()))
|
||||
}
|
||||
|
||||
fn is_not_git_repository(output: &str) -> bool {
|
||||
output.contains("not a git repository") || output.contains("ambiguous argument 'HEAD'")
|
||||
}
|
||||
|
||||
async fn run_hooks(
|
||||
hook_runner: Option<&HookRunner>,
|
||||
hook_context: &HookContext,
|
||||
|
|
@ -107,110 +50,6 @@ async fn run_hooks(
|
|||
runner.run(hook_context, sandbox, work_dir).await
|
||||
}
|
||||
|
||||
fn resolve_worktree_plan(options: &mut InitOptions) -> Option<WorktreePlan> {
|
||||
let Some(worktree_mode) = options.worktree_mode else {
|
||||
options.run_options.display_base_sha = None;
|
||||
return None;
|
||||
};
|
||||
|
||||
let is_local = matches!(options.sandbox, SandboxSpec::Local { .. });
|
||||
|
||||
if options.checkpoint.is_some() && is_local {
|
||||
if let Some(fork_source) = options.run_options.fork_source_ref.as_ref() {
|
||||
let base_sha = fork_source.checkpoint_sha.clone();
|
||||
options.run_options.display_base_sha = Some(base_sha.clone());
|
||||
return Some(WorktreePlan {
|
||||
branch_name: format!("{RUN_BRANCH_PREFIX}{}", options.run_id),
|
||||
base_sha: Some(base_sha),
|
||||
worktree_path: RunScratch::new(&options.run_options.run_dir).worktree_dir(),
|
||||
skip_branch_creation: false,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(git) = options.run_options.git.as_ref() {
|
||||
if let (Some(run_branch), Some(base_sha)) = (&git.run_branch, &git.base_sha) {
|
||||
options.run_options.display_base_sha = Some(base_sha.clone());
|
||||
return Some(WorktreePlan {
|
||||
branch_name: run_branch.clone(),
|
||||
base_sha: Some(base_sha.clone()),
|
||||
worktree_path: RunScratch::new(&options.run_options.run_dir)
|
||||
.worktree_dir(),
|
||||
skip_branch_creation: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let local_dirty = options
|
||||
.run_options
|
||||
.pre_run_git
|
||||
.as_ref()
|
||||
.map(|git| git.dirty);
|
||||
|
||||
if matches!(local_dirty, Some(fabro_types::DirtyStatus::Dirty)) {
|
||||
let env_name = if !is_local {
|
||||
Some("remote sandbox")
|
||||
} else if worktree_mode == WorktreeMode::Never {
|
||||
None
|
||||
} else {
|
||||
Some("worktree")
|
||||
};
|
||||
if let Some(env_name) = env_name {
|
||||
options.emitter.notice(
|
||||
RunNoticeLevel::Warn,
|
||||
RunNoticeCode::DirtyWorktree,
|
||||
format!("Uncommitted changes will not be included in the {env_name}."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_local {
|
||||
options.run_options.display_base_sha = options
|
||||
.run_options
|
||||
.pre_run_git
|
||||
.as_ref()
|
||||
.and_then(|git| git.sha.clone());
|
||||
return None;
|
||||
}
|
||||
|
||||
if worktree_mode == WorktreeMode::Never {
|
||||
options.run_options.display_base_sha = None;
|
||||
return None;
|
||||
}
|
||||
|
||||
let (branch_name, base_sha) =
|
||||
if let Some(fork_source) = options.run_options.fork_source_ref.as_ref() {
|
||||
(
|
||||
format!("{RUN_BRANCH_PREFIX}{}", options.run_id),
|
||||
Some(fork_source.checkpoint_sha.clone()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("{RUN_BRANCH_PREFIX}{}", options.run_id),
|
||||
options
|
||||
.run_options
|
||||
.pre_run_git
|
||||
.as_ref()
|
||||
.and_then(|git| git.sha.clone()),
|
||||
)
|
||||
};
|
||||
options.run_options.display_base_sha.clone_from(&base_sha);
|
||||
Some(WorktreePlan {
|
||||
branch_name,
|
||||
base_sha,
|
||||
worktree_path: RunScratch::new(&options.run_options.run_dir).worktree_dir(),
|
||||
skip_branch_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn worktree_skipped_notice(mode: Option<WorktreeMode>) -> Option<(RunNoticeCode, &'static str)> {
|
||||
matches!(mode, Some(WorktreeMode::Always)).then_some((
|
||||
RunNoticeCode::WorktreeSkippedNoGit,
|
||||
"Worktree mode `always` requested but no Git repository was found; running without a \
|
||||
worktree.",
|
||||
))
|
||||
}
|
||||
|
||||
fn git_setup_intent(run_options: &RunOptions) -> GitSetupIntent {
|
||||
if let Some(source) = run_options.fork_source_ref.as_ref() {
|
||||
GitSetupIntent::ForkFromCheckpoint {
|
||||
|
|
@ -489,11 +328,28 @@ pub async fn initialize(
|
|||
resolve_devcontainer(&mut options).await?;
|
||||
|
||||
let attach_existing = options.checkpoint.is_some();
|
||||
let worktree_plan = if attach_existing {
|
||||
None
|
||||
} else {
|
||||
resolve_worktree_plan(&mut options)
|
||||
};
|
||||
options.run_options.display_base_sha = options
|
||||
.run_options
|
||||
.pre_run_git
|
||||
.as_ref()
|
||||
.and_then(|git| git.sha.clone());
|
||||
if !attach_existing
|
||||
&& !matches!(options.sandbox, SandboxSpec::Local { .. })
|
||||
&& matches!(
|
||||
options
|
||||
.run_options
|
||||
.pre_run_git
|
||||
.as_ref()
|
||||
.map(|git| git.dirty),
|
||||
Some(fabro_types::DirtyStatus::Dirty)
|
||||
)
|
||||
{
|
||||
options.emitter.notice(
|
||||
RunNoticeLevel::Warn,
|
||||
RunNoticeCode::DirtyWorktree,
|
||||
"Uncommitted changes will not be included in the remote sandbox.",
|
||||
);
|
||||
}
|
||||
|
||||
let sandbox_event_callback: SandboxEventCallback = {
|
||||
let emitter = Arc::clone(&options.emitter);
|
||||
|
|
@ -501,7 +357,6 @@ pub async fn initialize(
|
|||
emitter.emit(&Event::Sandbox { event });
|
||||
})
|
||||
};
|
||||
let mut worktree_created = false;
|
||||
let mut sandbox_initialized = true;
|
||||
let sandbox: Arc<dyn Sandbox> = if attach_existing {
|
||||
let run_state = options
|
||||
|
|
@ -530,43 +385,6 @@ pub async fn initialize(
|
|||
.map_err(|err| Error::engine_with_anyhow("Failed to reconnect sandbox for resume", &err))?;
|
||||
sandbox_initialized = false;
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::from(sandbox)))
|
||||
} else if let Some(plan) = worktree_plan.as_ref() {
|
||||
let inner = options
|
||||
.sandbox
|
||||
.build(Some(Arc::clone(&sandbox_event_callback)))
|
||||
.await
|
||||
.map_err(|e| Error::engine_with_anyhow("Failed to build sandbox", &e))?;
|
||||
if let Some(base_sha) = resolve_worktree_base_sha(&*inner, plan).await? {
|
||||
sandbox_git
|
||||
.ensure_git_available(&*inner)
|
||||
.await
|
||||
.map_err(|err| Error::engine_with_source("sandbox git unavailable", &err))?;
|
||||
options.run_options.display_base_sha = Some(base_sha.clone());
|
||||
options.run_options.git = Some(GitCheckpointOptions {
|
||||
base_sha: Some(base_sha.clone()),
|
||||
run_branch: Some(plan.branch_name.clone()),
|
||||
meta_branch: Some(metadata_branch_name(&options.run_id.to_string())),
|
||||
});
|
||||
let mut worktree = WorktreeSandbox::new(inner, WorktreeOptions {
|
||||
branch_name: plan.branch_name.clone(),
|
||||
base_sha,
|
||||
worktree_path: plan.worktree_path.to_string_lossy().into_owned(),
|
||||
skip_branch_creation: plan.skip_branch_creation,
|
||||
setup_intent: Some(git_setup_intent(&options.run_options)),
|
||||
});
|
||||
worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback());
|
||||
match worktree.initialize().await {
|
||||
Ok(()) => {
|
||||
worktree_created = true;
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree)))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(Error::engine_with_source("Git worktree setup failed", &e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Arc::new(ReadBeforeWriteSandbox::new(inner))
|
||||
}
|
||||
} else {
|
||||
Arc::new(ReadBeforeWriteSandbox::new(
|
||||
options
|
||||
|
|
@ -576,16 +394,6 @@ pub async fn initialize(
|
|||
.map_err(|e| Error::engine_with_anyhow("Failed to build sandbox", &e))?,
|
||||
))
|
||||
};
|
||||
if worktree_plan.is_some() && !worktree_created {
|
||||
if let Some((code, message)) = worktree_skipped_notice(options.worktree_mode) {
|
||||
tracing::warn!(
|
||||
worktree_mode = ?options.worktree_mode,
|
||||
"worktree skipped: cwd is not a git repository"
|
||||
);
|
||||
options.emitter.notice(RunNoticeLevel::Warn, code, message);
|
||||
}
|
||||
options.run_options.git = None;
|
||||
}
|
||||
let cleanup_guard = (!attach_existing).then(|| {
|
||||
scopeguard::guard(Arc::clone(&sandbox), |sandbox| {
|
||||
if let Ok(handle) = Handle::try_current() {
|
||||
|
|
@ -856,7 +664,6 @@ mod tests {
|
|||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_sandbox::config::WorktreeMode;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures};
|
||||
use fabro_vault::{SecretType, Vault};
|
||||
|
|
@ -979,7 +786,6 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
in_place: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1038,7 +844,6 @@ mod tests {
|
|||
vault: None,
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: None,
|
||||
run_control: None,
|
||||
registry_override: None,
|
||||
artifact_sink: None,
|
||||
|
|
@ -1050,77 +855,6 @@ mod tests {
|
|||
(result, events)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_worktree_plan_uses_local_worktree_without_pre_run_git_context() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let run_dir = temp.path().join("run");
|
||||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
let store = memory_store();
|
||||
let emitter = Arc::new(crate::event::Emitter::new(test_run_id()));
|
||||
let mut options = InitOptions {
|
||||
run_id: test_run_id(),
|
||||
run_store: {
|
||||
let inner = store.create_run(&test_run_id()).await.unwrap();
|
||||
inner.into()
|
||||
},
|
||||
dry_run: false,
|
||||
emitter: emitter.clone(),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: std::env::current_dir().unwrap(),
|
||||
},
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
},
|
||||
interviewer: Arc::new(AutoApproveInterviewer::engine()),
|
||||
steering_hub: Arc::new(crate::steering_hub::SteeringHub::new(emitter.clone())),
|
||||
lifecycle: crate::run_options::LifecycleOptions {
|
||||
setup_commands: vec![],
|
||||
setup_command_timeout_ms: 1_000,
|
||||
devcontainer_phases: vec![],
|
||||
},
|
||||
run_options: test_settings(&run_dir),
|
||||
workflow_path: None,
|
||||
workflow_bundle: None,
|
||||
hooks: fabro_hooks::HookSettings { hooks: vec![] },
|
||||
sandbox_env: SandboxEnvSpec {
|
||||
devcontainer_env: HashMap::new(),
|
||||
toml_env: HashMap::new(),
|
||||
github_permissions: None,
|
||||
origin_url: None,
|
||||
},
|
||||
vault: None,
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: Some(WorktreeMode::Always),
|
||||
run_control: None,
|
||||
registry_override: None,
|
||||
artifact_sink: None,
|
||||
checkpoint: None,
|
||||
seed_context: None,
|
||||
};
|
||||
|
||||
let plan = resolve_worktree_plan(&mut options);
|
||||
|
||||
assert!(plan.is_some());
|
||||
assert!(options.run_options.display_base_sha.is_none());
|
||||
assert!(options.run_options.git.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worktree_skipped_notice_only_warns_for_always() {
|
||||
assert!(worktree_skipped_notice(None).is_none());
|
||||
assert!(worktree_skipped_notice(Some(WorktreeMode::Clean)).is_none());
|
||||
assert!(worktree_skipped_notice(Some(WorktreeMode::Dirty)).is_none());
|
||||
assert!(worktree_skipped_notice(Some(WorktreeMode::Never)).is_none());
|
||||
|
||||
let (code, _) = worktree_skipped_notice(Some(WorktreeMode::Always)).unwrap();
|
||||
assert_eq!(code, RunNoticeCode::WorktreeSkippedNoGit);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_prepares_sandbox_and_uses_persisted_run_dir() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
|
@ -1169,7 +903,6 @@ mod tests {
|
|||
vault: None,
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: None,
|
||||
run_control: None,
|
||||
registry_override: None,
|
||||
artifact_sink: None,
|
||||
|
|
@ -1310,7 +1043,6 @@ mod tests {
|
|||
vault: None,
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: None,
|
||||
run_control: None,
|
||||
registry_override: None,
|
||||
artifact_sink: None,
|
||||
|
|
@ -1425,7 +1157,6 @@ mod tests {
|
|||
vault: None,
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: None,
|
||||
run_control: None,
|
||||
registry_override: None,
|
||||
artifact_sink: None,
|
||||
|
|
@ -1491,7 +1222,6 @@ mod tests {
|
|||
vault: None,
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: None,
|
||||
run_control: None,
|
||||
registry_override: None,
|
||||
artifact_sink: None,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use fabro_llm::Provider;
|
|||
use fabro_mcp::config::McpServerSettings;
|
||||
use fabro_model::FallbackTarget;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_sandbox::config::WorktreeMode;
|
||||
use fabro_types::RunId;
|
||||
use fabro_types::settings::run::PullRequestSettings;
|
||||
use fabro_validate::{Diagnostic, Severity};
|
||||
|
|
@ -250,7 +249,6 @@ pub struct InitOptions {
|
|||
pub vault: Option<Arc<AsyncRwLock<Vault>>>,
|
||||
pub devcontainer: Option<DevcontainerSpec>,
|
||||
pub git: Option<GitCheckpointOptions>,
|
||||
pub worktree_mode: Option<WorktreeMode>,
|
||||
pub registry_override: Option<Arc<HandlerRegistry>>,
|
||||
pub artifact_sink: Option<ArtifactSink>,
|
||||
pub run_control: Option<Arc<RunControlState>>,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -29,10 +29,6 @@ export interface ManifestArgs {
|
|||
'dry_run'?: boolean;
|
||||
'auto_approve'?: boolean;
|
||||
'preserve_sandbox'?: boolean;
|
||||
/**
|
||||
* Override `run.sandbox.local.worktree_mode` (e.g. `never` for `--in-place`).
|
||||
*/
|
||||
'worktree_mode'?: string;
|
||||
'label'?: Array<string>;
|
||||
/**
|
||||
* Raw repeated CLI input overrides, each in `KEY=VALUE` form.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,5 @@ export interface NotificationRouteSettings {
|
|||
'provider': string | null;
|
||||
'events': Array<string>;
|
||||
'slack': NotificationProviderSettings | null;
|
||||
'discord': NotificationProviderSettings | null;
|
||||
'teams': NotificationProviderSettings | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
export interface ProjectNamespace {
|
||||
'name': string | null;
|
||||
'description': string | null;
|
||||
'directory': string;
|
||||
'metadata': { [key: string]: string; };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,5 @@ export interface RunSpec {
|
|||
'definition_blob'?: string | null;
|
||||
'git'?: GitContext | null;
|
||||
'fork_source_ref'?: ForkSourceRef | null;
|
||||
'in_place': boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue