refactor(config): move project state under .fabro

Keep project config and checked-in workflows under .fabro so they stay out of
normal repo listings. Update config discovery, CLI project commands, fixtures,
docs, and checked-in workflow paths to use .fabro/project.toml and
.fabro/workflows/*.
This commit is contained in:
Bryan Helmkamp 2026-04-11 12:55:46 -04:00
parent 501f0e76d1
commit dc93404e38
52 changed files with 412 additions and 325 deletions

View file

@ -1,8 +1,5 @@
_version = 1
[project]
directory = "fabro/"
[run.pull_request]
enabled = true
draft = false

View file

@ -8,8 +8,8 @@ digraph Smoke {
toolchain [label="Toolchain", shape=parallelogram, script="rustc --version && cargo --version && bun --version 2>&1", goal_gate=true]
compile_rust [label="Compile Rust", shape=parallelogram, script="cargo check -q --workspace 2>&1", goal_gate=true]
compile_typescript [label="Compile TypeScript", shape=parallelogram, script="cd apps/fabro-web && bun install && bun run typecheck 2>&1", goal_gate=true]
lint_rust [label="Lint Rust", shape=parallelogram, script="cargo fmt --check --all 2>&1 && cargo clippy -q --workspace -- -D warnings 2>&1", goal_gate=true]
test_rust [label="Test Rust", shape=parallelogram, script="cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true]
lint_rust [label="Lint Rust", shape=parallelogram, script="cargo +nightly fmt --check --all 2>&1 && cargo clippy -q --workspace -- -D warnings 2>&1", goal_gate=true]
test_rust [label="Test Rust", shape=parallelogram, script="ulimit -n 4096 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true]
test_typescript [label="Test TypeScript", shape=parallelogram, script="cd apps/fabro-web && bun test 2>&1", goal_gate=true]
start -> toolchain

View file

@ -107,7 +107,7 @@ Never run `cargo insta accept` without first checking what's pending — it acce
## Testing workflows
- `fabro run <name>` — run a workflow by name (resolves `fabro/workflows/<name>/workflow.toml`), e.g. `fabro run repl`
- `fabro run <name>` — run a workflow by name (resolves `.fabro/workflows/<name>/workflow.toml`), e.g. `fabro run repl`
- Use `--no-retro` to skip the retro step and finish faster
- `#[e2e_test(twin, live("VAR"))]` — dual-mode test that runs against twin-openai or real API. `#[e2e_test(twin)]` for twin-only tests (e.g., scripted failures). `#[e2e_test(live("VAR"))]` for live-only tests requiring secrets. `#[e2e_test()]` for sandbox tests with no API deps. Behavior is controlled by `FABRO_TEST_MODE` (`live`, `strict`; default is `twin`), and `cargo nextest run --profile e2e ...` implies `strict`. Use `fabro_test::e2e_openai!()` in twin/dual-mode tests to get `(base_url, api_key)`.
- Local test HTTP clients must use `.no_proxy()`. Prefer shared helpers like `fabro_test::test_http_client()` or crate-local equivalents instead of `reqwest::Client::new()`, bare `Client::builder().build()`, or `reqwest::get(...)`.

View file

@ -32,13 +32,13 @@ export async function run(argv: string[]) {
const { values } = parseArgs({
args: argv,
options: {
config: { type: "string", short: "c", default: "fabro.toml" },
config: { type: "string", short: "c", default: ".fabro/project.toml" },
"dry-run": { type: "boolean", default: false },
},
});
const opts: RunOptions = {
config: values.config ?? "fabro.toml",
config: values.config ?? ".fabro/project.toml",
dryRun: values["dry-run"] ?? false,
};
@ -71,14 +71,14 @@ export async function run(argv: string[]) {
const { values } = parseArgs({
args: argv,
options: {
config: { type: "string", short: "c", default: "fabro.toml" },
config: { type: "string", short: "c", default: ".fabro/project.toml" },
"dry-run": { type: "boolean", default: false },
verbose: { type: "boolean", short: "v", default: false },
},
});
const opts: RunOptions = {
config: values.config ?? "fabro.toml",
config: values.config ?? ".fabro/project.toml",
dryRun: values["dry-run"] ?? false,
verbose: values.verbose ?? false,
};

View file

@ -28,5 +28,5 @@ test("watch mode keeps running until interrupted", async () => {
}
process.kill("SIGINT");
expect(await process.exited).toBe(0);
expect([0, 130]).toContain(await process.exited);
});

View file

@ -13,7 +13,7 @@ name = "imagegen-tools-v3"
cpu = 4
memory = 8
disk = 10
dockerfile = { path = "../../fabro/workflows/imagegen/Dockerfile.imagegen" }
dockerfile = { path = "../../.fabro/workflows/imagegen/Dockerfile.imagegen" }
[assets]
include = ["output/**"]

View file

@ -18,7 +18,7 @@ Legacy `server.toml`, `user.toml`, and `cli.toml` are ignored with a warning. Re
| Scope | Examples |
|---|---|
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]`, `[features]` |
| Shared run defaults (layered through `fabro.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.sandbox]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |
| Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.sandbox]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |
The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing.
@ -135,7 +135,7 @@ When `enabled = false`, the server still exposes the machine API and `/health`,
The `[run.*]` sections in `settings.toml` act as defaults for every run.
On a same-machine setup, `settings.toml` is the shared machine-default layer under `workflow.toml` and `fabro.toml`.
On a same-machine setup, `settings.toml` is the shared machine-default layer under `workflow.toml` and `.fabro/project.toml`.
On a remote setup, the client bundles workflow, project, and user config into the run manifest. The server then layers those bundled client configs over its own local defaults for run-shaped fields. Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, `[features]`, and `[server.scheduler]` always come from the server machine's own `settings.toml` or `fabro server start` flags.
@ -194,7 +194,7 @@ Toggle experimental or opt-in features. All features default to `false`.
|---|---|
| `session_sandboxes` | Enable session sandboxes in the web UI |
The same `[features]` section can be set in `fabro.toml` (project-level) to enable features per-project.
The same `[features]` section can be set in `.fabro/project.toml` (project-level) to enable features per-project.
## Secrets and environment variables

View file

@ -105,9 +105,9 @@ Each hook fires on a specific lifecycle event:
Hooks are defined as `[[hooks]]` entries in any of these TOML config files:
- **`fabro.toml`** — project-level hooks, apply to all workflows in the project
- **`.fabro/project.toml`** — project-level hooks, apply to all workflows in the project
- **`workflow.toml`** — per-workflow hooks
- **`~/.fabro/settings.toml`** or **`~/.fabro/settings.toml`** — global defaults for all runs
- **`~/.fabro/settings.toml`** — global defaults for all runs
See [Merging hook configs](#merging-hook-configs) for how these layers combine.
@ -344,11 +344,11 @@ Command hooks do **not** fail open. A non-zero exit code (other than 0 or 2) pro
Hooks from multiple config files are merged in this order (later layers win on name collisions):
1. **`~/.fabro/settings.toml`** or **`~/.fabro/settings.toml`** — global defaults
2. **`fabro.toml`** — project-level overrides
1. **`~/.fabro/settings.toml`** — global defaults
2. **`.fabro/project.toml`** — project-level overrides
3. **`workflow.toml`** — per-workflow overrides
This lets you define global hooks at the server level, project-wide hooks in `fabro.toml`, and override or extend them per workflow.
This lets you define global hooks at the server level, project-wide hooks in `.fabro/project.toml`, and override or extend them per workflow.
## Full example

View file

@ -2219,7 +2219,7 @@ components:
path:
type: string
description: Resolved path that keys into the workflows map.
example: fabro/workflows/smoke/workflow.fabro
example: .fabro/workflows/smoke/workflow.fabro
ManifestConfig:
type: object

View file

@ -113,9 +113,9 @@ Both phases run automatically at the end of every CLI run. The API server derive
### CLI
To enable retros for your project, set `retros = true` under `[run.execution]` in your `fabro.toml`:
To enable retros for your project, set `retros = true` under `[run.execution]` in your `.fabro/project.toml`:
```toml title="fabro.toml"
```toml title=".fabro/project.toml"
_version = 1
[run.execution]

View file

@ -37,7 +37,7 @@ Goal precedence: CLI `--goal` > `[run].goal` > Graphviz graph attribute.
_version = 1
[workflow]
graph = "fabro/workflows/ci.fabro"
graph = ".fabro/workflows/ci.fabro"
[run]
goal = "Run the CI pipeline"
@ -389,7 +389,7 @@ Settings can come from multiple sources. Fabro resolves them in this order (firs
| Node-level [stylesheet](/workflows/stylesheets) | Highest |
| CLI flags (`--model`, `--provider`, `--sandbox`) | |
| Run config TOML (`workflow.toml` or equivalent) | |
| Project defaults (`fabro.toml`) | |
| Project defaults (`.fabro/project.toml`) | |
| Machine defaults (`~/.fabro/settings.toml`) | |
| Graphviz graph attributes (`default_model`, `default_provider`) | |
| Built-in defaults | Lowest |
@ -398,16 +398,13 @@ Settings can come from multiple sources. Fabro resolves them in this order (firs
Stylesheet rules on individual nodes always take priority over run config values.
</Note>
### Project defaults (`fabro.toml`)
### Project defaults (`.fabro/project.toml`)
The `fabro.toml` project config can set default values for any of the `[run.*]` sections described above. These defaults apply to all runs in the project unless the workflow config overrides them:
The `.fabro/project.toml` project config can set default values for any of the `[run.*]` sections described above. These defaults apply to all runs in the project unless the workflow config overrides them:
```toml title="fabro.toml"
```toml title=".fabro/project.toml"
_version = 1
[project]
directory = "fabro/"
[run.model]
name = "claude-sonnet-4-5"

View file

@ -37,7 +37,7 @@ cd my-repo/
fabro repo init
```
This creates a default workflow and configuration in your project directory.
This creates `.fabro/project.toml` and a starter workflow under `.fabro/workflows/hello/`.
## Configure API keys

View file

@ -145,7 +145,7 @@ permissions = { contents = "write", pull_requests = "write" }
Only the listed permissions are requested — the token is scoped to the minimum access needed. If the GitHub App isn't configured or the repository lacks an installation, the run logs a warning and continues without the token.
This also works in `fabro.toml` as a project-level default, so all workflows in the project automatically get a `GITHUB_TOKEN` without repeating the config in each run TOML.
This also works in `.fabro/project.toml` as a project-level default, so all workflows in the project automatically get a `GITHUB_TOKEN` without repeating the config in each run TOML.
### Checkpoint pushing

View file

@ -62,7 +62,7 @@ fabro settings demo
fabro settings run.toml
```
With no argument, Fabro prints the merged ambient defaults from `~/.fabro/settings.toml` and the nearest `fabro.toml`.
With no argument, Fabro prints the merged ambient defaults from `~/.fabro/settings.toml` and the nearest `.fabro/project.toml`.
When you pass a workflow name or path:
@ -86,7 +86,7 @@ fabro run run.toml
| Argument / Flag | Description |
|---|---|
| `<WORKFLOW>` | Path to a `.fabro` workflow file, `.toml` task config, or workflow name (resolved from `fabro/workflows/` in the project, then `~/.fabro/workflows/`). |
| `<WORKFLOW>` | Path to a `.fabro` workflow file, `.toml` task config, or workflow name (resolved from `.fabro/workflows/` in the project, then `~/.fabro/workflows/`). |
| `--dry-run` | Execute with a simulated LLM backend |
| `--auto-approve` | Auto-approve all human gates |
| `--model <MODEL>` | Override default LLM model |
@ -615,7 +615,7 @@ fabro workflow create my-workflow --goal "Run the CI pipeline"
| `<NAME>` | Name of the workflow (required) |
| `-g, --goal <GOAL>` | Goal description for the workflow |
Requires a `fabro.toml` project config in the current directory or a parent.
Requires a `.fabro/project.toml` project config in the current directory or a parent.
---
@ -648,22 +648,22 @@ fabro parse workflow.fabro
## `fabro repo init`
Initialize a new Fabro project in the current git repository. Creates a `fabro.toml` project config and a sample `hello` workflow.
Initialize a new Fabro project in the current git repository. Creates a `.fabro/project.toml` project config and a sample `hello` workflow.
```bash
fabro repo init
```
The command must be run inside a git repository. It creates:
- `fabro.toml` — project configuration with comments and a link to docs
- `fabro/workflows/hello/workflow.fabro` — a simple greeting workflow
- `fabro/workflows/hello/workflow.toml` — run config for the hello workflow
- `.fabro/project.toml` — project configuration with comments and a link to docs
- `.fabro/workflows/hello/workflow.fabro` — a simple greeting workflow
- `.fabro/workflows/hello/workflow.toml` — run config for the hello workflow
After creating files, it checks whether the GitHub App is installed for the repository. If the app is not installed and the repository owner differs from the app owner, it warns that the app may need to be [made public](/integrations/github#github-app-is-private-but-this-repo-belongs-to-a-different-owner) first.
## `fabro repo deinit`
Remove Fabro from a project by deleting `fabro.toml` and the `fabro/` directory. Fails with an error if the project is not initialized.
Remove Fabro from a project by deleting the `.fabro/` project directory. Fails with an error if the project is not initialized.
```bash
fabro repo deinit

View file

@ -29,7 +29,7 @@ Files that omit `_version` are treated as version `1`. The legacy top-level `ver
## Who reads what
`settings.toml` uses the same schema as `fabro.toml` and `workflow.toml`, but each process only reads the fields it understands. The top-level schema is strictly namespaced — the only allowed domains are `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, and `[features]`.
`settings.toml` uses the same schema as `.fabro/project.toml` and `workflow.toml`, but each process only reads the fields it understands. The top-level schema is strictly namespaced — the only allowed domains are `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, and `[features]`.
| Scope | Examples |
|---|---|
@ -37,7 +37,7 @@ Files that omit `_version` are treated as version `1`. The legacy top-level `ver
| Shared run defaults | `[run.model]`, `[run.sandbox]`, `[run.checkpoint]`, `[run.inputs]`, `[run.prepare]`, `[run.pull_request]`, `[run.hooks]`, `[run.agent.mcps]` |
| Server-only | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
`[cli.*]` and `[server.*]` stanzas are owner-specific: they are only consumed from `~/.fabro/settings.toml` (plus process-local flags and env overrides). The same stanzas in `fabro.toml` or `workflow.toml` remain schema-valid but runtime-inert.
`[cli.*]` and `[server.*]` stanzas are owner-specific: they are only consumed from `~/.fabro/settings.toml` (plus process-local flags and env overrides). The same stanzas in `.fabro/project.toml` or `workflow.toml` remain schema-valid but runtime-inert.
See [Server Configuration](/administration/server-configuration) for the server-owned sections.
@ -48,7 +48,7 @@ Shared layered domains (`[project]`, `[workflow]`, `[run]`, `[features]`) use th
1. **CLI flags** — always win
2. **Environment overrides** — Fabro-defined override channels
3. **`workflow.toml`** — per-workflow overrides
4. **`fabro.toml`** — project defaults
4. **`.fabro/project.toml`** — project defaults
5. **`~/.fabro/settings.toml`** — machine defaults
6. **Built-in defaults**
@ -297,7 +297,7 @@ enabled = true
| `auto_merge` | Enable GitHub auto-merge on the created PR (implies `draft = false`) | `false` |
| `merge_strategy` | One of `"squash"`, `"merge"`, `"rebase"` | `"squash"` |
Precedence: `workflow.toml` > `fabro.toml` > `~/.fabro/settings.toml` > built-in default (`false`).
Precedence: `workflow.toml` > `.fabro/project.toml` > `~/.fabro/settings.toml` > built-in default (`false`).
## `[run.agent.mcps]` section

View file

@ -97,7 +97,7 @@ You can also emit literal braces with expressions such as `{{ '{{' }}` when need
|---|---|
| CLI flags (`-V key=value`, repeated) | Highest |
| `workflow.toml` `[run.inputs]` | |
| `fabro.toml` `[run.inputs]` | |
| `.fabro/project.toml` `[run.inputs]` | |
| `~/.fabro/settings.toml` `[run.inputs]` | Lowest |
If you need per-key overrides on top of inherited defaults, set each input explicitly in the winning layer.

View file

@ -113,7 +113,7 @@ Allowed setup:
- checked-in workflow fixtures
- temp `.fabro` workflow files
- temp `workflow.toml` and `fabro.toml`
- temp `workflow.toml` and `.fabro/project.toml`
- temp git repositories
- temp user config and environment variables
- invoking commands to create runs, checkpoints, branches, and persisted state

View file

@ -1235,7 +1235,7 @@ pub(crate) struct RepoNamespace {
pub(crate) enum RepoCommand {
/// Initialize a new project
Init(RepoInitArgs),
/// Remove fabro.toml and fabro/ directory
/// Remove .fabro/ project directory
Deinit,
}

View file

@ -6,41 +6,27 @@ pub(crate) fn run_deinit(globals: &GlobalArgs) -> Result<Vec<String>> {
let repo_root = super::init::git_repo_root()?;
let mut removed = Vec::new();
let fabro_toml = repo_root.join("fabro.toml");
let fabro_dir = repo_root.join(".fabro");
let project_toml = fabro_dir.join("project.toml");
let green = console::Style::new().green();
let dim = console::Style::new().dim();
match std::fs::remove_file(&fabro_toml) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
bail!("not initialized — fabro.toml not found");
}
Err(e) => bail!("failed to remove {}: {e}", fabro_toml.display()),
if !project_toml.exists() {
bail!("not initialized — .fabro/project.toml not found");
}
removed.push("fabro.toml".to_string());
std::fs::remove_dir_all(&fabro_dir)
.with_context(|| format!("failed to remove {}", fabro_dir.display()))?;
removed.push(".fabro/".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro.toml")
dim.apply_to("removed .fabro/")
);
}
let fabro_dir = repo_root.join("fabro");
if fabro_dir.exists() {
std::fs::remove_dir_all(&fabro_dir)
.with_context(|| format!("failed to remove {}", fabro_dir.display()))?;
removed.push("fabro/".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro/")
);
}
}
if !globals.json {
eprintln!(
"\n{}",

View file

@ -25,26 +25,27 @@ pub(crate) async fn run_init(args: &RepoInitArgs, globals: &GlobalArgs) -> Resul
let repo_root = git_repo_root()?;
let mut created = Vec::new();
let fabro_toml = repo_root.join("fabro.toml");
if fabro_toml.exists() {
let fabro_dir = repo_root.join(".fabro");
let project_toml = fabro_dir.join("project.toml");
if project_toml.exists() {
bail!(
"already initialized — fabro.toml exists at {}",
fabro_toml.display()
"already initialized — .fabro/project.toml exists at {}",
project_toml.display()
);
}
// Create fabro.toml
std::fs::create_dir_all(&fabro_dir)
.with_context(|| format!("failed to create {}", fabro_dir.display()))?;
// Create .fabro/project.toml
std::fs::write(
&fabro_toml,
&project_toml,
"\
# Fabro project configuration
# https://docs.fabro.computer/getting-started/quick-start
_version = 1
[project]
directory = \"fabro/\"
# Auto-create pull requests on successful workflow runs.
[run.pull_request]
enabled = true
@ -52,18 +53,22 @@ draft = true
# auto_merge = true
",
)
.with_context(|| format!("failed to write {}", fabro_toml.display()))?;
created.push("fabro.toml".to_string());
.with_context(|| format!("failed to write {}", project_toml.display()))?;
created.push(".fabro/project.toml".to_string());
let green = console::Style::new().green();
let bold = console::Style::new().bold();
let dim = console::Style::new().dim();
if !globals.json {
eprintln!(" {} {}", green.apply_to(""), dim.apply_to("fabro.toml"));
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to(".fabro/project.toml")
);
}
// Create hello workflow directory
let workflow_dir = repo_root.join("fabro/workflows/hello");
let workflow_dir = repo_root.join(".fabro/workflows/hello");
std::fs::create_dir_all(&workflow_dir)
.with_context(|| format!("failed to create {}", workflow_dir.display()))?;
@ -85,12 +90,12 @@ draft = true
"#,
)
.with_context(|| format!("failed to write {}", dot_path.display()))?;
created.push("fabro/workflows/hello/workflow.fabro".to_string());
created.push(".fabro/workflows/hello/workflow.fabro".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("fabro/workflows/hello/workflow.fabro")
dim.apply_to(".fabro/workflows/hello/workflow.fabro")
);
}
@ -101,12 +106,12 @@ draft = true
"_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n\n[run.sandbox]\nprovider = \"local\"\n",
)
.with_context(|| format!("failed to write {}", toml_path.display()))?;
created.push("fabro/workflows/hello/workflow.toml".to_string());
created.push(".fabro/workflows/hello/workflow.toml".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("fabro/workflows/hello/workflow.toml")
dim.apply_to(".fabro/workflows/hello/workflow.toml")
);
}

View file

@ -11,7 +11,7 @@ pub(super) fn create_command(args: &WorkflowCreateArgs, globals: &GlobalArgs) ->
let Some((config_path, config)) = discover_project_config(&cwd)? else {
bail!(
"No fabro.toml found in {cwd} or any parent directory",
"No .fabro/project.toml found in {cwd} or any parent directory",
cwd = cwd.display()
);
};

View file

@ -16,7 +16,7 @@ pub(super) fn list_command(_args: &WorkflowListArgs, globals: &GlobalArgs) -> Re
let Some((config_path, config)) = discover_project_config(&cwd)? else {
bail!(
"No fabro.toml found in {cwd} or any parent directory",
"No .fabro/project.toml found in {cwd} or any parent directory",
cwd = cwd.display()
);
};

View file

@ -556,12 +556,12 @@ mod tests {
fn build_manifest_bundles_imports_prompts_and_children() {
let temp = tempfile::tempdir().unwrap();
let project = temp.path();
let workflow_dir = project.join("fabro/workflows/demo");
let child_dir = project.join("fabro/workflows/child");
let workflow_dir = project.join(".fabro/workflows/demo");
let child_dir = project.join(".fabro/workflows/child");
std::fs::create_dir_all(workflow_dir.join("prompts")).unwrap();
std::fs::create_dir_all(workflow_dir.join("imports")).unwrap();
std::fs::create_dir_all(&child_dir).unwrap();
std::fs::write(project.join("fabro.toml"), "_version = 1\n").unwrap();
std::fs::write(project.join(".fabro/project.toml"), "_version = 1\n").unwrap();
std::fs::write(
workflow_dir.join("workflow.toml"),
"_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n",
@ -600,7 +600,7 @@ mod tests {
.unwrap();
let built = build_run_manifest(ManifestBuildInput {
workflow: PathBuf::from("fabro/workflows/demo/workflow.toml"),
workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"),
cwd: project.to_path_buf(),
args_layer: SettingsLayer::default(),
args: None,
@ -612,49 +612,49 @@ mod tests {
assert_eq!(
built.manifest.target.path,
"fabro/workflows/demo/workflow.fabro"
".fabro/workflows/demo/workflow.fabro"
);
assert_eq!(built.manifest.workflows.len(), 2);
let root = &built.manifest.workflows["fabro/workflows/demo/workflow.fabro"];
let root = &built.manifest.workflows[".fabro/workflows/demo/workflow.fabro"];
assert!(
root.files
.contains_key("fabro/workflows/demo/prompts/goal.md")
.contains_key(".fabro/workflows/demo/prompts/goal.md")
);
assert!(
root.files
.contains_key("fabro/workflows/demo/prompts/plan.md")
.contains_key(".fabro/workflows/demo/prompts/plan.md")
);
assert!(
root.files
.contains_key("fabro/workflows/demo/imports/checks.fabro")
.contains_key(".fabro/workflows/demo/imports/checks.fabro")
);
assert!(
root.files
.contains_key("fabro/workflows/demo/prompts/lint.md")
.contains_key(".fabro/workflows/demo/prompts/lint.md")
);
assert_eq!(built.manifest.goal.unwrap().text, "ship it");
assert!(
built
.manifest
.workflows
.contains_key("fabro/workflows/child/workflow.fabro")
.contains_key(".fabro/workflows/child/workflow.fabro")
);
}
/// A relative `[run.goal] file = "..."` declared in `fabro.toml` must
/// resolve against the directory of `fabro.toml`, not against the
/// invocation cwd. We exercise this by invoking from a subdirectory
/// A relative `[run.goal] file = "..."` declared in `.fabro/project.toml`
/// must resolve against the directory of `.fabro/project.toml`, not against
/// the invocation cwd. We exercise this by invoking from a subdirectory
/// below the project root.
#[test]
fn build_manifest_resolves_relative_goal_file_in_project_config() {
let temp = tempfile::tempdir().unwrap();
let project = temp.path();
let workflow_dir = project.join("fabro/workflows/demo");
let workflow_dir = project.join(".fabro/workflows/demo");
std::fs::create_dir_all(&workflow_dir).unwrap();
std::fs::create_dir_all(project.join("prompts")).unwrap();
std::fs::create_dir_all(project.join(".fabro/prompts")).unwrap();
std::fs::write(
project.join("fabro.toml"),
project.join(".fabro/project.toml"),
r#"_version = 1
[run.goal]
@ -662,7 +662,11 @@ file = "prompts/goal.md"
"#,
)
.unwrap();
std::fs::write(project.join("prompts/goal.md"), "ship from project root").unwrap();
std::fs::write(
project.join(".fabro/prompts/goal.md"),
"ship from project root",
)
.unwrap();
std::fs::write(
workflow_dir.join("workflow.toml"),
@ -676,7 +680,7 @@ file = "prompts/goal.md"
.unwrap();
let built = build_run_manifest(ManifestBuildInput {
workflow: PathBuf::from("fabro/workflows/demo/workflow.toml"),
workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"),
cwd: project.to_path_buf(),
args_layer: SettingsLayer::default(),
args: None,
@ -690,7 +694,7 @@ file = "prompts/goal.md"
assert_eq!(goal.text, "ship from project root");
assert_eq!(goal.type_, types::ManifestGoalType::File);
let resolved = goal.path.expect("file goal must carry a path");
let expected = project.join("prompts").join("goal.md");
let expected = project.join(".fabro").join("prompts").join("goal.md");
assert_eq!(PathBuf::from(resolved), expected);
}
@ -701,10 +705,10 @@ file = "prompts/goal.md"
fn build_manifest_resolves_relative_goal_file_in_workflow_config() {
let temp = tempfile::tempdir().unwrap();
let project = temp.path();
let workflow_dir = project.join("fabro/workflows/demo");
let workflow_dir = project.join(".fabro/workflows/demo");
std::fs::create_dir_all(workflow_dir.join("prompts")).unwrap();
std::fs::write(project.join("fabro.toml"), "_version = 1\n").unwrap();
std::fs::write(project.join(".fabro/project.toml"), "_version = 1\n").unwrap();
std::fs::write(
workflow_dir.join("workflow.toml"),
r#"_version = 1
@ -729,7 +733,7 @@ file = "prompts/goal.md"
.unwrap();
let built = build_run_manifest(ManifestBuildInput {
workflow: PathBuf::from("fabro/workflows/demo/workflow.toml"),
workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"),
cwd: project.to_path_buf(),
args_layer: SettingsLayer::default(),
args: None,

View file

@ -200,14 +200,12 @@ shared = "cli"
);
let project = tempfile::tempdir().unwrap();
std::fs::create_dir_all(project.path().join(".fabro")).unwrap();
std::fs::write(
project.path().join("fabro.toml"),
project.path().join(".fabro/project.toml"),
r#"
_version = 1
[project]
directory = "fabro"
[run.model]
name = "project-model"
@ -224,7 +222,7 @@ script = "echo project"
)
.unwrap();
let workflow_dir = project.path().join("fabro").join("workflows").join("demo");
let workflow_dir = project.path().join(".fabro").join("workflows").join("demo");
std::fs::create_dir_all(&workflow_dir).unwrap();
std::fs::write(
workflow_dir.join("workflow.toml"),
@ -315,8 +313,9 @@ script = "cli-setup"
);
let project = tempfile::tempdir().unwrap();
std::fs::create_dir_all(project.path().join(".fabro")).unwrap();
std::fs::write(
project.path().join("fabro.toml"),
project.path().join(".fabro/project.toml"),
r#"
_version = 1
@ -387,7 +386,7 @@ fn settings_local_merges_cli_and_project_defaults() {
assert_eq!(run_model_name(&cfg).as_deref(), Some("project-model"));
assert_eq!(run_model_provider(&cfg).as_deref(), Some("openai"));
assert_eq!(run_goal_inline(&cfg).as_deref(), None);
assert_eq!(resolve_project(&cfg).directory, "fabro");
assert_eq!(resolve_project(&cfg).directory, ".");
// v2 R22: run.inputs replaces the inherited map wholesale rather than
// merging by key, so the project layer wipes out the CLI layer's inputs.

View file

@ -2,9 +2,9 @@ use fabro_test::{fabro_snapshot, test_context};
fn init_fabro_project(context: &fabro_test::TestContext) {
context
.write_temp("fabro.toml", "_version = 1\n")
.write_temp("fabro/workflows/hello/workflow.fabro", "digraph {}")
.write_temp("fabro/workflows/hello/workflow.toml", "_version = 1\n");
.write_temp(".fabro/project.toml", "_version = 1\n")
.write_temp(".fabro/workflows/hello/workflow.fabro", "digraph {}")
.write_temp(".fabro/workflows/hello/workflow.toml", "_version = 1\n");
}
#[test]
@ -22,7 +22,7 @@ fn help() {
Commands:
init Initialize a new project
deinit Remove fabro.toml and fabro/ directory
deinit Remove .fabro/ project directory
help Print this message or the help of the given subcommand(s)
Options:
@ -42,18 +42,18 @@ fn test_repo_deinit_removes_fabro_toml_and_dir() {
context.git_init();
init_fabro_project(&context);
assert!(context.temp_dir.join("fabro.toml").exists());
assert!(context.temp_dir.join("fabro").exists());
assert!(context.temp_dir.join(".fabro/project.toml").exists());
assert!(context.temp_dir.join(".fabro").exists());
context.repo().arg("deinit").assert().success();
assert!(
!context.temp_dir.join("fabro.toml").exists(),
"fabro.toml should be removed"
!context.temp_dir.join(".fabro/project.toml").exists(),
".fabro/project.toml should be removed"
);
assert!(
!context.temp_dir.join("fabro").exists(),
"fabro/ directory should be removed"
!context.temp_dir.join(".fabro").exists(),
".fabro/ directory should be removed"
);
}
@ -69,6 +69,6 @@ fn test_repo_deinit_fails_when_not_initialized() {
exit_code: 1
----- stdout -----
----- stderr -----
error: not initialized fabro.toml not found
error: not initialized .fabro/project.toml not found
");
}

View file

@ -9,7 +9,7 @@ fn help() {
success: true
exit_code: 0
----- stdout -----
Remove fabro.toml and fabro/ directory
Remove .fabro/ project directory
Usage: fabro repo deinit [OPTIONS]

View file

@ -27,7 +27,7 @@ fn help() {
}
#[test]
fn repo_init_creates_fabro_toml_and_hello_workflow() {
fn repo_init_creates_project_toml_and_hello_workflow() {
let context = test_context!();
context.git_init();
@ -39,9 +39,9 @@ fn repo_init_creates_fabro_toml_and_hello_workflow() {
exit_code: 0
----- stdout -----
----- stderr -----
fabro.toml
fabro/workflows/hello/workflow.fabro
fabro/workflows/hello/workflow.toml
.fabro/project.toml
.fabro/workflows/hello/workflow.fabro
.fabro/workflows/hello/workflow.toml
Project initialized! Run a workflow with:
@ -52,16 +52,13 @@ fn repo_init_creates_fabro_toml_and_hello_workflow() {
");
assert_snapshot!(
std::fs::read_to_string(context.temp_dir.join("fabro.toml")).unwrap(),
std::fs::read_to_string(context.temp_dir.join(".fabro/project.toml")).unwrap(),
@r###"
# Fabro project configuration
# https://docs.fabro.computer/getting-started/quick-start
_version = 1
[project]
directory = "fabro/"
# Auto-create pull requests on successful workflow runs.
[run.pull_request]
enabled = true
@ -70,7 +67,7 @@ fn repo_init_creates_fabro_toml_and_hello_workflow() {
"###
);
assert_snapshot!(
std::fs::read_to_string(context.temp_dir.join("fabro/workflows/hello/workflow.fabro"))
std::fs::read_to_string(context.temp_dir.join(".fabro/workflows/hello/workflow.fabro"))
.unwrap(),
@r###"
digraph Hello {
@ -87,7 +84,7 @@ fn repo_init_creates_fabro_toml_and_hello_workflow() {
"###
);
assert_snapshot!(
std::fs::read_to_string(context.temp_dir.join("fabro/workflows/hello/workflow.toml"))
std::fs::read_to_string(context.temp_dir.join(".fabro/workflows/hello/workflow.toml"))
.unwrap(),
@r###"
_version = 1
@ -105,7 +102,12 @@ fn repo_init_creates_fabro_toml_and_hello_workflow() {
fn repo_init_rejects_already_initialized_repo() {
let context = test_context!();
context.git_init();
std::fs::write(context.temp_dir.join("fabro.toml"), "_version = 1\n").unwrap();
std::fs::create_dir_all(context.temp_dir.join(".fabro")).unwrap();
std::fs::write(
context.temp_dir.join(".fabro/project.toml"),
"_version = 1\n",
)
.unwrap();
let mut cmd = context.command();
cmd.args(["repo", "init"]);
@ -115,7 +117,7 @@ fn repo_init_rejects_already_initialized_repo() {
exit_code: 1
----- stdout -----
----- stderr -----
error: already initialized fabro.toml exists at [TEMP_DIR]/fabro.toml
error: already initialized .fabro/project.toml exists at [TEMP_DIR]/.fabro/project.toml
");
}

View file

@ -635,7 +635,7 @@ fn worker_exits_with_retro_enabled_even_when_stdin_stays_open() {
let workflow_path = context.temp_dir.join("retro-success.fabro");
context.write_temp(
"fabro.toml",
".fabro/project.toml",
r#"_version = 1
[run.execution]

View file

@ -268,11 +268,8 @@ pub(crate) fn setup_git_backed_noop_run(context: &TestContext) -> GitRunSetup {
pub(crate) fn setup_project_fixture(context: &TestContext) -> ProjectFixture {
let project_dir = context.temp_dir.join("project");
let fabro_root = project_dir.join("fabro");
write_text_file(
&project_dir.join("fabro.toml"),
"_version = 1\n\n[project]\ndirectory = \"fabro/\"\n",
);
let fabro_root = project_dir.join(".fabro");
write_text_file(&project_dir.join(".fabro/project.toml"), "_version = 1\n");
std::fs::create_dir_all(fabro_root.join("workflows"))
.unwrap_or_else(|err| panic!("failed to create {}: {err}", fabro_root.display()));
ProjectFixture {

View file

@ -35,8 +35,8 @@ fn list() {
context
.write_temp(
"fabro.toml",
"_version = 1\n\n[project]\ndirectory = \".\"\n",
".fabro/project.toml",
"_version = 1\n\n[project]\ndirectory = \"..\"\n",
)
.write_temp(
"workflows/my_test_wf/workflow.toml",

View file

@ -46,12 +46,12 @@ fn workflow_create_writes_scaffold_files() {
exit_code: 0
----- stdout -----
----- stderr -----
fabro/workflows/hello-world/workflow.fabro
fabro/workflows/hello-world/workflow.toml
.fabro/workflows/hello-world/workflow.fabro
.fabro/workflows/hello-world/workflow.toml
Workflow created! Next steps:
1. Edit the graph: fabro/workflows/hello-world/workflow.fabro
1. Edit the graph: .fabro/workflows/hello-world/workflow.fabro
2. Validate: fabro validate hello-world
3. Run: fabro run hello-world
");
@ -136,7 +136,7 @@ fn workflow_create_rejects_existing_workflow() {
exit_code: 1
----- stdout -----
----- stderr -----
error: Workflow 'existing' already exists at [TEMP_DIR]/project/fabro/workflows/existing
error: Workflow 'existing' already exists at [TEMP_DIR]/project/.fabro/workflows/existing
");
}
@ -151,7 +151,7 @@ fn workflow_create_errors_without_project_config() {
exit_code: 1
----- stdout -----
----- stderr -----
error: No fabro.toml found in [TEMP_DIR] or any parent directory
error: No .fabro/project.toml found in [TEMP_DIR] or any parent directory
");
}
@ -160,8 +160,8 @@ fn workflow_create_json_uses_resolved_custom_root_paths() {
let context = test_context!();
let project_dir = context.temp_dir.join("project");
context.write_temp(
"project/fabro.toml",
"_version = 1\n\n[project]\ndirectory = \"custom/fabro-data\"\n",
"project/.fabro/project.toml",
"_version = 1\n\n[project]\ndirectory = \"../custom/fabro-data\"\n",
);
let output = context

View file

@ -37,7 +37,7 @@ fn workflow_list_errors_without_project_config() {
exit_code: 1
----- stdout -----
----- stderr -----
error: No fabro.toml found in [TEMP_DIR] or any parent directory
error: No .fabro/project.toml found in [TEMP_DIR] or any parent directory
");
}
@ -69,7 +69,7 @@ fn workflow_list_shows_project_and_user_sections() {
NAME DESCRIPTION
user-beta User beta goal
Project Workflows (fabro/workflows)
Project Workflows (.fabro/workflows)
NAME DESCRIPTION
project-alpha Project alpha goal

View file

@ -2,10 +2,12 @@
//! [`SettingsLayer`].
//!
//! Shared layered domains (`project`, `workflow`, `run`, `features`) merge
//! across all three config files (settings.toml, fabro.toml, workflow.toml).
//! across all three config files (settings.toml, .fabro/project.toml,
//! workflow.toml).
//! Owner-specific domains (`cli`, `server`) are consumed only from the local
//! `~/.fabro/settings.toml` plus explicit process-local overrides — their
//! stanzas in `fabro.toml` and `workflow.toml` remain schema-valid but inert.
//! stanzas in `.fabro/project.toml` and `workflow.toml` remain schema-valid but
//! inert.
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::run::{RunExecutionLayer, RunLayer};
@ -67,7 +69,7 @@ pub fn resolve_settings(
EffectiveSettingsMode::RemoteServer | EffectiveSettingsMode::LocalDaemon => {
let server_settings = server_settings.ok_or(Error::MissingServerSettings)?;
// Owner-specific domains (cli, server) may only come from the
// local ~/.fabro/settings.toml, never from fabro.toml or
// local ~/.fabro/settings.toml, never from .fabro/project.toml or
// workflow.toml. The user layer keeps its cli/server fields.
strip_owner_domains(&mut workflow);
strip_owner_domains(&mut project);

View file

@ -5,7 +5,7 @@
//! discovery helpers and re-exports resolved project settings.
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
use fabro_types::settings::SettingsLayer;
use serde::Serialize;
@ -17,7 +17,7 @@ use crate::{
run,
};
const CONFIG_FILENAME: &str = "fabro.toml";
const CONFIG_FILENAME: &str = ".fabro/project.toml";
#[derive(Clone, Debug)]
pub struct WorkflowPathResolution {
pub resolved_workflow_path: PathBuf,
@ -45,7 +45,7 @@ pub fn load_project_config(path: &Path) -> Result<SettingsLayer> {
Ok(config)
}
/// Walk ancestor directories from `start` looking for `fabro.toml`.
/// Walk ancestor directories from `start` looking for `.fabro/project.toml`.
/// Returns the config file path and parsed config, or `None` if not found.
pub fn discover_project_config(start: &Path) -> Result<Option<(PathBuf, SettingsLayer)>> {
for ancestor in start.ancestors() {
@ -360,9 +360,29 @@ pub fn is_retro_enabled() -> bool {
}
}
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 directory containing `fabro.toml` joined with the
/// `project.directory` value (default: `fabro/`).
/// 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, config: &SettingsLayer) -> PathBuf {
let project_dir = config_path
.parent()
@ -370,7 +390,7 @@ pub fn resolve_fabro_root(config_path: &Path, config: &SettingsLayer) -> PathBuf
let root = resolve_project_from_file(config)
.expect("project settings should resolve")
.directory;
project_dir.join(root)
normalize_joined_path(project_dir, Path::new(&root))
}
#[cfg(test)]
@ -395,13 +415,13 @@ mod tests {
_version = 1
[project]
directory = "fabro/"
directory = "custom/"
"#,
)
.unwrap();
assert_eq!(
resolve_project_from_file(&config).unwrap().directory,
"fabro/"
"custom/"
);
}
@ -449,7 +469,9 @@ retros = true
#[test]
fn load_from_disk() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("fabro.toml");
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));
@ -458,12 +480,14 @@ retros = true
#[test]
fn discover_walks_ancestors() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("fabro.toml"), "_version = 1\n").unwrap();
let config_dir = tmp.path().join(".fabro");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("project.toml"), "_version = 1\n").unwrap();
let sub = tmp.path().join("sub").join("dir");
fs::create_dir_all(&sub).unwrap();
let (found_path, config) = discover_project_config(&sub).unwrap().unwrap();
assert_eq!(found_path, tmp.path().join("fabro.toml"));
assert_eq!(found_path, config_dir.join("project.toml"));
assert_eq!(config.version, Some(1));
}
@ -472,7 +496,9 @@ retros = true
use fabro_types::settings::run::RunGoalLayer;
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("fabro.toml");
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
@ -489,7 +515,75 @@ file = "prompts/goal.md"
else {
panic!("expected file variant");
};
let expected = tmp.path().join("prompts").join("goal.md");
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();
let config = load_project_config(&config_path).unwrap();
assert_eq!(resolve_fabro_root(&config_path, &config), config_dir);
}
#[test]
fn custom_relative_directory_resolves_from_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,
r#"_version = 1
[project]
directory = "../custom"
"#,
)
.unwrap();
let config = load_project_config(&config_path).unwrap();
assert_eq!(
resolve_fabro_root(&config_path, &config),
tmp.path().join("custom")
);
}
#[test]
fn relative_goal_file_resolves_from_config_dir() {
use fabro_types::settings::run::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()
);
}
}

View file

@ -2,7 +2,7 @@ use fabro_types::settings::project::{ProjectLayer, ProjectSettings};
use super::ResolveError;
const DEFAULT_PROJECT_DIRECTORY: &str = "fabro/";
const DEFAULT_PROJECT_DIRECTORY: &str = ".";
pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) -> ProjectSettings {
ProjectSettings {

View file

@ -7,7 +7,7 @@ fn resolves_project_defaults_from_empty_settings() {
let project = resolve_project_from_file(&settings).expect("empty settings should resolve");
assert_eq!(project.directory, "fabro/");
assert_eq!(project.directory, ".");
assert!(project.name.is_none());
assert!(project.description.is_none());
assert!(project.metadata.is_empty());

View file

@ -11,7 +11,7 @@ fn resolves_root_settings_defaults() {
let settings =
fabro_config::resolve(&SettingsLayer::default()).expect("empty settings should resolve");
assert_eq!(settings.project.directory, "fabro/");
assert_eq!(settings.project.directory, ".");
assert_eq!(settings.workflow.graph, "workflow.fabro");
assert!(settings.run.execution.retros);
assert!(settings.cli.updates.check);

View file

@ -3,6 +3,8 @@ use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use crate::secret_store::SecretStoreError;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
@ -21,7 +23,7 @@ pub enum Error {
Config(#[from] fabro_config::Error),
#[error(transparent)]
SecretStore(#[from] crate::secret_store::SecretStoreError),
SecretStore(#[from] SecretStoreError),
#[error("bad request: {0}")]
BadRequest(String),

File diff suppressed because one or more lines are too long

View file

@ -61,7 +61,7 @@
<script type="module" src="/assets/chunk-sadshphz.js"></script>
<script type="module" src="/assets/chunk-pmthkscp.js"></script>
<script type="module" src="/assets/chunk-v61ks9f7.js"></script>
<script type="module" src="/assets/entry-9ys2gakq.js"></script>
<script type="module" src="/assets/entry-2zzbvk48.js"></script>
<script type="module" src="/assets/chunk-n1k68xa8.js"></script>
<script type="module" src="/assets/chunk-rsph5pvm.js"></script>
<script type="module" src="/assets/chunk-9t57pdty.js"></script>

View file

@ -2,7 +2,8 @@
//!
//! `[cli]` is owner-first: the CLI process reads its settings from
//! `~/.fabro/settings.toml` plus process-local overrides. `cli.*` stanzas in
//! `fabro.toml` and `workflow.toml` remain schema-valid but runtime-inert.
//! `.fabro/project.toml` and `workflow.toml` remain schema-valid but
//! runtime-inert.
use std::collections::HashMap;

View file

@ -1,8 +1,9 @@
//! The top-level sparse settings layer.
//!
//! This struct models a single settings file (`~/.fabro/settings.toml`,
//! `fabro.toml`, or `workflow.toml`) after deserialization. Fields unset in
//! the source stay `None`/empty and are layered later by `fabro-config`.
//! `.fabro/project.toml`, or `workflow.toml`) after deserialization. Fields
//! unset in the source stay `None`/empty and are layered later by
//! `fabro-config`.
use serde::{Deserialize, Serialize};

View file

@ -1,7 +1,7 @@
//! 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 `fabro/`.
//! Fabro-managed project directory inside the repo, defaulting to `.`.
use std::collections::HashMap;
@ -25,7 +25,7 @@ pub struct ProjectLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The Fabro-managed project directory inside the repo. Defaults to
/// `fabro/` after layering when unspecified.
/// `.` after layering when unspecified.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]

View file

@ -27,7 +27,7 @@ pub struct RunOptions {
pub run_id: RunId,
/// User-defined key-value labels for this run.
pub labels: HashMap<String, String>,
/// Workflow directory slug (e.g. "smoke" from `fabro/workflows/smoke/`).
/// Workflow directory slug (e.g. "smoke" from `.fabro/workflows/smoke/`).
pub workflow_slug: Option<String>,
/// GitHub App credentials for pushing metadata branches to origin.
pub github_app: Option<fabro_github::GitHubAppCredentials>,