Compare commits

...

89 commits
main ... v0.5.0

Author SHA1 Message Date
Bryan Helmkamp
76d863ca10
Bump version to 0.5.0 2026-03-15 18:33:39 -04:00
Bryan Helmkamp
7292ab4079
Update docs for new CLI commands, GitHub token injection, and events
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:33:18 -04:00
Bryan Helmkamp
ec0a612ea5
Add changelog entries for March 14-15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:28:28 -04:00
Bryan Helmkamp
8b948a2d68
Fix fabro-beastie linker error by linking IOKit framework
The extern block declaring IOPMAssertionCreateWithName and
IOPMAssertionRelease was missing the #[link] attribute, causing
undefined symbol errors on macOS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:27:42 -04:00
arc-1e68f1[bot]
43f5fb0edb
Inject GitHub App IAT into Sandbox as GITHUB_TOKEN (#7)
This PR adds GitHub App Installation Access Token (IAT) injection into
sandboxes, allowing `gh` CLI and other GitHub-authenticated tools to
work seamlessly inside workflow sandboxes. Workflow authors can declare
required GitHub permissions in `workflow.toml` under a `[github]`
section (e.g., `permissions = { contents = "write", pull_requests =
"read" }`), with project-wide defaults available in `fabro.toml`.
Workflow-level config fully replaces project-level defaults, consistent
with existing `[pull_request]` behavior.

The implementation introduces a `GitHubConfig` struct wired through
`WorkflowRunConfig`, `RunDefaults`, and `ProjectConfig`, with proper
`apply_defaults` (inherit if unset) and `merge_overlay` (replace if
present) semantics. At runtime, a new `mint_github_token()` helper signs
a JWT, resolves the repo's owner/repo from the origin URL, and requests
a scoped IAT which is injected as `GITHUB_TOKEN` into the sandbox
environment. The previously private
`create_installation_access_token_with_permissions` in `fabro-github` is
made public to support this. A preflight check also mints a token during
validation to surface credential or permission issues early.

Comprehensive tests cover TOML parsing with and without `[github]`,
default inheritance, workflow-over-default precedence, and overlay merge
semantics for `RunDefaults`.

### Fabro Details

<details>
<summary>Ran 7 stages in 27m 15s for $5.88</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $3.79 | 0 |
| simplify | 0s | $2.09 | 0 |
| verify | 0s | – | 0 |
| **Total** | **27m 15s** | **$5.88** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (10 nodes and 13
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan."]
    simplify          [label="Simplify", prompt="@prompts/simplify.md"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify -> verify
    verify -> exit  [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Claude <claude@anthropic.com>
2026-03-15 18:24:30 -04:00
arc-1e68f1[bot]
527b6252ef
Add goal to WorkflowRunStarted and render in fabro logs --pretty (#6)
This PR adds a `goal` field to the `WorkflowRunStarted` event so that
users can immediately see what a workflow is trying to accomplish when
reading logs. The field is an `Option<String>` with `serde(default,
skip_serializing_if)` to maintain backward compatibility with existing
JSONL logs that don't include it—mirroring the same pattern used by
`base_sha` and `run_branch`.

On the rendering side, `fabro logs --pretty` now displays the goal below
the workflow header line when present, using markdown rendering with
proper indentation and terminal-width wrapping. The markdown rendering
logic was extracted into a shared `render_indented_markdown` helper,
which is also now used by the existing `AssistantMessage` rendering to
eliminate duplication.

Tests cover round-trip serialization with a goal, backward-compatible
deserialization of old events without the field, verification that
`None` goals are omitted from JSON output, and pretty-formatting
behavior both with and without a goal present.

### Fabro Details

<details>
<summary>Ran 7 stages in 23m 38s for $3.02</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.76 | 0 |
| simplify | 0s | $1.26 | 0 |
| verify | 0s | – | 0 |
| **Total** | **23m 38s** | **$3.02** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (10 nodes and 13
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan."]
    simplify          [label="Simplify", prompt="@prompts/simplify.md"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify -> verify
    verify -> exit  [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:46:17 -04:00
dependabot[bot]
0929eaac87
Bump quinn-proto from 0.11.13 to 0.11.14 (#1)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.13 to
0.11.14.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/quinn-rs/quinn/releases">quinn-proto's
releases</a>.</em></p>
<blockquote>
<h2>quinn-proto 0.11.14</h2>
<p><a href="https://github.com/jxs"><code>@​jxs</code></a> reported a
denial of service issue in quinn-proto 5 days ago:</p>
<ul>
<li><a
href="https://github.com/quinn-rs/quinn/security/advisories/GHSA-6xvm-j4wr-6v98">https://github.com/quinn-rs/quinn/security/advisories/GHSA-6xvm-j4wr-6v98</a></li>
</ul>
<p>We coordinated with them to release this version to patch the issue.
Unfortunately the maintainers missed these issues during code review and
we did not have enough fuzzing coverage -- we regret the oversight and
have added an additional fuzzing target.</p>
<p>Organizations that want to participate in coordinated disclosure can
contact us privately to discuss terms.</p>
<h2>What's Changed</h2>
<ul>
<li>Fix over-permissive proto dependency edge by <a
href="https://github.com/Ralith"><code>@​Ralith</code></a> in <a
href="https://redirect.github.com/quinn-rs/quinn/pull/2385">quinn-rs/quinn#2385</a></li>
<li>0.11.x: avoid unwrapping VarInt decoding during parameter parsing by
<a href="https://github.com/djc"><code>@​djc</code></a> in <a
href="https://redirect.github.com/quinn-rs/quinn/pull/2559">quinn-rs/quinn#2559</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="2c315aa7f9"><code>2c315aa</code></a>
proto: bump version to 0.11.14</li>
<li><a
href="8ad47f431e"><code>8ad47f4</code></a>
Use newer rustls-pki-types PEM parser API</li>
<li><a
href="c81c0289ab"><code>c81c028</code></a>
ci: fix workflow syntax</li>
<li><a
href="0050172969"><code>0050172</code></a>
ci: pin wasm-bindgen-cli version</li>
<li><a
href="8a6f82c58d"><code>8a6f82c</code></a>
Take semver-compatible dependency updates</li>
<li><a
href="e52db4ad8d"><code>e52db4a</code></a>
Apply suggestions from clippy 1.91</li>
<li><a
href="6df7275c58"><code>6df7275</code></a>
chore: Fix <code>unnecessary_unwrap</code> clippy</li>
<li><a
href="c8eefa07e0"><code>c8eefa0</code></a>
proto: avoid unwrapping varint decoding during parameters parsing</li>
<li><a
href="9723a97775"><code>9723a97</code></a>
fuzz: add fuzzing target for parsing transport parameters</li>
<li><a
href="eaf0ef3025"><code>eaf0ef3</code></a>
Fix over-permissive proto dependency edge (<a
href="https://redirect.github.com/quinn-rs/quinn/issues/2385">#2385</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.13...quinn-proto-0.11.14">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=quinn-proto&package-manager=cargo&previous-version=0.11.13&new-version=0.11.14)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/fabro-sh/fabro/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-15 17:41:32 -04:00
arc-1e68f1[bot]
f24e410fb3
Plan: Add prevent_idle_sleep to fabro via fabro-beastie (#5)
This PR introduces `fabro-beastie`, a new cross-platform idle sleep
prevention crate (named after Beastie Boys — *No Sleep Till Brooklyn*),
and wires it into `fabro-cli` behind an opt-in `sleep_inhibitor` Cargo
feature. Long-running `fabro run` and `fabro exec` commands can be
killed by OS idle sleep, so when the feature is compiled in and
`prevent_idle_sleep = true` is set in `cli.toml`, an RAII guard keeps
the system awake for the duration of the command.

The `fabro-beastie` crate provides platform-specific backends: on macOS
it uses IOKit power assertions (`PreventUserIdleSystemSleep`), on Linux
it spawns `systemd-inhibit` (with `gnome-session-inhibit` as fallback)
and sets `PR_SET_PDEATHSIG` to prevent orphaned processes. Both fall
back to a no-op dummy backend if the platform backend is unavailable.
The public API is a single `guard(bool)` function returning an
`Option<SleepInhibitorGuard>` that releases on drop.

On the integration side, a `prevent_idle_sleep` boolean field is added
to `CliConfig` (defaulting to `false`), and `cfg`-guarded sleep guards
are placed at the entry points of both the `exec` and `run` command
paths in `fabro-cli`. Since the feature is off by default, there is zero
impact on normal builds — `fabro-beastie` is only pulled in when
explicitly enabled via `--features fabro-cli/sleep_inhibitor`.

### Fabro Details

<details>
<summary>Ran 7 stages in 20m 33s for $3.24</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.26 | 1 |
| simplify | 0s | $1.98 | 0 |
| verify | 0s | – | 0 |
| **Total** | **20m 33s** | **$3.24** | **1** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (10 nodes and 13
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan."]
    simplify          [label="Simplify", prompt="@prompts/simplify.md"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify -> verify
    verify -> exit  [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 17:41:20 -04:00
arc-1e68f1[bot]
b61786ff30
Rename metadata branch from refs/fabro/{run_id} to fabro/meta/{run_id} (#3)
* fabro(01KKS6PG9929P116A2RRKXC738): toolchain (success)

Fabro-Run: 01KKS6PG9929P116A2RRKXC738
Fabro-Completed: 2
Fabro-Checkpoint: a274eab045

⚒️ Generated with [Fabro](https://fabro.sh)

* fabro(01KKS6PG9929P116A2RRKXC738): preflight_compile (success)

Fabro-Run: 01KKS6PG9929P116A2RRKXC738
Fabro-Completed: 3
Fabro-Checkpoint: 8ce709ecdb

⚒️ Generated with [Fabro](https://fabro.sh)

* fabro(01KKS6PG9929P116A2RRKXC738): preflight_lint (success)

Fabro-Run: 01KKS6PG9929P116A2RRKXC738
Fabro-Completed: 4
Fabro-Checkpoint: 4f4c237214

⚒️ Generated with [Fabro](https://fabro.sh)

* fabro(01KKS6PG9929P116A2RRKXC738): implement (success)

Fabro-Run: 01KKS6PG9929P116A2RRKXC738
Fabro-Completed: 5
Fabro-Checkpoint: baf4d48a0f

⚒️ Generated with [Fabro](https://fabro.sh)

* fabro(01KKS6PG9929P116A2RRKXC738): simplify (success)

Fabro-Run: 01KKS6PG9929P116A2RRKXC738
Fabro-Completed: 6
Fabro-Checkpoint: ddc0967c0b

⚒️ Generated with [Fabro](https://fabro.sh)

* fabro(01KKS6PG9929P116A2RRKXC738): verify (success)

Fabro-Run: 01KKS6PG9929P116A2RRKXC738
Fabro-Completed: 7
Fabro-Checkpoint: 2249924bb2

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:40:51 -04:00
Bryan Helmkamp
a4abd68686
Use temp dir for dry-run instead of ~/.fabro/runs to avoid clutter in fabro ps -a
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
4c067f123c
Add fabro rm command to remove runs by ID with sandbox cleanup
Adds `fabro rm <RUN>...` to remove specific runs (the `docker rm` equivalent).
Refuses active runs unless `-f` is passed, writes Removing status, does
best-effort sandbox cleanup via reconnect, then deletes the run directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
50dcc04707
Simplify run status code: dedup color_if/abbreviate_home, fix Removing active state
- Replace duplicate `abbreviate_home` with existing `tilde_path`
- Include `Removing` in `is_active()` so removing runs aren't pruned
- Log warning on status.json write failure instead of silently discarding
- Extract `color_if` to cli/mod.rs, remove copies in runs.rs and rewind.rs
- Unify near-duplicate RunInfo construction in scan_runs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
a40494fd65
Replace CLI RunStatus with proper state machine (status.json)
Replace the 3-variant RunStatus enum (Concluded/Running/Unknown) with an
8-variant state machine (Submitted/Starting/Running/Paused/Removing/
Succeeded/Failed/Dead) persisted as status.json via RunStatusRecord.

Add StatusReason enum for fine-grained failure/success classification
(WorkflowError, Cancelled, SandboxInitFailed, Completed, etc.) and
validated state transitions via can_transition_to()/transition_to().

Map engine results to appropriate RunStatus+StatusReason at all write
sites: Submitted (detach), Starting+SandboxInitializing (run init),
Failed+SandboxInitFailed (scopeguard), Running (engine start), and
Succeeded/Failed with reason (engine completion).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
193a18285a
Fix truncate_goal: handle newlines, reuse for byte-unsafe inline truncation
- Take first line before truncating to prevent multi-line goals from
  breaking table layout
- Move goal field next to other manifest-sourced serialized fields
- Replace byte-slicing truncation in df_from (panics on multibyte chars)
  with the char-safe truncate_goal helper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
26f5603a13
Add GOAL column to fabro ps table output
Show each run's goal (from manifest) as the rightmost column, truncated
to 50 characters for readability. Adds a multibyte-safe truncate_goal
helper with tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
c2c351c472
Simplify status.txt implementation: reduce boilerplate and fix double read
- Add StatusInfo::simple() helper to eliminate repeated 4-field constructions
- Eliminate double read of status.txt in scan_runs() by calling read_status()
  once and branching on Unknown vs non-Unknown
- Use write_status_file() consistently in engine.rs instead of raw fs::write

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
047c2c4fef
Add status.txt for explicit run lifecycle tracking
Runs were invisible in `fabro ps` during sandbox initialization because
manifest.json isn't written until engine.run(). status.txt is written
immediately at run creation and updated at lifecycle transitions
(starting → running → concluded), replacing fragile inference logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
c8b9436678
Adopt cli-table for ANSI-aware table rendering and fix fabro ps bugs
Migrate all 7 CLI tables to cli-table, which measures column widths
correctly in the presence of ANSI escape codes, fixing misaligned
columns in `fabro ps`. Also fix DIRECTORY to show ~/relative paths
instead of just the last component, and compute elapsed duration for
running jobs instead of showing "-".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:10 -04:00
Bryan Helmkamp
2b2c37b82b
Extract short_run_id helper to deduplicate run ID truncation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:09 -04:00
Bryan Helmkamp
ceca61e952
Make fabro ps behave like docker ps
Default to showing only running processes (use -a for all), remove row
limit, display oldest-first, truncate run IDs to 12 chars, add DIRECTORY
column from host_repo_path, and drop STARTED/COST/LABELS columns.

- Add host_repo_path to Manifest and populate from RunConfig
- Add StatusFilter enum (RunningOnly/All) to filter_runs
- Replace --limit with -a/--all flag (docker-ps semantics)
- Add host_repo_path to RunInfo, extract in scan_runs
- New column layout: RUN ID | WORKFLOW | STATUS | DIRECTORY | DURATION

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:09 -04:00
Bryan Helmkamp
38cfb4286d
chore: bump snapshot to fabro-v5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:09 -04:00
Bryan Helmkamp
4e75174a6d
Disable debug info for dependencies in dev builds
[profile.dev.package."*"]
debug = false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:09 -04:00
Bryan Helmkamp
cf112ea195
chore: disable incremental compilation in sandbox image
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:27:09 -04:00
Bryan Helmkamp
0bb0e6e3cf Improve fabro ps output
This PR enriches the `fabro ps` output with colored status indicators,
duration/cost columns, relative timestamps, and pagination controls.
Status values are now color-coded (green for success, red for fail, cyan
for running, dim for unknown), the header row is bolded, and
separators/labels are dimmed.

Duration and total cost are now extracted from `conclusion.json` and
displayed as new columns. Start times are shown as human-friendly
relative strings (e.g., "2m ago", "3h ago") instead of raw RFC 3339
timestamps, with full timestamps preserved in `--json` output.

New `--limit N` (default 10) and `--all` flags cap the displayed output,
with a footer indicating how many runs are shown out of the total.

PR: https://github.com/fabro-sh/fabro/pull/2
2026-03-15 17:27:09 -04:00
Bryan Helmkamp
0a760e5d53
chore: add gh cli to image 2026-03-15 15:02:58 -04:00
Bryan Helmkamp
be88061479
Fix --goal-file not expanding ~ to home directory
Move expand_tilde from fabro-config to fabro-util::path so it can be
shared without circular dependencies, and apply it to the goal file
path in resolve_cli_goal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:57:38 -04:00
Bryan Helmkamp
7249fbaf54
Deduplicate CheckpointSaved hook, use idiomatic bsha.clone()
Move the identical CheckpointSaved hook block from both git and non-git
checkpoint branches to a single block after the if/else. Replace
bsha.to_string() with bsha.clone() for &String → String conversion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:50:42 -04:00
Bryan Helmkamp
a8f6dbb7fd
Fold CheckpointSaved into CheckpointCompleted
Remove the separate CheckpointSaved event — CheckpointCompleted now fires in
both git and non-git paths. git_commit_sha is Optional (None when git is
disabled or for start nodes). The CheckpointSaved hook event is preserved
unchanged for backward compat with user hook configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:43:09 -04:00
Bryan Helmkamp
36c1bd3794
Add granular git events, rename GitCheckpoint → CheckpointCompleted
Rename GitCheckpoint/GitCheckpointFailed to CheckpointCompleted/CheckpointFailed
to separate checkpoint lifecycle from git operations. Add 7 new granular git
events: GitCommit, GitPush, GitBranch, GitWorktreeAdd, GitWorktreeRemove,
GitFetch, GitReset. Emit at all relevant call sites in engine.rs and parallel.rs.
Update push helpers to return bool for GitPush success tracking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:35:17 -04:00
Bryan Helmkamp
510272fa69
Add RetroStarted, RetroCompleted, RetroFailed events
Replace fake StageStarted/StageCompleted events with dedicated retro
variants so consumers can distinguish retro activity from normal stages.
The resume path now emits retro events instead of silently skipping them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:21:43 -04:00
Bryan Helmkamp
ecec4c507c
Fix empty-status backward-compat bug, add From<&StageUsage> for Usage
- Fix: empty-string status from old logs now defaults to "success"
  instead of rendering as red/error
- Add From<&StageUsage> for fabro_llm::Usage to centralize conversion
- Simplify usage aggregation: replace collect+reduce+unwrap with
  direct .reduce() on the iterator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:14:17 -04:00
Bryan Helmkamp
70beb76c41
Add status + usage to WorkflowRunCompleted, enrich fabro logs --pretty summary
Adds aggregate `status` and `usage` fields to the WorkflowRunCompleted
event so `fabro logs --pretty` can render a complete end-of-run summary
(status, tokens, cache, reasoning) without scanning all StageCompleted
events. Also adds pretty handlers for PullRequestCreated/Failed events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:04:02 -04:00
Bryan Helmkamp
d97c1f4a09
Strip "Plan:" prefix from PR titles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:47:20 -04:00
Bryan Helmkamp
ea6aa93c22
Fix race condition between fabro run --detach and fabro logs -f
Write id.txt and touch empty progress.jsonl in detach_run() before
spawning the child process so that `fabro logs -f ULID` can resolve
the run and tail the file immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:37:48 -04:00
Bryan Helmkamp
0bd012acb6
Add -p short alias for --pretty in fabro logs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 13:29:34 -04:00
Bryan Helmkamp
891a6db29b
commas 2026-03-15 12:31:08 -04:00
Bryan Helmkamp
d25fc56b10
Fix dry-run bug in API server and deduplicate test helpers
The API server set RunConfig.dry_run but never called
engine.set_dry_run(), so command/script nodes executed for real
during API-served dry runs. Add the missing call.

Also extract EngineServices::test_default() to replace 11 identical
make_services() bodies and 8 inline struct constructions across
handler test modules (-254/+57 lines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:28:54 -04:00
Bryan Helmkamp
ef21be5a6d
Fix --dry-run pushing branches to remote
--dry-run was not suppressing real git push operations in three places:
pre-run branch sync, post-run auto-PR creation, and engine checkpoint
pushes. Guard all three with dry_run checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:12:35 -04:00
Bryan Helmkamp
938f3a7c74
docs 2026-03-15 12:11:54 -04:00
Bryan Helmkamp
bc3c5be59e
Fix --dry-run executing command/script nodes instead of simulating them
Command nodes were running for real during dry-run mode because the
dry_run flag only affected LLM-backed handlers. Propagate dry_run
through EngineServices so CommandHandler can skip execution and return
a simulated success.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:07:41 -04:00
Bryan Helmkamp
0c80d1f616
Add progress spinner to run --preflight
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 11:44:22 -04:00
Bryan Helmkamp
514686787e
cleanup workflows 2026-03-15 11:04:04 -04:00
Bryan Helmkamp
5428702bc5
files-internal -> docs-internal 2026-03-15 10:58:01 -04:00
Bryan Helmkamp
bc0cb34d5e
rm prompts 2026-03-15 10:57:26 -04:00
Bryan Helmkamp
70d58fc7ba
Add fabro inspect to show detailed JSON data for a workflow run
Outputs run_id, run_dir, status, manifest, conclusion, checkpoint, and
sandbox as a JSON array (null for missing files). Resolves runs by ID
prefix or workflow name, matching existing `fabro logs` semantics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:19:21 -04:00
Bryan Helmkamp
8dd860ea81
Handle credential-embedded GitHub URLs in parse_github_owner_repo
URLs like https://x-access-token:TOKEN@github.com/owner/repo.git are
used by Daytona sandboxes. Strip the credentials before matching the
github.com prefix so pr_create and other callers work in those envs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:47:55 -04:00
Bryan Helmkamp
d5236dcda2
Fix doctor trycmd test: move env vars to [env.add] section
trycmd's Env struct requires env vars under [env.add], not directly
under [env]. Vars placed directly under [env] are silently ignored by
serde, so the subprocess ran with a fully cleared env. On CI this caused
dirs::home_dir() to fall back to passwd, loading the real cli.toml
(with app_id) but without GITHUB_APP_PRIVATE_KEY → partial config error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:28:44 -04:00
Bryan Helmkamp
1e7fd59c69
Add fabro run --detach to fork workflows as background processes
Pre-generates a ULID in the parent, passes it to the child via hidden
`--run-id` arg, prints the ULID to stdout, and exits immediately.
Child stdout/stderr go to `{run_dir}/detach.log`. Uses `setsid()` on
unix to detach from the controlling terminal. Existing `fabro ps` and
`fabro logs` work with no changes since `run.pid` and `conclusion.json`
are written by the child as usual.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:18:50 -04:00
Bryan Helmkamp
84932eb3a8
Fix doctor dry-run trycmd test on CI by setting HOME to nonexistent path
`inherit = false` clears HOME but `dirs::home_dir()` falls back to the
passwd database, picking up the runner's ~/.fabro/cli.toml. The loaded
app_id without GITHUB_APP_PRIVATE_KEY triggers a partial-config error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:06:35 -04:00
Bryan Helmkamp
312bef9b87
Add routing context to EdgeSelected event: reason, status, hints
Emit reason (condition/preferred_label/suggested_next/unconditional/
jump/fallback), stage_status, preferred_label, suggested_next_ids,
and is_jump so logs explain why an edge was chosen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:47:28 -04:00
Bryan Helmkamp
008fdb03dc
Add inherit = false to all trycmd test envs to prevent CI flakiness
The doctor dry-run test was failing in CI because it inherited the host
environment. With no LLM API keys set, the doctor reported errors and
exited non-zero. Adding `inherit = false` to all 18 .toml test files
ensures deterministic behavior regardless of the host environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:43:23 -04:00
Bryan Helmkamp
81adcefd57
Tweak fabro logs --pretty styling: increase indent, dim model brackets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:32:03 -04:00
Bryan Helmkamp
6b03562749
Fix fabro logs --pretty wrapping past terminal edge on assistant messages
Markdown was rendered at full terminal width then indented, pushing lines
past the right edge. Now wraps to terminal_width minus indent first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:30:03 -04:00
Bryan Helmkamp
94652907b0
Store workflow slug in manifest so resolve_run can match by directory name
When running `fabro run smoke`, the slug "smoke" was used to locate the
workflow but never persisted. If the DOT graph name diverged from the
directory name (e.g. workflows/foo/ contains digraph Bar), resolve_run
couldn't find the run by slug. Now the slug is extracted from the
workflow path, stored in the manifest, and matched in resolve_run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:14:06 -04:00
Bryan Helmkamp
f58e02c8df
Add instability warning to run directory docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:11:24 -04:00
Bryan Helmkamp
5a1109f66a
Make resolve_run match workflow slugs and display names
Workflow names in manifests are PascalCase (e.g. "LegacyTool") but
users expect to type the slug (e.g. "legacy-tool"). resolve_run now
compares case-insensitively and with hyphens/underscores stripped,
so both forms work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:00:14 -04:00
Bryan Helmkamp
5b82cea0e1
Add fabro logs command to view workflow run event logs
Supports raw JSONL output (pipeable to jq) and --pretty mode with
colored, formatted output showing stages, tool calls, and assistant
messages. Includes --follow, --since, and --tail filtering options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:55:58 -04:00
Bryan Helmkamp
6cc5512ead
Add resolve_run() that accepts run ID prefix or workflow name
Subcommands like cp, diff, preview, ssh, and pr previously only
accepted run ID prefixes. The new resolve_run() tries run ID prefix
first, then falls back to workflow name (most recent run), making
these commands more ergonomic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:42:03 -04:00
Bryan Helmkamp
68b088d322
Reorganize docs nav: merge Server Mode into Deployment, move Comparison and Dark Factory
- Merge core-concepts/server-mode into administration/deploy-server
- Move Comparison from Getting Started to Reference
- Move Dark Factory from Getting Started to Core Concepts
- Update all internal links to server-mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:39:08 -04:00
Bryan Helmkamp
25ec842a7f
Fix detect_binary test to work on Ubuntu (dash) sandboxes
`sh --version` exits non-zero on dash (Ubuntu default), so the test
only passed on macOS where sh is bash. Use `git` instead which
reliably supports --version on all platforms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:31:27 -04:00
Bryan Helmkamp
c5df9176b3
Extend smoke workflow to run linting and tests, add python3 to sandbox
Add python3 to the Daytona Dockerfile so MCP integration tests can run
their test server. Update the smoke workflow to verify fmt, clippy,
cargo test, typecheck, and bun test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:28:38 -04:00
Bryan Helmkamp
6db816e8b3
Fix empty run_id on sandbox events in progress.jsonl
The JSONL listener's run_id was initialized to "" and only populated
when WorkflowRunStarted fired, but sandbox events emit before that.
Seed it with the already-generated ULID so all events carry the run_id.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:00:40 -04:00
Bryan Helmkamp
e6f59cf426
Add Daytona sandbox defaults to fabro.toml and smoke workflow
Set Daytona as the project-level default sandbox so workflows that don't
specify their own sandbox config run on Daytona automatically. Add a
smoke workflow that verifies the sandbox toolchain (git, rustc, bun).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:53:53 -04:00
Bryan Helmkamp
12e71e3464
Add fabro workflow create <name> subcommand to scaffold new workflows
Writes a starter workflow.fabro (DOT graph) and workflow.toml into the
project's workflows directory. Supports --goal flag and derives the
digraph name from the workflow name using PascalCase conversion. Also
defaults the `graph` field in workflow.toml to "workflow.fabro" so it
can be omitted from generated configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:32:00 -04:00
Bryan Helmkamp
79c602cc7f
Extract RunDefaults::merge_overlay to replace inline field-by-field merge
The inline merge in run_command() duplicated the structure of
apply_defaults() with shallower (inconsistent) semantics. This extracts
a proper merge_overlay method that deep-merges compound fields (vars,
hooks, mcp_servers, sandbox sub-fields) consistently with apply_defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:23:23 -04:00
Bryan Helmkamp
76fde62234
Fix logo SVG viewBox clipping the right edge of the O
The scale(1.25) transform pushed the O's rightmost extent to ~x=1488,
past the old viewBox width of 1455.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:11:36 -04:00
Bryan Helmkamp
2220b9194d
Extend fabro.toml with project-level run defaults
Add project-level run defaults to fabro.toml so sandbox, LLM, hooks,
MCP servers, and other settings can be shared across workflows instead
of duplicated in each workflow.toml. Precedence: workflow.toml >
fabro.toml > cli.toml/server.toml.

- Rename `directory` → `work_dir` with backwards-compat serde alias
- Add `hooks` and `mcp_servers` to `RunDefaults` with merge logic
- Extend `ProjectConfig` with all run-defaults fields + `into_run_defaults()`
- Remove duplicate `McpServerEntry` from fabro-config (use run_config's)
- Move `hook_config` from ServerConfig into `run_defaults.hooks`
- Wire project config merge and hooks/mcp fallbacks in run_command()
- Update OpenAPI spec and regenerate TypeScript client

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:11:16 -04:00
Bryan Helmkamp
36ed218ce0
Update docs for rewind, workflow list, daytona, and validation
Fix changelog rewind syntax to use positional args instead of flags.
Clarify Daytona snapshot note to distinguish configured-but-missing vs
unconfigured cases. Add rewind/workflow-list CLI reference sections,
checkpoints rewind guide, thread_id validation rule, and human gate
behavior details.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:51:57 -04:00
Bryan Helmkamp
72865245a4
Add ~/.fabro/ fallback for @ file references
When an @file reference can't be resolved against the workflow's
directory, fall back to ~/.fabro/ so users can share prompt files
across workflows without duplication. The workflow directory keeps
higher precedence so project-specific overrides still win.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:44:16 -04:00
Bryan Helmkamp
69e7f415d8
Add changelog entries for March 13-14 and update March 12
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:24:04 -04:00
Bryan Helmkamp
4ee215cada
Rename arc/ to fabro/ in git branch prefixes and workflow paths
Complete the rebrand by replacing hardcoded "arc/run/" branch prefixes
with a RUN_BRANCH_PREFIX constant ("fabro/run/") and updating
fabro init to create workflows under fabro/workflows/ instead of
arc/workflows/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:20:07 -04:00
Bryan Helmkamp
39191be1a5
Use daytona-medium snapshot as default Daytona sandbox
The bare ubuntu:22.04 image lacks git and other tooling, causing git
checkpoints to fail. Switch to the daytona-medium snapshot which has
standard dev tools pre-installed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:10:50 -04:00
Bryan Helmkamp
29bd80824b
Improve workflow diagram with LR layout and docs styling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:10:35 -04:00
Bryan Helmkamp
0347d5bf0d
Add workflow diagram and update doc URLs in README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:08:10 -04:00
Bryan Helmkamp
52475dde28
Add Dark Factory docs page and rebrand as "dark software factory"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:05:35 -04:00
Bryan Helmkamp
178677a9a9
Backfill missing git_commit_sha from run branch commit messages
The engine writes git_commit_sha to on-disk checkpoint.json but not to the
metadata branch. When the metadata checkpoint lacks this field, walk the
run branch and match commits by message pattern to fill in the SHAs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 11:52:33 -04:00
Bryan Helmkamp
9e3d2d0688
Add fabro rewind command to rewind workflow runs to earlier checkpoints
Enables rewinding both the metadata branch and run branch refs to a
target checkpoint, allowing resume from an earlier point with
`fabro run --run-branch`. Supports targeting by node name, node@visit,
or @ordinal, with parallel interior snap-back and optional remote push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 11:38:17 -04:00
Bryan Helmkamp
d207c4b1f7
Improve workflow list output with grouped sections, descriptions, and tests
Show workflows grouped by User/Project with directory paths in headings,
aligned NAME/DESCRIPTION columns, truncated goal snippets, and (none)
for empty sections. Add tests for list_workflows_detailed, read_workflow_goal,
and truncate_str.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 11:19:08 -04:00
Bryan Helmkamp
2bc211f267
Add fabro workflow list command
Adds a new `workflow` subcommand group with a `list` command that
discovers available workflows via `fabro.toml` and prints their names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 09:58:03 -04:00
Bryan Helmkamp
f6da846523
Add user-level workflow lookup in ~/.fabro/workflows/
`fabro run NAME` now checks ~/.fabro/workflows/ as a fallback when the
workflow isn't found in the project directory, letting users have
personal workflows available across all projects. Project workflows
take precedence over user workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 23:16:18 -04:00
Bryan Helmkamp
e7017eb127
Fix remaining .dot test references in integration.rs
Update two test TOML graph references from test.dot to test.fabro.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:39:58 -04:00
Bryan Helmkamp
35bd79a2ed
Update docs, frontend, marketing, and skills for .fabro extension
Update 47 MDX doc pages, OpenAPI spec, SVG diagram, language
grammar, frontend demo data, marketing page, skills, and README
to use .fabro extension. Add "fabro" to fileTypes in language
grammars. Document stack.child_workflow alongside stack.child_dotfile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:38:25 -04:00
Bryan Helmkamp
8cb6c4f26f
Rename .dot files to .fabro and update all references
Rename 79 workflow files from .dot to .fabro extension across
fabro/workflows/, test/, test/docs/, and files-internal/demo/.
Update TOML configs, Rust production code, test code, and shell
scripts. Backward compat tests in test/attractor/ are unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:27:19 -04:00
Bryan Helmkamp
c4dc2f09c8
Add graph.fabro as primary filename with graph.dot fallback
Write graph.fabro in run dirs and metadata branches. Read with
graph.dot fallback for backward compatibility with existing runs.
Add stack.child_workflow attribute with stack.child_dotfile fallback.
No files renamed yet — fallback paths handle everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:16:10 -04:00
Bryan Helmkamp
0ee5b079c5
Add thread_id_requires_fidelity_full lint rule
Warn when thread_id is set without fidelity=full, since session reuse
only works with full fidelity. Checks node-level, edge-level, and
graph-level default_thread attributes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 20:58:08 -04:00
Bryan Helmkamp
714c6f5eaf
Regenerate TypeScript API client from updated OpenAPI spec
Picks up Arc-to-Fabro rename and new ssh-configuration model.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 20:43:25 -04:00
Bryan Helmkamp
20bfbb939f
Rename Arc to Fabro in user-facing strings, comments, and tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 20:43:21 -04:00
Bryan Helmkamp
707c5fbfe3
Fix relative path in fabro-api-client generate script
The path was two levels up (../../) but needs three (../../../) since
the package lives at lib/packages/fabro-api-client.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 20:42:45 -04:00
Bryan Helmkamp
ff2e2fbf42
add comparison 2026-03-13 18:52:20 -04:00
523 changed files with 9532 additions and 4190 deletions

View file

@ -1,46 +0,0 @@
# Simplify: Code Review and Cleanup
Review all changed files for reuse, quality, and efficiency. Fix any issues found.
## Phase 1: Identify Changes
Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
## Phase 2: Launch Three Review Agents in Parallel
Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
### Agent 1: Code Reuse Review
For each change:
1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.
3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.
### Agent 2: Code Quality Review
Review the same changes for hacky patterns:
1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones
3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction
4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
### Agent 3: Efficiency Review
Review the same changes for efficiency:
1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
2. Missed concurrency: independent operations run sequentially when they could run in parallel
3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths
4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
5. Memory: unbounded data structures, missing cleanup, event listener leaks
6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one
## Phase 3: Fix Issues
Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.
When done, briefly summarize what was fixed (or confirm the code was already clean).

View file

@ -1 +1 @@
8bd90191bc4deac93a95e065f6584b64e48ca0e0
8b948a2d6852023eb41eed3a99e9b402da3f5fbe

View file

@ -1 +1 @@
542adbf1e1b0a9d6b3149ae25a097addf3bfb61a
ec0a612ea531fcf53383afb15ad23561a7bbe6ae

View file

@ -82,5 +82,5 @@ When interpolating values into shell command strings (in `fabro-exe` and `fabro-
## Testing workflows
- `fabro run <name>` — run a workflow by name (resolves `arc/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

38
Cargo.lock generated
View file

@ -516,6 +516,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "cli-table"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14da8d951cef7cc4f13ccc9b744d736963d57863c7e6fc33c070ea274546082c"
dependencies = [
"termcolor",
"unicode-width 0.2.2",
]
[[package]]
name = "cmake"
version = "0.1.57"
@ -1218,6 +1228,15 @@ dependencies = [
"x509-parser",
]
[[package]]
name = "fabro-beastie"
version = "0.4.0"
dependencies = [
"core-foundation 0.9.4",
"libc",
"tracing",
]
[[package]]
name = "fabro-cli"
version = "0.4.0"
@ -1235,6 +1254,7 @@ dependencies = [
"dotenvy",
"fabro-agent",
"fabro-api",
"fabro-beastie",
"fabro-config",
"fabro-github",
"fabro-llm",
@ -1247,6 +1267,7 @@ dependencies = [
"indicatif",
"insta",
"jsonwebtoken",
"libc",
"open",
"predicates",
"rand 0.8.5",
@ -1264,6 +1285,7 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"trycmd",
"ulid",
"x509-parser",
]
@ -1275,6 +1297,7 @@ dependencies = [
"dirs",
"fabro-agent",
"fabro-mcp",
"fabro-util",
"fabro-workflows",
"serde",
"tempfile",
@ -1378,6 +1401,7 @@ dependencies = [
"base64",
"bytes",
"clap",
"cli-table",
"dialoguer",
"dotenvy",
"fabro-util",
@ -1541,6 +1565,7 @@ dependencies = [
"base64",
"chrono",
"clap",
"cli-table",
"console 0.15.11",
"daytona-api-client",
"daytona-sdk",
@ -3501,9 +3526,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.13"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.4",
@ -4873,6 +4898,15 @@ dependencies = [
"utf-8",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "termimad"
version = "0.34.1"

View file

@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
version = "0.4.0"
version = "0.5.0"
license = "MIT"
[workspace.dependencies]
@ -30,6 +30,7 @@ jsonschema = "0.42"
chrono = { version = "0.4", features = ["clock"] }
bollard = "0.18"
tar = "0.4"
cli-table = { version = "0.5", default-features = false }
console = "0.15"
dialoguer = "0.12"
git2 = "0.20"
@ -63,6 +64,9 @@ daytona-api-client = { git = "https://github.com/brynary/daytona-sdk-rust", rev
lto = "thin"
strip = true
[profile.dev.package."*"]
debug = false # Disable debug info for all dependencies
# regex is extremely slow in debug builds (~10s to compile gitleaks patterns)
[profile.dev.package.regex]
opt-level = 2

View file

@ -1,13 +1,13 @@
<div align="left" id="top">
<a href="https://fabro.dev"><img alt="Fabro" src="docs/logo/dark.svg" height="75"></a>
<a href="https://docs.fabro.sh"><img alt="Fabro" src="docs/logo/dark.svg" height="75"></a>
</div>
## The open source software factory for expert engineers
## The open source, dark software factory for expert engineers
AI coding agents are powerful but unpredictable. You either babysit every step or review a 50-file diff you don't trust. Fabro gives you a middle path: define the process as a graph, let agents execute it, and intervene only where it matters. [Why Fabro?](https://fabro.dev/getting-started/why-arc)
AI coding agents are powerful but unpredictable. You either babysit every step or review a 50-file diff you don't trust. Fabro gives you a middle path: define the process as a graph, let agents execute it, and intervene only where it matters. [Why Fabro?](https://docs.fabro.sh/getting-started/why-arc)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE.md)
[![docs](https://img.shields.io/badge/docs-fabro.dev-357F9E)](https://fabro.dev)
[![docs](https://img.shields.io/badge/docs-fabro.dev-357F9E)](https://docs.fabro.sh)
```bash
curl -fsSL https://fabro.sh/install.sh | bash
@ -53,6 +53,8 @@ curl -fsSL https://fabro.sh/install.sh | bash
A plan-approve-implement workflow where a human reviews the plan before the agent writes code:
<img src="docs/images/plan-implement-readme.svg" alt="Plan-Implement workflow graph showing Start → Plan → Approve Plan → Implement → Simplify → Exit with a Revise loop" />
```dot
digraph PlanImplement {
graph [
@ -78,19 +80,19 @@ digraph PlanImplement {
}
```
Agents run as multi-turn LLM sessions with tool access. Human gates (`hexagon`) pause for approval. The stylesheet routes planning to a cheap model and coding to a frontier model. See the [DOT language reference](https://fabro.dev/reference/dot-language) for the full syntax.
Agents run as multi-turn LLM sessions with tool access. Human gates (`hexagon`) pause for approval. The stylesheet routes planning to a cheap model and coding to a frontier model. See the [DOT language reference](https://docs.fabro.sh/reference/dot-language) for the full syntax.
---
## 📖 Documentation
Fabro ships with [comprehensive documentation](https://fabro.dev) covering every feature in depth:
Fabro ships with [comprehensive documentation](https://docs.fabro.sh) covering every feature in depth:
- [**Getting Started**](https://fabro.dev/getting-started/introduction) -- Installation, first workflow, and why Fabro exists
- [**Defining Workflows**](https://fabro.dev/workflows/stages-and-nodes) -- Node types, transitions, variables, stylesheets, and human gates
- [**Executing Workflows**](https://fabro.dev/execution/run-configuration) -- Run configuration, sandboxes, checkpoints, retros, and failure handling
- [**Tutorials**](https://fabro.dev/tutorials/hello-world) -- Step-by-step guides from hello world to parallel multi-model ensembles
- [**API Reference**](https://fabro.dev/api-reference/overview) -- Full OpenAPI spec with authentication, SSE events, and client SDKs
- [**Getting Started**](https://docs.fabro.sh/getting-started/introduction) -- Installation, first workflow, and why Fabro exists
- [**Defining Workflows**](https://docs.fabro.sh/workflows/stages-and-nodes) -- Node types, transitions, variables, stylesheets, and human gates
- [**Executing Workflows**](https://docs.fabro.sh/execution/run-configuration) -- Run configuration, sandboxes, checkpoints, retros, and failure handling
- [**Tutorials**](https://docs.fabro.sh/tutorials/hello-world) -- Step-by-step guides from hello world to parallel multi-model ensembles
- [**API Reference**](https://docs.fabro.sh/api-reference/overview) -- Full OpenAPI spec with authentication, SSE events, and client SDKs
---

View file

@ -3,7 +3,7 @@ import type { LanguageRegistration } from "@pierre/diffs";
export const dotLanguage: LanguageRegistration = {
name: "dot",
scopeName: "source.dot",
fileTypes: ["dot", "DOT", "gv"],
fileTypes: ["fabro", "dot", "DOT", "gv"],
firstLineMatch: "digraph.*",
patterns: [
{

View file

@ -19,12 +19,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
fix_build: {
name: "Fix Build",
slug: "fix_build",
filename: "fix_build.dot",
filename: "fix_build.fabro",
description: "Automatically diagnoses and fixes CI build failures by analyzing error logs, identifying root causes, and applying targeted code changes.",
config: {
version: 1,
goal: "Diagnose and fix CI build failures",
graph: "fix_build.dot",
graph: "fix_build.fabro",
llm: { model: "claude-sonnet" },
vars: { repo_url: "https://github.com/org/service", branch: "main" },
sandbox: {
@ -60,12 +60,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
implement: {
name: "Implement Feature",
slug: "implement",
filename: "implement.dot",
filename: "implement.fabro",
description: "Generates production-ready code from a technical blueprint, including tests, documentation, and a pull request ready for review.",
config: {
version: 1,
goal: "Implement feature from technical blueprint",
graph: "implement.dot",
graph: "implement.fabro",
llm: { model: "claude-sonnet" },
vars: { spec_path: "specs/feature.md", test_framework: "vitest" },
setup: { commands: ["bun install", "bun run typecheck"], timeout_ms: 120000 },
@ -116,12 +116,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
sync_drift: {
name: "Sync Drift",
slug: "sync_drift",
filename: "sync_drift.dot",
filename: "sync_drift.fabro",
description: "Detects configuration and code drift between environments, then generates reconciliation patches to bring everything back in sync.",
config: {
version: 1,
goal: "Detect and reconcile configuration drift across environments",
graph: "sync_drift.dot",
graph: "sync_drift.fabro",
llm: { model: "claude-sonnet" },
vars: { source_env: "production", target_env: "staging", drift_threshold: "warn" },
sandbox: {
@ -161,12 +161,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
expand: {
name: "Expand Product",
slug: "expand",
filename: "expand.dot",
filename: "expand.fabro",
description: "Evolves the product by analyzing usage patterns and specifications to propose and implement incremental improvements.",
config: {
version: 1,
goal: "Propose and implement incremental product improvements",
graph: "expand.dot",
graph: "expand.fabro",
llm: { model: "claude-sonnet" },
vars: { analytics_window: "30d", min_confidence: "0.8" },
sandbox: {

View file

@ -232,7 +232,7 @@ const cssExample = `<span class="text-ice-300">/* Fast model for planning */</sp
<span class="h-3 w-3 rounded-full bg-coral/70"></span>
<span class="h-3 w-3 rounded-full bg-amber/70"></span>
<span class="h-3 w-3 rounded-full bg-mint/70"></span>
<span class="ml-3 text-xs text-ice-300/60 font-mono">workflow.dot</span>
<span class="ml-3 text-xs text-ice-300/60 font-mono">workflow.fabro</span>
</div>
<pre class="overflow-x-auto text-sm leading-relaxed"><code class="font-mono" set:html={dotExample} /></pre>
</div>
@ -440,7 +440,7 @@ const cssExample = `<span class="text-ice-300">/* Fast model for planning */</sp
Composable steps. Auditable history. Repeatable results. IaC for coding.
</p>
<div class="reveal reveal-d2 mt-10 inline-block rounded-lg border border-navy-800 bg-navy-900/50 px-6 py-3">
<code class="text-sm font-mono text-teal-300">$ fabro run workflow.dot</code>
<code class="text-sm font-mono text-teal-300">$ fabro run workflow.fabro</code>
</div>
</div>
</section>

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 608 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View file

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,6 +1,6 @@
version = 1
goal = "Search the web for a famous landmark, then generate an image of it"
graph = "14-search-imagegen.dot"
graph = "14-search-imagegen.fabro"
[sandbox]
provider = "daytona"
@ -13,7 +13,7 @@ name = "imagegen-tools-v3"
cpu = 4
memory = 8
disk = 10
dockerfile = { path = "../../arc/workflows/imagegen/Dockerfile.imagegen" }
dockerfile = { path = "../../fabro/workflows/imagegen/Dockerfile.imagegen" }
[assets]
include = ["output/**"]

View file

@ -184,9 +184,15 @@ WorkflowRunEvent::MyNewEvent { node_id, duration_ms, .. } => {
| Event | JSONL fields |
|---|---|
| `CheckpointSaved` | `node_id`, `node_label` |
| `GitCheckpoint` | `run_id`, `node_id`, `node_label`, `status`, `git_commit_sha` |
| `GitCheckpointFailed` | `node_id`, `node_label`, `error` |
| `CheckpointCompleted` | `node_id`, `node_label`, `status`, `git_commit_sha` (optional) |
| `CheckpointFailed` | `node_id`, `node_label`, `error` |
| `GitCommit` | `node_id` (optional), `node_label` (optional), `sha` |
| `GitPush` | `branch`, `success` |
| `GitBranch` | `branch`, `sha` |
| `GitWorktreeAdd` | `path`, `branch` |
| `GitWorktreeRemove` | `path` |
| `GitFetch` | `branch`, `success` |
| `GitReset` | `sha` |
### Human interaction
@ -296,5 +302,5 @@ Error information is stored as plain strings. The `error` field contains the hum
| `cli/run.rs` non-verbose listener | `name`, `duration_ms`, `status`, `usage` from `StageCompleted/Failed` | CLI progress output |
| `cli/mod.rs` `format_event_summary()` | All events | `-v` verbose output |
| `cli/run.rs` cost accumulator | `usage` from `StageCompleted` | Total cost tracking |
| `cli/run.rs` git SHA tracker | `git_commit_sha` from `GitCheckpoint` | Final SHA for `conclusion.json` |
| `cli/run.rs` git SHA tracker | `git_commit_sha` from `CheckpointCompleted` | Final SHA for `conclusion.json` |
| External tooling | `progress.jsonl` | Live monitoring, dashboards |

View file

@ -1,8 +1,145 @@
---
title: "Server Deployment"
description: "Deploy the Fabro server to production"
title: "Server Mode"
description: "Run Fabro as an API server with a web UI, concurrent runs, and team access"
---
<Warning>
This guide is coming soon. Deployment guides are currently in development.
Server mode is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
Fabro has two modes: **standalone** and **server**. Standalone mode (`fabro run`) executes a single workflow synchronously in your terminal. Server mode (`fabro serve`) starts an HTTP API that queues runs, streams events, and serves a web UI — so you can close your laptop and let workflows run.
Both modes use the same workflow engine, the same DOT files, and the same sandbox providers. The difference is how you interact with them.
## Standalone vs. server mode
| | Standalone | Server |
|---|---|---|
| **Command** | `fabro run workflow.fabro` | `fabro serve` |
| **Best for** | Local development, one-off runs, CI/CD | Production, team use, running at scale |
| **Execution** | Synchronous, one run per process | Asynchronous, queued with configurable concurrency |
| **Human-in-the-loop** | Terminal prompts | Web UI or HTTP endpoints |
| **Events** | Printed to stderr | Streamed via SSE |
| **Persistence** | Checkpoint files only | SQLite database + checkpoint files |
| **Web UI** | Not available | Full React interface |
| **Authentication** | None | JWT and/or mTLS |
## Starting the server
```bash
fabro serve
```
This starts the API on `127.0.0.1:3000` by default. To also run the web UI:
```bash
fabro serve # API on port 3000
cd apps/fabro-web && bun run dev # Web UI on port 5173
```
Common flags:
| Flag | Default | Description |
|---|---|---|
| `--port` | `3000` | Port to listen on |
| `--host` | `127.0.0.1` | Host address to bind to |
| `--model` | — | Override default LLM model |
| `--sandbox` | — | Override default sandbox provider |
| `--max-concurrent-runs` | `5` | Maximum concurrent run executions |
See [Server Configuration](/administration/server-configuration) for the full `server.toml` reference.
## Submitting runs
In server mode, workflows are submitted via the REST API and executed in the background:
```bash
curl -X POST http://localhost:3000/runs \
-H "Content-Type: application/json" \
-d '{"workflow": "implement-feature", "goal": "Add user authentication"}'
```
The server returns immediately with a run ID. A background scheduler promotes queued runs to `Running` in FIFO order, up to the concurrency limit.
## Run lifecycle
1. **Submit** — `POST /runs` creates the run with status `Queued`.
2. **Schedule** — The scheduler picks up queued runs up to `max_concurrent_runs`.
3. **Execute** — The engine walks the graph, streaming events to all subscribers.
4. **Complete** — The run transitions to `Completed`, `Failed`, or `Cancelled`.
## Web UI
The web UI connects to the API server and provides:
- **Runs board** — Monitor all active runs organized by status
- **Run detail** — Real-time stage progress, event stream, diffs, and usage stats
- **Start new run** — Submit workflows from the browser
- **Human-in-the-loop** — Answer agent questions through the web interface
- **Workflows** — Browse available workflows, view their graphs, and see run history
- **Insights** — SQL-based analysis across runs via DuckDB
<Frame caption="The Runs board shows all active runs organized by status.">
<img src="/images/web/runs-board.png" alt="Fabro web UI Runs board with Working, Pending, Verify, and Merge columns" />
</Frame>
<Frame caption="The run detail view shows stage progress alongside the workflow graph.">
<img src="/images/web/run-overview.png" alt="Fabro web UI run detail showing stages and workflow graph" />
</Frame>
## Event streaming
The API streams run events via [Server-Sent Events (SSE)](/api-reference/runs#get-events). Every stage start, LLM call, tool invocation, and edge selection is emitted as a structured JSON event. Any HTTP client that supports SSE can subscribe — the web UI is just one consumer.
## Human-in-the-loop
In server mode, human-in-the-loop questions are served over HTTP instead of terminal prompts. The engine blocks the current stage until an answer is submitted, then continues execution. See the [Human-in-the-Loop API reference](/api-reference/human-in-the-loop) for the polling and answer endpoints.
## Authentication
Server mode supports two authentication strategies, configurable in `server.toml`:
- **JWT** — EdDSA-signed bearer tokens. Used by the web UI. See [API Overview](/api-reference/overview#jwt-bearer-token) for token format.
- **mTLS** — Mutual TLS with client certificates. Used for service-to-service communication. See [API Overview](/api-reference/overview#mtls-mutual-tls) for setup.
Both strategies can be enabled simultaneously — the first successful match wins.
## Demo mode
Send the `X-Fabro-Demo: 1` header on any API request to get static mock data with authentication disabled. The web UI enables this automatically with the `FABRO_DEMO=1` environment variable. This lets you explore the UI without API keys or real workflow execution. See [Demo Mode](/api-reference/demo-mode) for details.
## Pointing the CLI at a server
The CLI can delegate commands to a running Fabro server instead of executing locally. Set `mode = "server"` in `~/.fabro/cli.toml`:
```toml title="cli.toml"
mode = "server"
[server]
base_url = "https://fabro.example.com:3000"
```
Or use the `--mode` flag:
```bash
fabro --mode server --server-url https://fabro.example.com:3000 models list
```
This applies to commands like `fabro models list`, `fabro llm chat`, and `fabro exec`. See [CLI Configuration](/reference/cli-configuration#mode) for the full options including mTLS setup.
## Next steps
<Columns cols={2}>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
Full server.toml reference — authentication, TLS, run defaults, and more.
</Card>
<Card title="Deploy to Railway" icon="train" href="/administration/deploy-railway">
Step-by-step guide for deploying Fabro on Railway.
</Card>
<Card title="API Reference" icon="code" href="/api-reference/overview">
REST API for submitting runs, streaming events, and managing resources.
</Card>
<Card title="How Fabro Works" icon="lightbulb" href="/core-concepts/how-fabro-works">
The workflow engine that powers both modes.
</Card>
</Columns>

View file

@ -154,7 +154,7 @@ A workflow that uses Playwright MCP to automate a browser inside a Daytona sandb
```toml title="run.toml"
version = 1
goal = "Test the login page"
graph = "workflow.dot"
graph = "workflow.fabro"
[sandbox]
provider = "daytona"

View file

@ -182,9 +182,9 @@ This keeps preambles concise while still giving agents a path to read the full o
Artifact data is persisted on the Git [metadata branch](/execution/checkpoints#metadata-branch) alongside checkpoint data. Each time a checkpoint is written, any file-backed artifacts are included as additional entries:
```
refs/fabro/{run_id}
fabro/meta/{run_id}
manifest.json
graph.dot
graph.fabro
checkpoint.json
artifacts/
response.plan.json
@ -291,4 +291,4 @@ Outputs and artifacts appear in several observability surfaces:
| `WorkflowRunCompleted` event | `artifact_count` -- total number of offloaded artifacts across the run |
| [Retros](/execution/retros) | Per-stage `files_touched` and aggregate `files_touched` across all stages |
| [Preambles](/execution/context#preamble-construction) | File list and artifact pointer references for completed stages |
| Stage logs | `status.json` in each stage's run directory contains the full outcome including `files_touched` |
| Stage logs | `status.json` in each stage's run directory contains the full outcome including `files_touched` |

View file

@ -43,11 +43,13 @@ review [label="Review", prompt="@prompts/implement/review.md"]
The `@` prefix tells the engine to read the file contents and use them as the prompt text. This keeps DOT files concise and lets you version prompts as standalone Markdown.
File references are resolved relative to the DOT file's directory first, then fall back to `~/.fabro/`. This lets you keep shared prompts in your user-level config and reference them from any project.
### Variable expansion
Prompts support `$variable` placeholders that expand at runtime. Currently the only built-in variable is `$goal`, which resolves to the graph-level `goal` attribute:
```dot title="pipeline.dot"
```dot title="pipeline.fabro"
digraph Pipeline {
graph [goal="Add a /health endpoint to the API server"]

View file

@ -2924,7 +2924,7 @@ components:
filename:
type: string
description: DOT graph filename.
example: fix_build.dot
example: fix_build.fabro
last_run:
$ref: "#/components/schemas/WorkflowLastRun"
schedule:
@ -2952,7 +2952,7 @@ components:
filename:
type: string
description: DOT graph filename.
example: fix_build.dot
example: fix_build.fabro
description:
type: string
description: Prose description of what the workflow does.
@ -4007,8 +4007,8 @@ components:
graph:
type: string
description: DOT graph filename.
example: fix_build.dot
directory:
example: fix_build.fabro
work_dir:
type: string
description: Working directory for the run.
llm:
@ -4277,7 +4277,7 @@ components:
$ref: "#/components/schemas/FeatureFlags"
log:
$ref: "#/components/schemas/LogConfiguration"
directory:
work_dir:
type: string
description: Default working directory.
llm:
@ -4301,6 +4301,55 @@ components:
$ref: "#/components/schemas/HookDefinition"
assets:
$ref: "#/components/schemas/AssetsConfiguration"
mcp_servers:
type: object
additionalProperties:
$ref: "#/components/schemas/McpServerEntry"
description: Default MCP server configurations.
github:
$ref: "#/components/schemas/GitHubConfiguration"
GitHubConfiguration:
description: GitHub App token injection configuration.
type: object
properties:
permissions:
type: object
additionalProperties:
type: string
description: GitHub API permissions to request (e.g. contents = write).
McpServerEntry:
description: MCP server connection entry.
type: object
properties:
type:
type: string
description: Transport type (stdio or http).
command:
type: array
items:
type: string
description: Command and arguments for stdio transport.
env:
type: object
additionalProperties:
type: string
description: Environment variables for stdio transport.
url:
type: string
description: URL for http transport.
headers:
type: object
additionalProperties:
type: string
description: HTTP headers for http transport.
startup_timeout_secs:
type: integer
description: Startup timeout in seconds.
tool_timeout_secs:
type: integer
description: Tool call timeout in seconds.
AssetsConfiguration:
description: Asset collection configuration.

View file

@ -8,9 +8,9 @@ date: "2026-02-23"
Run AI workflows from the command line with `fabro run start`, validate DOT workflow definitions with `fabro validate`, and step through dry-runs to test logic before committing real LLM calls.
```bash
fabro run start spec-dod-multimodel.dot
fabro validate my-workflow.dot
fabro run start --dry-run my-workflow.dot
fabro run start spec-dod-multimodel.fabro
fabro validate my-workflow.fabro
fabro run start --dry-run my-workflow.fabro
```
The CLI streams LLM responses in real time and supports interactive tool approval — each tool call pauses for you to approve or reject via arrow-key prompts, giving fine-grained control over what the agent does.
@ -20,7 +20,7 @@ The CLI streams LLM responses in real time and supports interactive tool approva
Agent tool execution can now run inside Docker containers, so workflows can safely run shell commands, edit files, and install dependencies without affecting your host machine.
```bash
fabro run start --docker my-workflow.dot
fabro run start --docker my-workflow.fabro
```
The container is shared across all stages in a run, so tools have access to the same filesystem throughout the workflow.

View file

@ -8,7 +8,7 @@ date: "2026-02-26"
Workflows can now execute in Daytona cloud environments — full dev containers with SSH access, persistent storage, and network isolation. Previously, Docker was the only sandbox option, which meant running everything locally. Daytona moves execution to the cloud, freeing up your machine and providing a more production-like environment.
```bash
fabro run start --execution-env daytona my-workflow.dot
fabro run start --execution-env daytona my-workflow.fabro
```
## TOML run configuration

View file

@ -14,7 +14,7 @@ Verifications run after each workflow completes and report pass/fail status, so
After each run, an LLM-powered retro agent analyzes what happened and generates a structured summary — what worked, what didn't, timing breakdown, cost, and improvement suggestions. The retro prints inline in your terminal after the run completes, rendered as Markdown.
```bash
fabro run start my-workflow.dot
fabro run start my-workflow.fabro
# ... run executes ...
# === Retro ===
# The run completed in 4m 32s across 6 stages...

View file

@ -14,7 +14,7 @@ Previously, starting too many runs at once could overwhelm the machine. Now exce
Use `--ssh` to get SSH access into running Daytona sandboxes for live debugging while the workflow executes. When something goes wrong mid-run, you can drop into the sandbox, inspect the filesystem, and understand the problem without waiting for the run to finish.
```bash
fabro run start --ssh my-workflow.dot
fabro run start --ssh my-workflow.fabro
```
Use `--preserve-sandbox` to keep sandboxes alive after a run completes for post-mortem inspection.

View file

@ -35,7 +35,7 @@ image = "my-custom-image:latest"
```
```bash
fabro run --ssh my-workflow.dot
fabro run --ssh my-workflow.fabro
```
## `fabro cp` — copy files to and from sandboxes
@ -68,7 +68,7 @@ fabro system df
<Accordion title="CLI">
- PRs are now created as drafts by default; opt out with `draft = false` in `[pull_request]` config
- Added `[sandbox.local] worktree_mode` config (`always`/`clean`/`dirty`/`never`) for controlling when git worktrees are created
- Added `[pull_request]` config section in `cli.toml` so auto-PR works with `.dot` files
- Added `[pull_request]` config section in `cli.toml` so auto-PR works with `.fabro` files
- Added version info (semver, git SHA, build date) to `fabro --version`
- Run summary now shows Run ID, logs path, base commit, branch, and PR URL
- Workflow run output now shows local time instead of UTC

View file

@ -22,7 +22,7 @@ After:
```
<Warning>
**Breaking change.** `llm_model` and `llm_provider` stylesheet properties have been renamed to `model` and `provider`. Update your DOT workflow stylesheets.
**Breaking change.** `llm_model` and `llm_provider` stylesheet properties have been renamed to `model` and `provider`. Update your workflow stylesheets.
</Warning>
## More

View file

@ -0,0 +1,36 @@
---
title: ".fabro file extension, human gate improvements, and user workflows"
date: "2026-03-13"
---
## .fabro file extension
Workflow files now use the `.fabro` extension instead of `.dot`. This gives workflows a distinct identity and avoids conflicts with Graphviz `.dot` files. Existing `.dot` files still work as a fallback — the engine checks for `graph.fabro` first, then falls back to `graph.dot`.
```toml
[workflow]
graph = "workflow.fabro"
```
<Warning>
**Breaking change.** Workflow files have been renamed from `.dot` to `.fabro`. Existing `.dot` files continue to work as a fallback, but new projects should use `.fabro`.
</Warning>
## Smarter human gates
Human-in-the-loop gates now show the previous stage's output before prompting, so you can see what the agent produced before deciding what to do next. Gates with only a freeform edge also skip the multiple-choice menu and go straight to a text input, making conversational loops like REPL workflows feel more natural.
## More
<Accordion title="CLI">
- Added user-level workflow lookup in `~/.fabro/workflows/` — personal workflows are now available across all projects
</Accordion>
<Accordion title="Workflows">
- New `thread_id_requires_fidelity_full` lint rule warns when `thread_id` is set without `fidelity=full`
- Added example REPL workflow for interactive agent loops
</Accordion>
<Accordion title="Improvements">
- Branding now appears in generated commits and PR descriptions
</Accordion>

View file

@ -0,0 +1,58 @@
---
title: "fabro logs, background runs, and workflow scaffolding"
date: "2026-03-14"
---
## View run logs with fabro logs
Previously, the only way to follow a workflow's progress was through the terminal that started it. The new `fabro logs` command lets you view event logs for any run — active or completed — from any terminal. With `--pretty`, agent conversations render with formatted messages and tool calls instead of raw JSON.
```bash
fabro logs my-workflow --pretty
fabro logs -f abc123 -p
```
You can reference runs by name, ID prefix, or workflow slug. The `-f` flag follows live events as they happen.
## Background workflows with --detach
You can now fork a workflow into a background process with `fabro run --detach`, then reconnect later with `fabro logs -f`. This is useful for long-running workflows where you don't want to keep a terminal open.
```bash
fabro run my-workflow --detach
fabro logs -f my-workflow
```
## Rewind workflow runs
You can now rewind a workflow run to an earlier checkpoint and resume from there. This is useful when a later stage goes off-track and you want to try again from a known-good point without restarting the entire workflow.
```bash
fabro rewind my-run plan@2
```
Target a specific node by name, `node@visit` for a particular visit count, or `@ordinal` for a checkpoint index.
## More
<Accordion title="CLI">
- Added `fabro workflow create <name>` subcommand to scaffold new workflows from a template
- Added `fabro workflow list` command showing all available workflows grouped by source
- Added `fabro inspect` command to display detailed JSON data for a workflow run
- Added project-level run defaults in `fabro.toml` (model, environment, sandbox image, etc.)
- Added `~/.fabro/` fallback for `@` file references
</Accordion>
<Accordion title="Improvements">
- Default Daytona sandbox now uses the `daytona-medium` snapshot with standard dev tools pre-installed
- Run resolution now matches workflow slugs and display names, not just run IDs
- Routing events now include context about why an edge was selected
</Accordion>
<Accordion title="Fixes">
- Fixed `fabro logs --pretty` wrapping past terminal edge on long assistant messages
- Fixed empty `run_id` on sandbox events in `progress.jsonl`
- Fixed credential-embedded GitHub URLs not being parsed correctly
- Fixed logo SVG viewBox clipping the right edge of the O
- Fixed missing `git_commit_sha` in run branch commit messages
</Accordion>

View file

@ -0,0 +1,42 @@
---
title: "fabro ps overhaul, fabro rm, and GitHub token injection"
date: "2026-03-15"
---
## Docker-style process listing with fabro ps
`fabro ps` has been rebuilt to behave like `docker ps`. It now shows a table with run ID, status, workflow name, goal, and timing — making it easy to see what's running at a glance. The GOAL column shows the first line of each run's goal, so you can distinguish between multiple runs of the same workflow.
```bash
fabro ps # active runs
fabro ps -a # all runs including completed
```
Run status is now tracked via a proper state machine, so status transitions are reliable and `fabro ps` always reflects the current state.
## GitHub token injection for sandboxes
When a GitHub App is configured, Fabro now automatically injects an installation access token into sandboxes as `GITHUB_TOKEN`. Agents running inside sandboxes can use this token to clone private repos, push branches, and create pull requests without any manual credential setup.
## More
<Accordion title="CLI">
- Added `fabro rm` command to remove runs by ID with sandbox cleanup
- Added `-p` short alias for `--pretty` in `fabro logs`
- Added progress spinner during `run --preflight`
</Accordion>
<Accordion title="Workflows">
- Metadata branch renamed from `refs/fabro/{run_id}` to `fabro/meta/{run_id}` for cleaner ref namespace
- Added granular git checkpoint events and retro lifecycle events (`RetroStarted`, `RetroCompleted`, `RetroFailed`)
- `goal` field now included in `WorkflowRunStarted` event and rendered in `fabro logs --pretty`
- Run completion events now include final status and usage totals
</Accordion>
<Accordion title="Fixes">
- Fixed `--dry-run` executing command/script nodes instead of simulating them
- Fixed `--dry-run` pushing branches to remote
- Fixed `--goal-file` not expanding `~` to the home directory
- Fixed race condition between `fabro run --detach` and `fabro logs -f`
- Fixed dry-run runs cluttering `fabro ps -a` output (now uses temp directory)
</Accordion>

View file

@ -16,13 +16,13 @@ Fabro has two interfaces, both backed by the same workflow engine:
- **Standalone mode** (`fabro run`) — Run a single workflow synchronously in your terminal. Best for local development, one-off runs, and CI/CD.
- **Server mode** (`fabro serve`) — Start an HTTP API server with a web UI, concurrent run scheduling, and team access. Best for production use and running at scale.
Both modes parse the same DOT files, use the same execution engine, and support the same sandbox providers. See [Server Mode](/core-concepts/server-mode) for a detailed comparison and setup guide, or [Architecture](/reference/architecture) for internals.
Both modes parse the same DOT files, use the same execution engine, and support the same sandbox providers. See [Server Mode](/administration/deploy-server) for a detailed comparison and setup guide, or [Architecture](/reference/architecture) for internals.
## Author time
You provide three inputs:
1. **Workflow graph** (`.dot`) — A Graphviz DOT file defining nodes, edges, and their attributes. This is the core of what Fabro executes. See [Workflows](/core-concepts/workflows).
1. **Workflow graph** (`.fabro`) — A Graphviz DOT file defining nodes, edges, and their attributes. This is the core of what Fabro executes. See [Workflows](/core-concepts/workflows).
2. **Run config** (`.toml`, optional) — Overrides for the default model, sandbox provider, setup commands, and variables. See [Run Configuration](/execution/run-configuration).
3. **API keys** (`.env`) — Provider credentials for LLM APIs. See [Quick Start](/getting-started/quick-start).

View file

@ -52,7 +52,7 @@ When no model is specified, the `fabro exec` command uses a default model based
Assign models to workflow nodes using [model stylesheets](/workflows/stylesheets), which use a CSS-like syntax:
```dot title="example.dot"
```dot title="example.fabro"
digraph Example {
graph [
model_stylesheet="
@ -79,8 +79,8 @@ Model stylesheets set per-node models inside the workflow graph, but you can als
Pass `--model` and optionally `--provider` to `fabro run`:
```bash
fabro run files-internal/demo/01-hello.dot --model claude-opus-4-6
fabro run files-internal/demo/04-pipeline.dot --model gemini-3.1-pro-preview
fabro run files-internal/demo/01-hello.fabro --model claude-opus-4-6
fabro run files-internal/demo/04-pipeline.fabro --model gemini-3.1-pro-preview
```
These flags set the default model for all nodes that don't have an explicit model assigned via a stylesheet. The provider is automatically inferred from the model catalog — you only need `--provider` for models not in the catalog or to force a specific provider.
@ -92,7 +92,7 @@ For repeatable runs, set the model in a run config file:
```toml title="run.toml"
version = 1
goal = "Implement the feature"
graph = "implement.dot"
graph = "implement.fabro"
[llm]
model = "claude-sonnet-4-5"

View file

@ -1,145 +0,0 @@
---
title: "Server Mode"
description: "Run Fabro as an API server with a web UI, concurrent runs, and team access"
---
<Warning>
Server mode is in private early access. Contact [bryan@qlty.sh](mailto:bryan@qlty.sh) if you're interested in trying it.
</Warning>
Fabro has two modes: **standalone** and **server**. Standalone mode (`fabro run`) executes a single workflow synchronously in your terminal. Server mode (`fabro serve`) starts an HTTP API that queues runs, streams events, and serves a web UI — so you can close your laptop and let workflows run.
Both modes use the same workflow engine, the same DOT files, and the same sandbox providers. The difference is how you interact with them.
## Standalone vs. server mode
| | Standalone | Server |
|---|---|---|
| **Command** | `fabro run workflow.dot` | `fabro serve` |
| **Best for** | Local development, one-off runs, CI/CD | Production, team use, running at scale |
| **Execution** | Synchronous, one run per process | Asynchronous, queued with configurable concurrency |
| **Human-in-the-loop** | Terminal prompts | Web UI or HTTP endpoints |
| **Events** | Printed to stderr | Streamed via SSE |
| **Persistence** | Checkpoint files only | SQLite database + checkpoint files |
| **Web UI** | Not available | Full React interface |
| **Authentication** | None | JWT and/or mTLS |
## Starting the server
```bash
fabro serve
```
This starts the API on `127.0.0.1:3000` by default. To also run the web UI:
```bash
fabro serve # API on port 3000
cd apps/fabro-web && bun run dev # Web UI on port 5173
```
Common flags:
| Flag | Default | Description |
|---|---|---|
| `--port` | `3000` | Port to listen on |
| `--host` | `127.0.0.1` | Host address to bind to |
| `--model` | — | Override default LLM model |
| `--sandbox` | — | Override default sandbox provider |
| `--max-concurrent-runs` | `5` | Maximum concurrent run executions |
See [Server Configuration](/administration/server-configuration) for the full `server.toml` reference.
## Submitting runs
In server mode, workflows are submitted via the REST API and executed in the background:
```bash
curl -X POST http://localhost:3000/runs \
-H "Content-Type: application/json" \
-d '{"workflow": "implement-feature", "goal": "Add user authentication"}'
```
The server returns immediately with a run ID. A background scheduler promotes queued runs to `Running` in FIFO order, up to the concurrency limit.
## Run lifecycle
1. **Submit** — `POST /runs` creates the run with status `Queued`.
2. **Schedule** — The scheduler picks up queued runs up to `max_concurrent_runs`.
3. **Execute** — The engine walks the graph, streaming events to all subscribers.
4. **Complete** — The run transitions to `Completed`, `Failed`, or `Cancelled`.
## Web UI
The web UI connects to the API server and provides:
- **Runs board** — Monitor all active runs organized by status
- **Run detail** — Real-time stage progress, event stream, diffs, and usage stats
- **Start new run** — Submit workflows from the browser
- **Human-in-the-loop** — Answer agent questions through the web interface
- **Workflows** — Browse available workflows, view their graphs, and see run history
- **Insights** — SQL-based analysis across runs via DuckDB
<Frame caption="The Runs board shows all active runs organized by status.">
<img src="/images/web/runs-board.png" alt="Fabro web UI Runs board with Working, Pending, Verify, and Merge columns" />
</Frame>
<Frame caption="The run detail view shows stage progress alongside the workflow graph.">
<img src="/images/web/run-overview.png" alt="Fabro web UI run detail showing stages and workflow graph" />
</Frame>
## Event streaming
The API streams run events via [Server-Sent Events (SSE)](/api-reference/runs#get-events). Every stage start, LLM call, tool invocation, and edge selection is emitted as a structured JSON event. Any HTTP client that supports SSE can subscribe — the web UI is just one consumer.
## Human-in-the-loop
In server mode, human-in-the-loop questions are served over HTTP instead of terminal prompts. The engine blocks the current stage until an answer is submitted, then continues execution. See the [Human-in-the-Loop API reference](/api-reference/human-in-the-loop) for the polling and answer endpoints.
## Authentication
Server mode supports two authentication strategies, configurable in `server.toml`:
- **JWT** — EdDSA-signed bearer tokens. Used by the web UI. See [API Overview](/api-reference/overview#jwt-bearer-token) for token format.
- **mTLS** — Mutual TLS with client certificates. Used for service-to-service communication. See [API Overview](/api-reference/overview#mtls-mutual-tls) for setup.
Both strategies can be enabled simultaneously — the first successful match wins.
## Demo mode
Send the `X-Fabro-Demo: 1` header on any API request to get static mock data with authentication disabled. The web UI enables this automatically with the `FABRO_DEMO=1` environment variable. This lets you explore the UI without API keys or real workflow execution. See [Demo Mode](/api-reference/demo-mode) for details.
## Pointing the CLI at a server
The CLI can delegate commands to a running Fabro server instead of executing locally. Set `mode = "server"` in `~/.fabro/cli.toml`:
```toml title="cli.toml"
mode = "server"
[server]
base_url = "https://fabro.example.com:3000"
```
Or use the `--mode` flag:
```bash
fabro --mode server --server-url https://fabro.example.com:3000 models list
```
This applies to commands like `fabro models list`, `fabro llm chat`, and `fabro exec`. See [CLI Configuration](/reference/cli-configuration#mode) for the full options including mTLS setup.
## Next steps
<Columns cols={2}>
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
Full server.toml reference — authentication, TLS, run defaults, and more.
</Card>
<Card title="Server Deployment" icon="server" href="/administration/deploy-server">
Deploy Fabro to production infrastructure.
</Card>
<Card title="API Reference" icon="code" href="/api-reference/overview">
REST API for submitting runs, streaming events, and managing resources.
</Card>
<Card title="How Fabro Works" icon="lightbulb" href="/core-concepts/how-fabro-works">
The workflow engine that powers both modes.
</Card>
</Columns>

View file

@ -13,7 +13,7 @@ Every workflow is a `digraph` with a `goal`, a `start` node, an `exit` node, and
<img src="/images/anatomy-workflow.svg" alt="Simple workflow: Start → Scan Files → Analyze → Exit" />
</Frame>
```dot title="my-workflow.dot"
```dot title="my-workflow.fabro"
digraph MyWorkflow {
graph [goal="Describe the project"]
rankdir=LR
@ -112,7 +112,7 @@ validate [label="Validate", prompt="Run the test suite and verify all tests pass
From the CLI:
```bash
fabro run workflow.dot
fabro run workflow.fabro
```
Or from a [run config TOML](/execution/run-configuration) for repeatable, parameterized runs:

View file

@ -20,8 +20,7 @@
"pages": [
"getting-started/introduction",
"getting-started/why-fabro",
"getting-started/quick-start",
"getting-started/comparison"
"getting-started/quick-start"
]
},
{
@ -29,10 +28,10 @@
"icon": "lightbulb",
"pages": [
"core-concepts/how-fabro-works",
"getting-started/dark-factory",
"core-concepts/workflows",
"core-concepts/agents",
"core-concepts/models",
"core-concepts/server-mode"
"core-concepts/models"
]
},
{
@ -103,6 +102,7 @@
"group": "Reference",
"icon": "book",
"pages": [
"getting-started/comparison",
"reference/dot-language",
"reference/cli",
"reference/cli-configuration",
@ -270,6 +270,9 @@
"group": "March 2026",
"icon": "clock-rotate-left",
"pages": [
"changelog/2026-03-15",
"changelog/2026-03-14",
"changelog/2026-03-13",
"changelog/2026-03-12",
"changelog/2026-03-11",
"changelog/2026-03-10",

View file

@ -5,7 +5,7 @@ description: "Build an entire application from a detailed specification using de
The Clone Substack workflow takes a detailed specification document and autonomously builds a complete, working application — in this case, a Substack-like newsletter creation tool. It uses ensemble planning (two independent plans debated into one), a multi-stage verification chain, parallel code review with consensus, and a postmortem repair loop that feeds failures back into the next iteration.
This pattern is adapted from [Kilroy](https://github.com/danshapiro/kilroy)'s `substack-spec-v01.dot`, which builds a full React application from a natural language spec with acceptance criteria.
This pattern is adapted from [Kilroy](https://github.com/danshapiro/kilroy)'s `substack-spec-v01.fabro`, which builds a full React application from a natural language spec with acceptance criteria.
## When to use this
@ -20,7 +20,7 @@ This pattern is adapted from [Kilroy](https://github.com/danshapiro/kilroy)'s `s
<img src="/images/example-clone-substack.svg" alt="Clone Substack workflow: Start → Bootstrap → Plan Fan-Out → Plan A and Plan B → Debate → Implement → Verify Chain → Review Fan-Out → Review A and Review B → Consensus → Exit, with Fix loop from Verify back to Implement, Rejected path from Consensus to Postmortem, and Replan loop from Postmortem back to Plan Fan-Out" />
</Frame>
```dot title="clone-substack.dot"
```dot title="clone-substack.fabro"
digraph CloneSubstack {
graph [
goal="Build the Substack Creator Newsletter Engine — a pure React frontend \

View file

@ -22,7 +22,7 @@ This pattern is useful when you have detailed specs with acceptance criteria (De
The simpler variant uses one model throughout, with sequential audits across multiple specs:
```dot title="spec-dod.dot"
```dot title="spec-dod.fabro"
digraph SpecDoD {
graph [
goal="Satisfy every Definition of Done checkbox across both specs (unified-llm-spec.md, coding-agent-loop-spec.md). The implementation is in Rust under crates/. Do NOT modify the spec files. Only modify implementation code.",
@ -278,7 +278,7 @@ Otherwise set preferred_next_label to \"more_work_needed\"."
The multi-model variant applies the same audit-triage-fix-verify structure but uses independent assessments from two models (Claude Opus and GPT-5.2) at each phase, with cross-critique and consensus merging. This catches blind spots that a single model might miss.
```dot title="spec-dod-multimodel.dot"
```dot title="spec-dod-multimodel.fabro"
digraph SpecDoDMultiModel {
graph [
goal="Satisfy every Definition of Done checkbox across both specs (unified-llm-spec.md, coding-agent-loop-spec.md). The implementation is in Rust under crates/. Do NOT modify the spec files. Only modify implementation code. Uses multi-model consensus: Opus 4.6 and GPT-5.2 compete on audits and planning, GPT-5.2-codex and Opus 4.6 alternate on implementation.",

View file

@ -17,7 +17,7 @@ The NLSpec Conformance pattern gives an agent a detailed specification document,
<img src="/images/nlspec-conformance.svg" alt="NLSpec Conformance workflow: Start → Plan → Implement → Quick Tests → Quick passing? → Full Tests → All passing? → Exit, with Fix Failures loop" />
</Frame>
```dot title="n-l-spec-conformance.dot"
```dot title="n-l-spec-conformance.fabro"
digraph NLSpecConformance {
graph [
goal="Implement a conformant system from a natural language specification",
@ -67,7 +67,7 @@ digraph NLSpecConformance {
```
```bash
fabro run workflows/nlspec-conformance.dot
fabro run workflows/nlspec-conformance.fabro
```
## How it works

View file

@ -13,7 +13,7 @@ This pattern is useful when you maintain a downstream implementation (e.g., a Go
<img src="/images/example-semantic-port.svg" alt="Semantic Port workflow: Start → Fetch → Analyze → Plan → Implement → Validate → Tests pass? → Finalize → loops back to Fetch, with Skip shortcut from Analyze back to Fetch, Fix loop from gate back to Validate, and Done exit from Fetch" />
</Frame>
```dot title="semantic-port.dot"
```dot title="semantic-port.fabro"
digraph SemanticPort {
graph [
goal="Port semantic changes from upstream Python repository to our Go implementation",
@ -216,7 +216,7 @@ Pair the workflow with a run config TOML for repeatable execution:
```toml title="run.toml"
version = 1
goal = "Port semantic changes from upstream openai-agents-python to our Go SDK"
graph = "semport.dot"
graph = "semport.fabro"
[llm]
model = "claude-sonnet-4-5"

View file

@ -13,7 +13,7 @@ This pattern is useful when you want an agent to build something non-trivial fro
<img src="/images/example-solitaire.svg" alt="Build Solitaire workflow: Start → Spec → Setup → OK? → Data → OK? → Logic → OK? → UI → OK? → Integrate → OK? → Review → OK? → Exit, with Retry arcs from each gate back to its phase, and a Fix arc from the review gate back to UI" />
</Frame>
```dot title="build-solitaire.dot"
```dot title="build-solitaire.fabro"
digraph BuildSolitaire {
graph [
goal="Build a terminal-based solitaire (Klondike) game in Python",
@ -257,7 +257,7 @@ Pair the workflow with a run config for repeatable execution:
```toml title="run.toml"
version = 1
goal = "Build a terminal-based solitaire (Klondike) game in Python"
graph = "build-solitaire.dot"
graph = "build-solitaire.fabro"
[llm]
model = "claude-sonnet-4-5"

View file

@ -12,7 +12,7 @@ Each run creates two Git branches that work in tandem:
| Branch | Ref format | Contains |
|---|---|---|
| **Run branch** | `fabro/run/{run_id}` | File changes made by agents and commands — the actual work product |
| **Metadata branch** | `refs/fabro/{run_id}` | Checkpoint JSON, the workflow graph, a run manifest, and offloaded artifacts |
| **Metadata branch** | `fabro/meta/{run_id}` | Checkpoint JSON, the workflow graph, a run manifest, and offloaded artifacts |
The run branch is a regular Git branch that grows one commit per completed node. The metadata branch is an orphan branch (no shared history with your code) that stores structured data using Git's object database directly — no working tree needed.
@ -41,10 +41,10 @@ The `Fabro-Checkpoint` trailer links each run branch commit to its metadata bran
### Metadata branch
The metadata branch (`refs/fabro/{run_id}`) is an orphan branch that stores structured run data using Git's object storage directly (via `git2`). It is initialized at run start with:
The metadata branch (`fabro/meta/{run_id}`) is an orphan branch that stores structured run data using Git's object storage directly (via `git2`). It is initialized at run start with:
- **`manifest.json`** — Run metadata: run ID, graph name, node/edge counts, base SHA, and branch name
- **`graph.dot`** — The workflow DOT source as it was parsed
- **`graph.fabro`** — The workflow DOT source as it was parsed
After each node, the metadata branch is updated with:
@ -87,7 +87,7 @@ This means your original working directory stays untouched while the agent makes
If the working directory has uncommitted changes, Fabro skips worktree setup and runs in place, logging a warning. Git checkpointing is disabled in this case.
</Note>
For Daytona sandboxes, the worktree is created inside the remote sandbox instead. The metadata branch is still written to the host repository so that runs can be resumed locally. Both the run branch and the metadata branch are pushed to origin after each checkpoint — the run branch is pushed from the sandbox, while the metadata branch is pushed from the host using a GitHub App installation token. On the remote, the metadata branch appears at `fabro/meta/{run_id}` (rather than the local `refs/fabro/{run_id}` custom ref, since GitHub disallows branch names starting with `refs/`).
For Daytona sandboxes, the worktree is created inside the remote sandbox instead. The metadata branch is still written to the host repository so that runs can be resumed locally. Both the run branch and the metadata branch are pushed to origin after each checkpoint — the run branch is pushed from the sandbox, while the metadata branch is pushed from the host using a GitHub App installation token.
## Resuming a run
@ -98,7 +98,7 @@ There are two ways to resume an interrupted run:
Resume from a `checkpoint.json` saved in the run directory:
```bash
fabro run workflow.dot --resume path/to/logs/checkpoint.json
fabro run workflow.fabro --resume path/to/logs/checkpoint.json
```
Fabro loads the checkpoint, restores the context and execution state, and continues from the next node after the checkpoint.
@ -111,11 +111,11 @@ Resume from the Git branches created during a previous run:
fabro run --run-branch fabro/run/01JKXYZ...
```
This reads the checkpoint, manifest, and graph DOT from the metadata branch (`refs/fabro/01JKXYZ...`), re-attaches a worktree to the existing run branch, and resumes execution. No workflow file argument is needed — everything is recovered from Git.
This reads the checkpoint, manifest, and graph DOT from the metadata branch (`fabro/meta/01JKXYZ...`), re-attaches a worktree to the existing run branch, and resumes execution. No workflow file argument is needed — everything is recovered from Git.
<Accordion title="What happens during resume">
1. Fabro reads `checkpoint.json` from the metadata branch
2. Reads `manifest.json` and `graph.dot` to reconstruct the workflow
2. Reads `manifest.json` and `graph.fabro` to reconstruct the workflow
3. Creates a fresh worktree attached to the existing run branch
4. Restores the full context, completed node list, retry counts, and failure signatures
5. If the checkpointed node used `full` fidelity, downgrades the first resumed node to `summary:high` (since the original conversation thread no longer exists in memory)
@ -147,13 +147,27 @@ git show fabro/run/01JKXYZ...
# Diff the full run against the starting point
git diff main..fabro/run/01JKXYZ...
# Read checkpoint data from the metadata branch (local)
git show refs/fabro/01JKXYZ...:checkpoint.json | jq .current_node
# Read checkpoint data from the remote (Daytona runs)
git show origin/fabro/meta/01JKXYZ...:checkpoint.json | jq .current_node
# Read checkpoint data from the metadata branch
git show fabro/meta/01JKXYZ...:checkpoint.json | jq .current_node
```
## Rewinding to an earlier checkpoint
If a later stage goes off-track, you can rewind a run to an earlier checkpoint and resume from there instead of restarting the entire workflow:
```bash
# List the checkpoint timeline
fabro rewind <RUN_ID> --list
# Rewind to a specific checkpoint
fabro rewind <RUN_ID> plan@2
# Resume from the rewound point
fabro run --run-branch fabro/run/<RUN_ID>
```
See [`fabro rewind`](/reference/cli#fabro-rewind) for the full command reference.
## When checkpointing is active
Git checkpointing activates automatically when:
@ -165,4 +179,4 @@ It is skipped when:
- The working directory has uncommitted changes
- The working directory is not a Git repository
- The run uses `--dry-run`
- The run uses `--dry-run`

View file

@ -11,7 +11,7 @@ Set `devcontainer = true` in the `[sandbox]` section of your run config:
```toml title="run.toml"
version = 1
graph = "workflow.dot"
graph = "workflow.fabro"
[sandbox]
provider = "daytona"

View file

@ -22,11 +22,11 @@ Set the sandbox provider via CLI flag, [run config TOML](/execution/run-configur
```bash
# CLI flag
fabro run workflow.dot --sandbox local
fabro run workflow.dot --sandbox docker
fabro run workflow.dot --sandbox daytona
fabro run workflow.dot --sandbox ssh
fabro run workflow.dot --sandbox exe
fabro run workflow.fabro --sandbox local
fabro run workflow.fabro --sandbox docker
fabro run workflow.fabro --sandbox daytona
fabro run workflow.fabro --sandbox ssh
fabro run workflow.fabro --sandbox exe
```
```toml title="run.toml"
@ -93,7 +93,7 @@ The Docker sandbox is configured through the `DockerSandboxConfig`:
By default, the container is destroyed when the run finishes. To keep it alive for debugging:
```bash
fabro run workflow.dot --sandbox docker --preserve-sandbox
fabro run workflow.fabro --sandbox docker --preserve-sandbox
```
Or in the run config:
@ -170,7 +170,7 @@ When using server defaults, labels are merged — run config labels override def
Connect to a running Daytona sandbox via SSH for live debugging:
```bash
fabro run workflow.dot --sandbox daytona --ssh
fabro run workflow.fabro --sandbox daytona --ssh
```
This creates temporary SSH credentials (valid for 60 minutes) and prints the connection command.
@ -180,7 +180,7 @@ This creates temporary SSH credentials (valid for 60 minutes) and prints the con
Like Docker, Daytona sandboxes are destroyed on cleanup by default. Use `--preserve-sandbox` to keep them alive:
```bash
fabro run workflow.dot --sandbox daytona --preserve-sandbox
fabro run workflow.fabro --sandbox daytona --preserve-sandbox
```
Fabro prints the sandbox name so you can find it in the [Daytona dashboard](https://app.daytona.io/dashboard/sandboxes).
@ -306,7 +306,7 @@ image = "my-custom-image:latest"
Connect to a running exe.dev sandbox via SSH for live debugging:
```bash
fabro run workflow.dot --sandbox exe --ssh
fabro run workflow.fabro --sandbox exe --ssh
```
This prints the SSH connection command so you can connect to the VM while the workflow runs.

View file

@ -132,7 +132,7 @@ Fabro has two independent mechanisms for detecting stuck loops: **node visit lim
The `max_node_visits` graph attribute sets the maximum number of times any single node can execute before the run is terminated:
```dot title="example.dot"
```dot title="example.fabro"
digraph Example {
graph [max_node_visits="20"]
// ...
@ -155,7 +155,7 @@ node "verify" visited 20 times (graph limit 20); run is stuck in a cycle
You can set `max_visits` on individual nodes to override the graph-level limit for that node:
```dot title="example.dot"
```dot title="example.fabro"
digraph Example {
graph [max_node_visits="20"]
fix [max_visits=3]
@ -231,7 +231,7 @@ When a goal gate is unsatisfied at the exit node, Fabro looks for a **retry targ
3. Graph-level `retry_target` attribute
4. Graph-level `fallback_retry_target` attribute
```dot title="example.dot"
```dot title="example.fabro"
digraph Example {
graph [retry_target="plan"]
verify [shape=box, goal_gate="true", retry_target="implement"]
@ -255,7 +255,7 @@ Fabro runs a background watchdog that monitors event activity. If no events are
| `stall_timeout` | 1800 seconds (30 minutes) |
| Set to `0` | Disables the watchdog |
```dot title="example.dot"
```dot title="example.fabro"
digraph Example {
graph [stall_timeout="300"] // 5 minutes
}

View file

@ -21,8 +21,8 @@ Events fall into several categories:
| Event | Key fields | Description |
|---|---|---|
| `WorkflowRunStarted` | `name`, `run_id`, `base_sha`, `run_branch` | Run begins |
| `WorkflowRunCompleted` | `duration_ms`, `artifact_count`, `total_cost` | Run finishes successfully |
| `WorkflowRunStarted` | `name`, `run_id`, `base_sha`, `run_branch`, `goal` | Run begins |
| `WorkflowRunCompleted` | `duration_ms`, `artifact_count`, `total_cost`, `status`, `usage` | Run finishes successfully |
| `WorkflowRunFailed` | `error`, `duration_ms` | Run terminates with an error |
**Stage lifecycle** — events for each node execution:
@ -56,11 +56,20 @@ Events fall into several categories:
| Event | Key fields | Description |
|---|---|---|
| `EdgeSelected` | `from_node`, `to_node`, `label`, `condition` | Transition between nodes |
| `EdgeSelected` | `from_node`, `to_node`, `label`, `condition`, `reason`, `stage_status` | Transition between nodes |
| `LoopRestart` | `from_node`, `to_node` | Loop restart edge taken |
| `CheckpointSaved` | `node_id` | Checkpoint written to disk |
| `GitCheckpoint` | `node_id`, `git_commit_sha` | Checkpoint committed to Git |
| `CheckpointCompleted` | `node_id`, `git_commit_sha` (optional) | Checkpoint saved (with git SHA when git is enabled) |
| `GitCommit` | `node_id`, `sha` | Git commit created |
| `GitPush` | `branch`, `success` | Git push attempted |
| `GitBranch` | `branch`, `sha` | Git branch created |
| `GitWorktreeAdd` | `path`, `branch` | Git worktree added |
| `GitWorktreeRemove` | `path` | Git worktree removed |
| `GitFetch` | `branch`, `success` | Git fetch attempted |
| `GitReset` | `sha` | Git reset executed |
| `Failover` | `stage`, `from_provider`, `to_provider`, `error` | LLM provider failover |
| `RetroStarted` | — | Retrospective generation begins |
| `RetroCompleted` | `duration_ms` | Retrospective generation finished |
| `RetroFailed` | `error`, `duration_ms` | Retrospective generation failed |
**Parallel execution:**
@ -140,7 +149,7 @@ cat ~/.fabro/runs/01JKXYZ.../live.json
Fabro uses the `tracing` crate to write structured logs to `~/.fabro/logs/YYYY-MM-DD.log`. Control the log level with the `FABRO_LOG` environment variable:
```bash
FABRO_LOG=debug fabro run workflow.dot
FABRO_LOG=debug fabro run workflow.fabro
```
| Level | What's logged |

View file

@ -119,7 +119,7 @@ Retro: smooth — Successfully implemented the feature
To skip retro generation for a single run, pass `--no-retro`:
```bash
fabro run workflow.dot --no-retro
fabro run workflow.fabro --no-retro
```
To disable retros project-wide, set `retro = false` in your `fabro.toml`:

View file

@ -15,7 +15,7 @@ A run config requires two fields:
```toml title="run.toml"
version = 1
graph = "workflow.dot"
graph = "workflow.fabro"
goal = "Implement the login feature"
```
@ -32,7 +32,7 @@ Goal precedence: CLI `--goal` > TOML `goal` > DOT graph attribute.
```toml title="run.toml"
version = 1
goal = "Run the CI pipeline for $repo_name"
graph = "fabro/workflows/ci.dot"
graph = "fabro/workflows/ci.fabro"
directory = "/tmp/workdir"
[llm]
@ -286,7 +286,7 @@ language = "rust"
Variables can be used anywhere in the DOT file with `$name` syntax:
```dot title="c-i.dot"
```dot title="c-i.fabro"
digraph CI {
graph [goal="Run tests for $repo_name"]
clone [shape=parallelogram, script="git clone $repo_url repo"]
@ -352,6 +352,21 @@ draft = true
| `enabled` | When `true`, Fabro creates a PR from the agent's working branch after a successful run. Default: `false`. |
| `draft` | When `true`, the PR is created as a draft pull request. Default: `true`. |
### `[github]`
Request a scoped GitHub Installation Access Token and inject it into the sandbox as `GITHUB_TOKEN`. The token is minted from the configured [GitHub App](/integrations/github) with only the permissions you specify.
```toml title="run.toml"
[github]
permissions = { contents = "write", pull_requests = "read" }
```
| Field | Description |
|---|---|
| `permissions` | Map of GitHub API permission names to access levels (`"read"` or `"write"`). Only the listed permissions are requested. |
This requires a GitHub App to be configured. If the app is missing or the repository doesn't have an installation, the run logs a warning and continues without injecting the token.
### `[[hooks]]`
Define hooks that run in response to lifecycle events. Each hook is a TOML array entry:
@ -394,8 +409,8 @@ The `graph` path is resolved relative to the TOML file's parent directory, not t
```
project/
runs/
ci.toml # graph = "ci.dot"
ci.dot
ci.toml # graph = "ci.fabro"
ci.fabro
```
Absolute paths are used as-is.
@ -409,14 +424,37 @@ Settings can come from multiple sources. Fabro resolves them in this order (firs
| Node-level [stylesheet](/workflows/stylesheets) | Highest |
| Run config TOML | |
| CLI flags (`--model`, `--provider`, `--sandbox`) | |
| Project defaults (`fabro.toml`) | |
| Server defaults (`~/.fabro/server.toml`) | |
| DOT graph attributes (`default_model`, `default_provider`) | |
| Built-in defaults | Lowest |
<Note>
For model and provider specifically, the precedence is: CLI flags > TOML config > server defaults > DOT graph attributes > built-in defaults. Stylesheet rules on individual nodes always take priority over all of these.
For model and provider specifically, the precedence is: CLI flags > TOML config > project defaults > server defaults > DOT graph attributes > built-in defaults. Stylesheet rules on individual nodes always take priority over all of these.
</Note>
### Project defaults (`fabro.toml`)
The `fabro.toml` project config can set default values for `[llm]`, `[setup]`, `[sandbox]`, `[vars]`, `[checkpoint]`, `[pull_request]`, `[github]`, `[assets]`, `[[hooks]]`, and `[mcp_servers]`. These defaults apply to all runs in the project unless the run config overrides them:
```toml title="fabro.toml"
version = 1
[llm]
model = "claude-sonnet-4-5"
[sandbox]
provider = "daytona"
[sandbox.daytona.snapshot]
name = "my-project-snapshot"
[github]
permissions = { contents = "write" }
```
Project defaults are merged with run config values using the same rules as server defaults — run config wins on key collisions.
### Server defaults
When running via `fabro serve`, the server config at `~/.fabro/server.toml` can set default values for `[llm]`, `[setup]`, `[sandbox]`, and `[vars]`. These defaults are applied to every run unless the run config overrides them.

View file

@ -0,0 +1,180 @@
---
title: Comparison
description: How Fabro compares to AI coding agents, software factories, and orchestration platforms.
---
Fabro is a [dark software factory](/getting-started/dark-factory). It is not an IDE plugin. It is not a REPL, command line or otherwise. It is not a web appliction where you drag-and-drop workflows or update technical documents (we have Git for those).
Almost every other tool in AI coding starts from the same place: a developer at a keyboard, typing prompts, reviewing responses and outputs. Fabro starts from a different premise — that the highest-leverage work for expert engineers is defining **what** gets built and **how quality is verified**, not supervising each line of code as it's written.
This means Fabro intentionally does not include:
- **An IDE integration** — no VS Code extension, no editor plugins
- **A REPL CLI** — no interactive prompt-response loop
- **Autocomplete** — no inline code suggestions
Instead, Fabro provides workflow graphs, verification gates, multi-model orchestration, and observability — the infrastructure a small team needs to run coding agents with minimal human interaction.
## Notable Comparisons
### Automated Coding Workflows
These are the closest alternatives to Fabro — platforms that structure and automate multi-step coding processes rather than offering a single interactive agent session. They are proprietary products which offer a more "light" software factory approach where humans are still watching and driving.
- **Factory AI (Droids)** — Enterprise coding automation platform
- **Ona (Gitpod)** — Agentic coding platform with cloud dev environments
- **Devin** — Autonomous coding agent by Cognition
### AI Coding REPLs
AI coding REPLs like Claude Code, Codex CLI, and Cursor are interactive, prompt-driven agents designed for single-session tasks. They are powerful pair-programming tools but operate at a different level than Fabro — they lack declarative workflow definition, multi-stage orchestration, and Git-native checkpointing.
REPLs and Fabro pair nicely. REPLs are ideal for exploring ideas interactively — prototyping an approach, testing assumptions, and iterating in real time. When you're ready to move from exploration to implementation, you can hand the work off to a Fabro workflow for the build out.
## Other Comparisons
**8090 Software Factory**
8090.ai is an AI SDLC orchestration platform that structures upstream context — requirements, architecture, and planning — then delegates implementation to external coding agents via MCP. Because 8090 stops before code generation and does not perform coding activities itself, it operates in a different stage than Fabro and is not compared in the sections below.
**OpenAI Symphony**
[Symphony](https://github.com/openai/symphony) is a new multi-agent orchestration framework from OpenAI that dispatches Codex sessions from a Linear issue board. It is currently an engineering preview with a narrow, fixed workflow (poll → dispatch → resolve → land). Because it is early-stage and with a limited implementation, it is not compared in the sections below.
## Comparison Dimensions
<Note>
We strive to keep this information accurate, but the landscape changes rapidly. If you spot an inaccuracy, please [let us know](mailto:hello@fabro.sh).
</Note>
### Licensing
Open source vs. proprietary — trust, auditability, extensibility.
| Tool | License |
|---|---|
| **Fabro** | Open source, MIT license. Fork and customize. |
| **Factory AI** | Proprietary, closed source |
| **Devin** | Proprietary, closed source |
| **Ona** | Proprietary, closed source |
| **AI Coding REPLs** | Proprietary except Codex CLI (Apache 2.0) |
### Workflow Definition
How coding tasks are structured, repeated, and version-controlled.
| Tool | Approach |
|---|---|
| **Fabro** | Declarative, deterministic workflow graphs with loops, branching, and gates. Version controlled. |
| **Factory AI** | Non-deterministic Markdown skills (custom Droids) and black box Missions stored in a proprietary database. |
| **Devin** | One-off chat tasks or non-deterministic Markdown skills stored in a proprietary database. |
| **Ona** | One-off chat tasks or proprietary, web-based workflow builder. |
| **AI Coding REPLs** | Not applicable. Imperative prompts with Markdown skills. |
### Model Access
Access to models of various intelligence and costs across providers with per-step control.
| Tool | Models |
|---|---|
| **Fabro** | Multi-provider with ensembles. |
| **Factory AI** | Multi-provider with ensembles for Missions. |
| **Devin** | Proprietary, black box selection. |
| **Ona** | Multi-provider. One user selection per task. |
| **AI Coding REPLs** | Locked to REPL provider's ecosystem. |
### Human-in-the-Loop
How and where humans intervene in the workflow.
| Tool | Approach |
|---|---|
| **Fabro** | Deterministically defined human gates in the workflow, plus ad-hoc injected steering. |
| **Factory AI** | Plan before implementation, then review pull requests. |
| **Devin** | IDE-based mid-task chat intervention plus pull request review. |
| **Ona** | Humans review at the pull request boundary. |
| **AI Coding REPLs** | REPL prompts plus permission prompt modes. |
### Multi-Agent Orchestration
How multiple agents coordinate on complex work.
| Tool | Coordination |
|---|---|
| **Fabro** | Explicit graph of stages that fan out and converge. Deterministic composition of agents, prompts, commands, and gates. |
| **Factory AI** | Hierarchical subagents delegated from a parent Droid. Non-deterministic Missions for parallel feature workers. |
| **Devin** | Opaque internal compound system. Multiple instances run in parallel but users cannot define the orchestration. |
| **Ona** | Fleet of independent agents in isolated VMs. Horizontal scale but no agent-to-agent communication. |
| **AI Coding REPLs** | Limited. Session-scoped sub-agents or parallel background agents with no shared workflow definition. |
### Execution Environment
Where code runs and who controls the sandbox.
| Tool | Environment |
|---|---|
| **Fabro** | Local or BYO provider (Daytona, Sprites, exe.dev) and SSH support. |
| **Factory AI** | Local plus cloud VM configured with cloud templates. Enterprise supports air-gapped environments. |
| **Devin** | Proprietary cloud VM managed by Devin ("Devbox") or self hosted. |
| **Ona** | Proprietary cloud VMs managed by Ona (Gitpods) or self-hosted. |
| **AI Coding REPLs** | Local and proprietary cloud modes. |
### Cloud Sandbox Access
Human access to the running sandbox via preview URLs, SSH, and VNC.
| Tool | Access |
|---|---|
| **Fabro** | Live preview URLs, SSH, and VNC when using a supported sandbox provider (e.g. Daytona). |
| **Factory AI** | SSH access to cloud environments. No preview URLs or VNC. |
| **Devin** | Browser-based shell and VS Code in the Devbox. No direct SSH or VNC. |
| **Ona** | SSH access to cloud environments. No preview URLs or VNC. |
| **AI Coding REPLs** | None. |
### Git Checkpointing
How state is managed during execution.
| Tool | Git behavior |
|---|---|
| **Fabro** | Commits after every stage with provenance captured. Inspect, revert, or fork from any checkpoint. |
| **Factory AI** | Standard git operations. No per-step checkpointing. |
| **Devin** | Opens pull requests. No per-step git checkpointing. |
| **Ona** | Disposable VMs with pull request output. No per-step checkpointing. |
| **AI Coding REPLs** | None. |
### Quality Verification
How the system verifies that generated code meets requirements.
| Tool | Verification |
|---|---|
| **Fabro** | First class quality verification system (criteria, controls, evals) combining agent reviews, CI checks, and human approvals. |
| **Factory AI** | Sub-agent code reviews. PR-based human review. |
| **Devin** | Sub-agent code reviews. PR-based human review. |
| **Ona** | Human review at PR boundary. |
| **AI Coding REPLs** | Manual. User reviews output in the REPL or at the PR boundary. |
### Observability
Can you trace what happened step-by-step? Audit and debug agent behavior.
| Tool | Visibility |
|---|---|
| **Fabro** | Full trace of every model call, tool invocation, and decision. Cross-run comparison. |
| **Factory AI** | OpenTelemetry-native with dual-export and structured metrics. |
| **Devin** | Session replay with timeline and milestones. Enterprise audit logging. |
| **Ona** | Enterprise audit logging with real-time event streaming. |
| **AI Coding REPLs** | Varies. Limited to session-level transcripts or enterprise audit logs. |
### Deployment Model
SaaS dependency vs. self-hosted control.
| Tool | Deployment |
|---|---|
| **Fabro** | Self-hosted Rust single binary with no runtime dependencies. |
| **Factory AI** | SaaS with enterprise hybrid and air-gapped options. |
| **Devin** | SaaS only. Enterprise can host execution in customer VPC. |
| **Ona** | SaaS with enterprise self-hosted VPC option. |
| **AI Coding REPLs** | Local CLI tools with optional proprietary cloud modes. |

View file

@ -0,0 +1,70 @@
---
title: "Dark Factory"
description: "How Fabro helps small teams incrementally adopt a dark factory approach to software development"
---
The term "dark factory" comes from manufacturing. Since 2001, FANUC has operated a factory near Mt. Fuji where robots build other robots — running unsupervised for up to 30 days at a time. The factory is "dark" because no humans are present and robots don't need light.
In software, the dark factory concept is different. It doesn't mean zero human involvement — it means **minimal human interaction** with the code itself. Humans supervise the specs, guardrails, and outcomes, not each line of code. Engineers shift from writing and reviewing code to defining what should be built, how quality is measured, and when to intervene.
This is an aspirational concept, and getting there is iterative.
## From coding to orchestrating
Dan Shapiro's [five-level framework](https://www.danshapiro.com/blog/2026/01/the-five-levels-from-spicy-autocomplete-to-the-software-factory/) describes the progression from AI-assisted coding to autonomous software production:
| Level | Name | Human role |
|-------|------|------------|
| 0 | Spicy Autocomplete | Copy/paste from chat |
| 1 | Coding Intern | AI writes boilerplate; human reviews everything |
| 2 | Junior Developer | Pair programming with AI |
| 3 | Developer | Most code is AI-generated; human is a full-time reviewer |
| 4 | Engineering Team | Human manages specs and plans; agents do the work |
| 5 | Dark Software Factory | Specs go in, software comes out |
Most teams today operate at Level 23: AI writes code, humans review it line by line. The transition from Level 3 to Level 4 is the hardest — it requires replacing ad-hoc human review with structured, repeatable verification that you actually trust.
## What makes it work
The dark factory isn't a single tool or practice. It's a set of capabilities that compound:
**Declarative workflows over imperative prompts.** When the process is a version-controlled graph — not a chat transcript — you can review, iterate, and share it like any other source file. The workflow itself becomes the specification of how work gets done.
**Deterministic verification over human review.** Test suites, linters, type checkers, and LLM-as-judge evaluations replace line-by-line code review. Failures route back to fix loops automatically. Humans define the criteria; the system enforces them.
**Multi-model ensembles over single-model dependence.** Using different models for implementation and verification breaks the circularity problem — where the builder and inspector share the same blind spots. Cross-critique with fresh eyes catches what self-review misses.
**Checkpointed execution over black-box runs.** Git commits after every stage create an audit trail. When something goes wrong, you can inspect, revert, or fork from any point — without having watched the run live.
**Continuous improvement over static processes.** Automatic retrospectives after every run feed a learning loop. Workflows get better over time, not just the code they produce.
## The human role in a dark factory
The dark factory doesn't eliminate engineering judgment. It redirects it:
| Before | After |
|--------|-------|
| Writing code | Defining workflows and prompts |
| Reviewing diffs | Defining verification criteria |
| Debugging test failures | Designing fix loops |
| Watching agent sessions | Reviewing retrospectives |
| Manual quality checks | Tuning goal gates and evals |
The goal is to spend your time on the parts that require human judgment — what to build, how to verify it, and when something doesn't look right — while the factory handles the rest.
## Further reading
<Columns cols={2}>
<Card title="Workflows" icon="diagram-project" href="/core-concepts/workflows">
Learn how workflow graphs orchestrate agents, commands, and human gates.
</Card>
<Card title="Human-in-the-Loop" icon="hand" href="/workflows/human-in-the-loop">
Control where and how humans intervene in workflows.
</Card>
<Card title="Quality Verification" icon="shield-check" href="/workflows/best-practices">
Build verification into your workflows.
</Card>
<Card title="Retros" icon="magnifying-glass-chart" href="/execution/retros">
Automatic retrospectives for continuous improvement.
</Card>
</Columns>

View file

@ -1,6 +1,6 @@
---
title: "Introduction"
description: "Fabro is the open source software factory for small teams of expert engineers"
description: "Fabro is the open source, dark software factory for small teams of expert engineers"
---
Fabro replaces the prompt-wait-review loop with version-controlled workflow graphs that orchestrate AI agents, shell commands, and human decisions into repeatable, long-horizon coding processes.

View file

@ -7,7 +7,7 @@ description: "Get up and running with Fabro"
Fabro has two modes:
- **Standalone mode** — Run workflows directly from the CLI. This is what the quick start covers below.
- **Server mode** — An API server with a web UI for launching and managing workflow runs at scale. See [Server Mode](/core-concepts/server-mode) for details.
- **Server mode** — An API server with a web UI for launching and managing workflow runs at scale. See [Server Mode](/administration/deploy-server) for details.
</Note>
## Install

View file

@ -3,7 +3,7 @@ title: "Why Fabro?"
description: "The problems Fabro solves for AI-assisted software teams"
---
Fabro is the open source software factory for small teams of expert engineers. It replaces the prompt-wait-review loop with version-controlled workflow graphs that orchestrate AI agents, shell commands, and human decisions into repeatable, long-horizon coding processes.
Fabro is the open source, dark software factory for small teams of expert engineers. It replaces the prompt-wait-review loop with version-controlled workflow graphs that orchestrate AI agents, shell commands, and human decisions into repeatable, long-horizon coding processes.
## The problem
@ -52,7 +52,7 @@ Workflows are defined in Graphviz DOT, a simple graph description language. Here
<img src="/images/plan-implement-workflow.svg" alt="Plan-Implement workflow graph" />
</Frame>
```dot title="plan-implement.dot"
```dot title="plan-implement.fabro"
digraph PlanImplement {
graph [goal="Plan, approve, implement, and simplify a change"]

View file

@ -68,7 +68,7 @@ The `Interviewer` trait has a simple interface — `ask(question) → answer`
The default for CLI runs. On a TTY, the console interviewer uses interactive widgets (arrow-key selection, checkbox multi-select, confirm prompts) via `dialoguer`. When stdin is piped (non-TTY), it falls back to a line-based reader with numbered options.
```bash
fabro run workflow.dot
fabro run workflow.fabro
# At a human gate:
# ? Approve Plan
# [1] A - [A] Approve
@ -97,7 +97,7 @@ For fully automated runs or CI pipelines, the auto-approve interviewer answers e
Enable it with the `--auto-approve` flag:
```bash
fabro run workflow.dot --auto-approve
fabro run workflow.fabro --auto-approve
```
## Timeouts

View file

@ -29,7 +29,7 @@ fabro ssh <run-id> --ttl 120
Pass the `--ssh` flag to `fabro run` to create SSH credentials at the start of the run:
```bash
fabro run workflow.dot --sandbox daytona --ssh
fabro run workflow.fabro --sandbox daytona --ssh
```
After the sandbox is created, Fabro generates temporary SSH credentials (valid for 60 minutes) and prints the connection command:
@ -46,7 +46,7 @@ Copy and run the `ssh` command in a separate terminal to connect.
By default, Daytona sandboxes are destroyed when the workflow finishes. To keep the sandbox running after the workflow completes — so you can continue debugging — combine `--ssh` with `--preserve-sandbox`:
```bash
fabro run workflow.dot --sandbox daytona --ssh --preserve-sandbox
fabro run workflow.fabro --sandbox daytona --ssh --preserve-sandbox
```
Without `--preserve-sandbox`, the SSH session is terminated when the run ends and the sandbox is cleaned up.

View file

@ -19,7 +19,7 @@ VS Code remote access requires [SSH access](/human-tools/ssh-access), which is o
1. Start a workflow with SSH access and a preserved sandbox:
```bash
fabro run workflow.dot --sandbox daytona --ssh --preserve-sandbox
fabro run workflow.fabro --sandbox daytona --ssh --preserve-sandbox
```
2. Fabro prints the SSH connection command:

View file

@ -38,7 +38,7 @@
<polyline fill="none" stroke="#357f9e" points="71.5,-380 71.5,-374"/>
<polyline fill="none" stroke="#357f9e" points="77.5,-374 71.5,-374"/>
<text xml:space="preserve" text-anchor="middle" x="47" y="-363.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#1a1a1a">Workflow</text>
<text xml:space="preserve" text-anchor="middle" x="47" y="-351.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#1a1a1a">(.dot)</text>
<text xml:space="preserve" text-anchor="middle" x="47" y="-351.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#1a1a1a">(.fabro)</text>
</g>
<!-- parse -->
<g id="node4" class="node">

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.3 (20260303.0454)
-->
<!-- Title: PlanImplement Pages: 1 -->
<svg width="674pt" height="44pt"
viewBox="0.00 0.00 674.00 44.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
@media (prefers-color-scheme: dark) {
[fill="#1a1a1a"] { fill: #e5e5e5; }
[fill="#666666"] { fill: #aaaaaa; }
[stroke="#666666"] { stroke: #aaaaaa; }
[stroke="#999999"] { stroke: #777777; }
}
</style>
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 40.25)">
<title>PlanImplement</title>
<!-- start -->
<g id="node1" class="node">
<title>start</title>
<polygon fill="none" stroke="#357f9e" points="35.26,-36.12 0,-18.13 35.26,-0.13 70.52,-18.12 35.26,-36.12"/>
<polyline fill="none" stroke="#357f9e" points="10.69,-23.58 10.69,-12.67"/>
<polyline fill="none" stroke="#357f9e" points="24.57,-5.58 45.95,-5.58"/>
<polyline fill="none" stroke="#357f9e" points="59.83,-12.67 59.83,-23.58"/>
<polyline fill="none" stroke="#357f9e" points="45.95,-30.67 24.57,-30.67"/>
<text xml:space="preserve" text-anchor="middle" x="35.26" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Start</text>
</g>
<!-- plan -->
<g id="node3" class="node">
<title>plan</title>
<path fill="none" stroke="#357f9e" d="M149.52,-36.12C149.52,-36.12 119.52,-36.12 119.52,-36.12 113.52,-36.12 107.52,-30.12 107.52,-24.12 107.52,-24.12 107.52,-12.12 107.52,-12.12 107.52,-6.12 113.52,-0.12 119.52,-0.12 119.52,-0.12 149.52,-0.12 149.52,-0.12 155.52,-0.12 161.52,-6.12 161.52,-12.12 161.52,-12.12 161.52,-24.13 161.52,-24.13 161.52,-30.12 155.52,-36.12 149.52,-36.12"/>
<text xml:space="preserve" text-anchor="middle" x="134.52" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Plan</text>
</g>
<!-- start&#45;&gt;plan -->
<g id="edge1" class="edge">
<title>start&#45;&gt;plan</title>
<path fill="none" stroke="#666666" d="M71.37,-18.12C79.34,-18.12 87.84,-18.12 95.91,-18.12"/>
<polygon fill="#666666" stroke="#666666" points="95.8,-21.63 105.8,-18.13 95.8,-14.63 95.8,-21.63"/>
</g>
<!-- exit -->
<g id="node2" class="node">
<title>exit</title>
<polygon fill="none" stroke="#357f9e" points="666.18,-36.25 629.93,-36.25 629.93,0 666.18,0 666.18,-36.25"/>
<polyline fill="none" stroke="#357f9e" points="641.93,-36.25 629.93,-24.25"/>
<polyline fill="none" stroke="#357f9e" points="629.93,-12 641.93,0"/>
<polyline fill="none" stroke="#357f9e" points="654.18,0 666.18,-12"/>
<polyline fill="none" stroke="#357f9e" points="666.18,-24.25 654.18,-36.25"/>
<text xml:space="preserve" text-anchor="middle" x="648.05" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Exit</text>
</g>
<!-- approve -->
<g id="node4" class="node">
<title>approve</title>
<polygon fill="none" stroke="#357f9e" points="353.68,-18.12 322.33,-36.12 259.62,-36.12 228.27,-18.13 259.62,-0.13 322.33,-0.12 353.68,-18.12"/>
<text xml:space="preserve" text-anchor="middle" x="290.98" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Approve Plan</text>
</g>
<!-- plan&#45;&gt;approve -->
<g id="edge2" class="edge">
<title>plan&#45;&gt;approve</title>
<path fill="none" stroke="#666666" d="M161.72,-18.12C176.76,-18.12 196.47,-18.12 215.91,-18.12"/>
<polygon fill="#666666" stroke="#666666" points="215.77,-21.63 225.77,-18.13 215.77,-14.63 215.77,-21.63"/>
</g>
<!-- approve&#45;&gt;plan -->
<g id="edge4" class="edge">
<title>approve&#45;&gt;plan</title>
<path fill="none" stroke="#666666" d="M247.15,-6.91C226.65,-3 201.77,-0.36 179.52,-3.88 177.32,-4.22 175.08,-4.66 172.83,-5.17"/>
<polygon fill="#666666" stroke="#666666" points="171.93,-1.79 163.18,-7.77 173.75,-8.55 171.93,-1.79"/>
<text xml:space="preserve" text-anchor="middle" x="194.9" y="-5.62" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Revise</text>
</g>
<!-- implement -->
<g id="node5" class="node">
<title>implement</title>
<path fill="none" stroke="#357f9e" d="M485.18,-36.12C485.18,-36.12 437.68,-36.12 437.68,-36.12 431.68,-36.12 425.68,-30.12 425.68,-24.12 425.68,-24.12 425.68,-12.12 425.68,-12.12 425.68,-6.12 431.68,-0.12 437.68,-0.12 437.68,-0.12 485.18,-0.12 485.18,-0.12 491.18,-0.12 497.18,-6.12 497.18,-12.12 497.18,-12.12 497.18,-24.13 497.18,-24.13 497.18,-30.12 491.18,-36.12 485.18,-36.12"/>
<text xml:space="preserve" text-anchor="middle" x="461.43" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Implement</text>
</g>
<!-- approve&#45;&gt;implement -->
<g id="edge3" class="edge">
<title>approve&#45;&gt;implement</title>
<path fill="none" stroke="#666666" d="M354.22,-18.12C374,-18.12 395.51,-18.12 413.97,-18.12"/>
<polygon fill="#666666" stroke="#666666" points="413.81,-21.63 423.81,-18.13 413.81,-14.63 413.81,-21.63"/>
<text xml:space="preserve" text-anchor="middle" x="389.68" y="-19.88" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Approve</text>
</g>
<!-- simplify -->
<g id="node6" class="node">
<title>simplify</title>
<path fill="none" stroke="#357f9e" d="M580.93,-36.12C580.93,-36.12 546.18,-36.12 546.18,-36.12 540.18,-36.12 534.18,-30.12 534.18,-24.12 534.18,-24.12 534.18,-12.12 534.18,-12.12 534.18,-6.12 540.18,-0.12 546.18,-0.12 546.18,-0.12 580.93,-0.12 580.93,-0.12 586.93,-0.12 592.93,-6.12 592.93,-12.12 592.93,-12.12 592.93,-24.13 592.93,-24.13 592.93,-30.12 586.93,-36.12 580.93,-36.12"/>
<text xml:space="preserve" text-anchor="middle" x="563.55" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Simplify</text>
</g>
<!-- implement&#45;&gt;simplify -->
<g id="edge5" class="edge">
<title>implement&#45;&gt;simplify</title>
<path fill="none" stroke="#666666" d="M497.45,-18.12C505.5,-18.12 514.12,-18.12 522.37,-18.12"/>
<polygon fill="#666666" stroke="#666666" points="522.17,-21.63 532.17,-18.13 522.17,-14.63 522.17,-21.63"/>
</g>
<!-- simplify&#45;&gt;exit -->
<g id="edge6" class="edge">
<title>simplify&#45;&gt;exit</title>
<path fill="none" stroke="#666666" d="M593.41,-18.12C601.43,-18.12 610.14,-18.12 618.17,-18.12"/>
<polygon fill="#666666" stroke="#666666" points="618.13,-21.63 628.13,-18.13 618.13,-14.63 618.13,-21.63"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

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