Compare commits

...

186 commits
main ... v0.6.0

Author SHA1 Message Date
Bryan Helmkamp
3951464325
Bump version to 0.6.0 2026-03-16 23:38:20 -04:00
brynary-fabro[bot]
0530605fdb
Add -q/--quiet to fabro ps (#64)
Adds a `-q`/`--quiet` flag to `fabro ps`, mirroring the behavior of
`docker ps -q`. When specified, the command outputs only full run IDs,
one per line, with no headers, footers, or "no runs found"
messages—empty output simply means no matching runs exist.

The quiet flag takes precedence over JSON output, and it composes
naturally with other flags like `-a` (e.g., `fabro ps -qa` lists all run
IDs).

### Fabro Details

<details>
<summary>Ran 10 stages in 10m 13s for $1.09</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.17 | 0 |
| simplify_opus | 0s | $0.30 | 0 |
| simplify_gemini | 0s | $0.30 | 0 |
| simplify_gpt | 0s | $0.32 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **10m 13s** | **$1.09** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 22:00:20 -04:00
brynary-fabro[bot]
9197895f10
Add Sentry panic reporting to fabro CLI (#35)
This PR adds Sentry-based panic reporting to the fabro CLI and improves
the reliability of all background telemetry senders. When the CLI
panics, a Sentry event is serialized to a temp file and uploaded by a
fully detached subprocess, giving visibility into crashes that would
otherwise go unnoticed.

The core infrastructure change is a new `spawn_detached` utility that
uses the double-fork pattern on Unix (fork → setsid → close_fd → fork →
exec) to ensure background subprocesses survive parent exit and terminal
close. This replaces the previous simple `Command::spawn()` approach
used by the analytics sender, which was unreliable since the child could
be killed when the parent exits. Both the analytics sender
(`__send_analytics`) and the new panic sender (`__send_panic`) now share
this `spawn_fabro_subcommand` helper.

The panic module installs a hook early in `main()` that captures panic
info, builds a Sentry event with exception details, stacktrace, and OS
context, then spawns a detached `fabro __send_panic` subprocess to
upload it. It respects the telemetry level setting (no-ops when off),
prevents recursion by setting `FABRO_TELEMETRY=off` in the child,
filters benign "Broken pipe" panics from `| head` usage, and uses a
compile-time `SENTRY_DSN` so dev builds without the DSN set are
unaffected.

### Fabro Details

<details>
<summary>Ran 10 stages in 32m 34s for $8.13</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $2.35 | 0 |
| simplify_opus | 0s | $2.23 | 0 |
| simplify_gemini | 0s | $1.75 | 0 |
| simplify_gpt | 0s | $1.80 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **32m 34s** | **$8.13** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 22:00:11 -04:00
brynary-fabro[bot]
9d0a2a9a66
Move sandbox lifecycle into the engine (#22)
This PR moves sandbox lifecycle management (initialization, setup
commands, devcontainer phases, and cleanup) from the CLI's `run_command`
god function into the workflow engine. Two new engine methods are
introduced: `run_with_lifecycle()` orchestrates sandbox init, fires the
`SandboxReady` hook (now blocking by default), emits a new
`SandboxInitialized` event, handles remote git setup, runs setup
commands and devcontainer lifecycle phases, then delegates to the
existing `run_internal()` graph execution. `cleanup_sandbox()` fires the
`SandboxCleanup` hook and optionally tears down the sandbox. Both
`SandboxReady` and `SandboxCleanup` hook events were previously defined
but never fired — they now fire naturally within the engine alongside
all other hooks.

The CLI is simplified significantly: sandbox record persistence and
progress UI updates are handled via an event listener for
`SandboxInitialized` rather than inline code. The `run_from_branch`
resume path also benefits, gaining hook support and proper cleanup for
free. A new `LifecycleConfig` struct captures setup commands, timeouts,
and devcontainer phases, keeping the engine's API clean. The existing
`run()` method is unchanged, so API server and integration tests
continue working with pre-initialized sandboxes.

The `setup_remote_git` helper is moved from `cli/run.rs` into
`engine.rs` since it only depends on sandbox exec. Config is now passed
by mutable reference to `run_with_lifecycle` so the engine can fill in
remote git fields (base SHA, run branch) that downstream code needs.
Comprehensive tests verify event emission ordering, setup command
execution/failure, and cleanup behavior.

### Fabro Details

<details>
<summary>Ran 10 stages in 53m 27s for $14.42</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $6.22 | 0 |
| simplify_opus | 0s | $2.63 | 0 |
| simplify_gemini | 0s | $2.73 | 0 |
| simplify_gpt | 0s | $2.84 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **53m 27s** | **$14.42** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</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-16 22:00:05 -04:00
brynary-fabro[bot]
74dae0a342
Refactor: Collapse pull_request fields on RunConfig into Option<PullRequestConfig> (#21)
This refactoring collapses four flat `pull_request_*` fields on
`RunConfig` (`pull_request_enabled`, `pull_request_draft`,
`pull_request_auto_merge`, `pull_request_merge_strategy`) into a single
`pull_request: Option<PullRequestConfig>` field. This better represents
the natural tree structure of the configuration: `None` means PR
creation is disabled, while `Some(config)` carries all PR settings
directly.

The PR creation logic in `run.rs` is restructured to use `if let
Some(ref pr_cfg) = config.pull_request` instead of checking a boolean
flag, and construction is simplified to filter out disabled configs at
build time via `.filter(|p| p.enabled).cloned()`. All test constructors
across `engine.rs`, `integration.rs`, `daytona_integration.rs`,
`server.rs`, and `manager_loop.rs` are updated from two lines
(`pull_request_enabled: false, pull_request_draft: false/true`) to a
single `pull_request: None`.

### Fabro Details

<details>
<summary>Ran 10 stages in 20m 32s for $6.62</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.71 | 0 |
| simplify_opus | 0s | $1.63 | 0 |
| simplify_gemini | 0s | $1.74 | 0 |
| simplify_gpt | 0s | $1.55 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **20m 32s** | **$6.62** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</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-16 21:55:41 -04:00
brynary-fabro[bot]
469b5d4670
Fix: Stage durations always show 0s in PR descriptions (#20)
This PR fixes a bug where stage durations in PR descriptions always
displayed `0s`. The root cause was a key mismatch in
`extract_stage_durations()`: the function was building its HashMap using
`node_label` (human-readable names like `"Preflight Compile"`), but all
three lookup sites were querying by `node_id` (DOT graph identifiers
like `"preflight_compile"`). Every lookup missed and fell back to the
default value of `0`.

The fix changes the HashMap key from `node_label` to `node_id` so that
it matches what the callers actually use for lookups. Additionally, the
existing test is updated so that `node_label` and `node_id` values
differ from each other (e.g., `"plan"` vs `"Plan"`), ensuring the test
would have caught this mismatch before the fix rather than silently
passing due to both fields being identical.

### Fabro Details

<details>
<summary>Ran 10 stages in 7m 46s for $1.28</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.21 | 0 |
| simplify_opus | 0s | $0.35 | 0 |
| simplify_gemini | 0s | $0.35 | 0 |
| simplify_gpt | 0s | $0.38 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **7m 46s** | **$1.28** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 21:39:01 -04:00
brynary-fabro[bot]
d29c1d817e
Wire up missing hook invocations (#19)
This PR wires up three `HookEvent` variants—`StageRetrying`,
`ParallelStart`, and `ParallelComplete`—that were defined in the enum
and documented but never actually invoked by the engine. `StageRetrying`
hooks now fire at both retry sites in `execute_with_retry` (error-retry
and explicit-Retry-status paths) immediately before the backoff sleep.
`ParallelStart` and `ParallelComplete` hooks fire in the parallel
handler after their corresponding event emissions, using a new
`EngineServices::run_hooks()` convenience method since the handler
doesn't have direct access to the engine's hook method.

The two remaining unwired events, `SandboxReady` and `SandboxCleanup`,
are marked as reserved with doc comments on the enum variants and
annotated in the docs table, since wiring them requires sandbox
lifecycle changes outside the engine's scope. A small
`HookContext::set_node()` helper is introduced to reduce repeated field
assignment across all hook call sites, and existing
`StageStart`/`StageComplete` hook calls are refactored to use it.

### Fabro Details

<details>
<summary>Ran 10 stages in 25m 6s for $5.60</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.04 | 0 |
| simplify_opus | 0s | $1.51 | 0 |
| simplify_gemini | 0s | $1.44 | 0 |
| simplify_gpt | 0s | $1.62 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **25m 6s** | **$5.60** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 21:38:53 -04:00
brynary-fabro[bot]
50c032576d
Style SVG output from fabro graph and API (#18)
This PR styles the SVG output from `fabro graph` and the API graph
endpoints to match the polished look of the documentation SVGs. It
introduces two internal functions in `graph.rs`:
`inject_dot_style_defaults`, which inserts DOT-level defaults
(transparent background, teal `#357f9e` node strokes, gray `#666666`
edges, Helvetica font) after the first `{` in any DOT source, and
`postprocess_svg`, which removes the Graphviz-generated white background
polygon and injects a `<style>` block with `prefers-color-scheme: dark`
media queries for automatic dark mode support.

The `render_dot` function is updated to apply both transformations (DOT
defaults before rendering, SVG post-processing after), and is made `pub`
so the API layer can reuse it. Both `get_graph` in `server.rs` and
`get_run_graph` in `demo/mod.rs` are simplified from ~30 lines of inline
Graphviz process management down to a single call to a shared
`render_dot_svg` helper that delegates to `render_dot` on a blocking
thread.

Six new unit tests validate the styling pipeline: default injection with
and without braces, white background removal, dark mode style insertion,
and the existing SVG integration test is extended to assert styled
output. PNG output is unaffected by the SVG post-processing step.

### Fabro Details

<details>
<summary>Ran 10 stages in 23m 31s for $5.48</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.15 | 0 |
| simplify_opus | 0s | $1.55 | 0 |
| simplify_gemini | 0s | $1.65 | 0 |
| simplify_gpt | 0s | $1.13 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **23m 31s** | **$5.48** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</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-16 21:38:47 -04:00
Bryan Helmkamp
c7c6d2999a
Remove AI slop from introducing-fabro blog post
Replace "leveraging" buzzword and "This is the key insight:" trope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:38:14 -04:00
Bryan Helmkamp
09ee73e1a0
Hide Showcase from homepage navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:34:18 -04:00
Bryan Helmkamp
5de39fc398
Update marketing site fonts to Razor Geometric treatment
Switch from Space Grotesk / DM Sans / JetBrains Mono to
Outfit / Lexend / Fira Code for a tighter, sharper aesthetic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:22:14 -04:00
Bryan Helmkamp
f375073355
Rewrite "Introducing Fabro" blog post with founder voice
Expanded from a 1-min stub to a full introductory post with
problem framing, workflow graph example, model stylesheet syntax,
verification gates, checkpoint/resume, and install CTA.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:21:24 -04:00
Bryan Helmkamp
f8dc3446a4
Hide Showcase from marketing site navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:19:01 -04:00
Bryan Helmkamp
3e34ad5d78
Record GPT-5.4 20min timeout: 65.7% on SWE-Bench-Lite ($718.62)
Best resolve rate at 197/300 but 3.3x more expensive than Opus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 20:55:59 -04:00
Bryan Helmkamp
f382f2764f
Showcase: render workflow graphs as SVGs, add nav, fix prompt
- Render DOT workflow definitions as visual SVG diagrams at build time
  using @viz-js/viz, replacing raw code blocks on show pages and
  placeholder first-letter thumbnails on index cards
- Collapse models/skills/languages into a compact metadata strip on
  show pages instead of separate boxed sections
- Fix prompt expand/collapse to use a single DOM element with max-height
  animation instead of duplicating the text in two swapped containers
- Add prev/next navigation links at the bottom of show pages
- Extract duplicated langIcons data into shared src/lib/langIcons.ts
- Use varied reveal animation types (reveal-scale, reveal-left)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 20:55:31 -04:00
Bryan Helmkamp
f416643a48
Update roadmap and standardize on Graphviz terminology
Roadmap: replace placeholder items with current shipped/building/planned
features. Use real dates for sorting instead of manual sortOrder. Fix
UTC timezone rendering for date display.

Terminology: replace all standalone "DOT" references with "Graphviz" or
"Graphviz DOT" across docs, marketing, README, AGENTS.md, and OpenAPI
spec. Changelogs left unchanged as historical records.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 20:01:33 -04:00
Bryan Helmkamp
b116c07ea7
Update /discord redirect to use invite link
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 19:50:29 -04:00
Bryan Helmkamp
f80f52920b
Add Showcase section to marketing site
Gallery of workflow recipes with index grid and detail pages.
Three sample entries: PR Review Bot, Test Generator, Docs Sync.
Add Roadmap link to shared Nav component and homepage nav.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 19:11:05 -04:00
Bryan Helmkamp
dcad7059d4
Docs: add auto-merge config, auto-merge to GitHub features, turn-level retries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 19:06:15 -04:00
Bryan Helmkamp
610aff7446
Add March 16 changelog and update March 15 with new entries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 19:03:47 -04:00
Bryan Helmkamp
aa8ff368df
Roadmap: replace inline shipped descriptions with hover tooltip
Saves vertical space by showing only date + title in shipped rows,
with an info icon that reveals the description on hover.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:27:58 -04:00
Bryan Helmkamp
124894d0eb
Add blog and redesign blog/roadmap pages
- Add blog collection (content config, prose styles, introducing-fabro post)
- Add Blog link to homepage nav and footer
- Make Layout description prop dynamic for per-page meta/OG tags
- Extract shared Nav, Footer, PageScripts components from duplicated markup
- Blog index: compact header, featured card for latest post, row list for older posts
- Blog post: clean reading surface (no grid/noise overlay), reading time, better header
- Roadmap: improved card contrast with tinted backgrounds, conditional section rendering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:26:10 -04:00
Bryan Helmkamp
f13310dfc4
Reduce default patch generation concurrency to 75
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 16:05:24 -04:00
Bryan Helmkamp
1af5193a82
Handle assistant output replay on stream retries 2026-03-16 15:59:00 -04:00
Bryan Helmkamp
335da2318d
Fix typos in docs: "appliction" and "an Fabro"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:57:33 -04:00
Bryan Helmkamp
b907896907
Fix broken link and stale badge label in README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:25:31 -04:00
Bryan Helmkamp
76dc5f72a4
Add turn-level retry for stream-ended-without-Finish errors
When an LLM stream drops mid-response (e.g. under high concurrency with
OpenAI), retry the same turn up to 3 times instead of failing the entire
agent session. Conversation history is preserved across retries.

Previously this killed the whole stage and restarted from scratch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:16:35 -04:00
Bryan Helmkamp
b18521058a
Record Opus 4.6 20min timeout: 58.0% on SWE-Bench-Lite ($218.65)
100% patch rate, 0 timeouts. Only +1 instance over Sonnet at 4x the cost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 14:43:20 -04:00
Bryan Helmkamp
cf77d2b19f
Update install instructions to show Claude Code, Codex, and Bash methods
Match the marketing website's tabbed install widget across README, docs
quick-start, and CLAUDE.md. Add marketing site build/deploy commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 14:03:53 -04:00
Bryan Helmkamp
29cf519e99
Add tabbed install widget (Claude/Codex/Bash) with copy buttons, show hero screenshot on load
Replace single curl command with a tabbed install widget defaulting to Claude,
with Codex and Bash alternatives. Each tab has a copy-to-clipboard button.
Remove scroll-reveal animation from hero screenshot so it's visible immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:56:42 -04:00
Bryan Helmkamp
7799d59514
Record Sonnet 4.6 20min timeout: 57.7% on SWE-Bench-Lite ($55.22)
20min timeout vs 10min: 173 vs 167 resolved (+6), patch rate 99% vs 94%.
Also fixes: revert to v4 snapshots, concurrency default to 100, preflight
uses actual 4 CPU per sandbox.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:36:26 -04:00
Bryan Helmkamp
9c011cc722
Move install files to apps/marketing/public, symlink from repo root
Vercel deploys only the apps/marketing/ subtree, so the real files
need to live there. Repo root now symlinks into marketing/public.
Also add .vercel to gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:23:58 -04:00
Bryan Helmkamp
fec037c546
Temporarily hide Roadmap from nav while content is being written
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:51:55 -04:00
Bryan Helmkamp
08b74863fa
Improve marketing site mobile layout: reduce hero top spacing and hide curl command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:46:22 -04:00
Bryan Helmkamp
4cd9e301ea
Fix snapshot CPU mismatch: use v4 snapshots (4 CPU), reduce concurrency to 100
Daytona bakes CPU/memory at snapshot creation time. v4 snapshots have
4 CPU / 8 GB. Preflight now checks against 4 CPU per sandbox.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:44:14 -04:00
Bryan Helmkamp
33a2d32fae
Reduce default concurrency to 150, increase default timeout to 20min
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:32:18 -04:00
Bryan Helmkamp
684f4070e0
Add llms.txt and additional meta tags for marketing site
- Add llms.txt with structured overview of Fabro docs for LLM consumption
- Add canonical link, application-name, apple-mobile-web-app-title
- Add twitter:image dimensions
- Make og:url dynamic per page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:30:16 -04:00
Bryan Helmkamp
fe6ce15c20
Add OG image and meta tags for link sharing, remove comma from tagline
- Add 1200x630 branded OG image matching Mintlify docs card style
- Add Open Graph and Twitter Card meta tags to Layout.astro
- Save og-image-template.html for easy regeneration
- Remove comma from "open source, dark software factory" everywhere

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:24:58 -04:00
Bryan Helmkamp
1a258242bd
Fix marketing site mobile layout: responsive nav and content overflow
Nav links, icons, and CTA button overflowed the viewport on mobile.
Text in Workflow-as-Code and Multi-model sections was clipped because
wide SVG/pre children caused CSS grid blowout (min-width: auto default).

- Add hamburger menu for mobile nav on both pages (hidden md:, toggle JS)
- Add overflow-x-hidden to html/body/main to prevent horizontal scroll
- Add .grid > * { min-width: 0 } to prevent grid children from expanding
  beyond their container

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:23:35 -04:00
Bryan Helmkamp
8101f948fc
Add Daytona CPU preflight check before starting eval runs
Checks running sandboxes against the 500 CPU org limit with 20% buffer.
Exits with a suggested --max-workers value if capacity is insufficient.
Default concurrency set to 200 (safe with 2 CPU per sandbox).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:20:36 -04:00
Bryan Helmkamp
6c596fe027
Migrate roadmap data to Astro content collections
Replace inline arrays with a `roadmap` collection using glob loader
and Zod schema. Each item is a YAML file in src/content/roadmap/ with
title, description, status, date, and sortOrder fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:17:31 -04:00
Bryan Helmkamp
6f5b5daeba
Add REPL Handoff example workflow to docs
Documents the pattern of planning interactively in Claude Code and
delegating implementation to Fabro via the /fabro-implement slash
command, with multi-model simplification and verification gates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:13:05 -04:00
Bryan Helmkamp
571dac721d
Add /roadmap page to marketing site with timeline layout
Shipped/Building/Next sections with sample content, vertical timeline,
scroll reveal animations, and matching dark factory aesthetic. Linked
from top nav and footer on both pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:04:48 -04:00
Bryan Helmkamp
322be8a0b8
Update marketing site: copyright to Qlty Software Inc. and allow ngrok hosts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:58:29 -04:00
Bryan Helmkamp
b45483ae4a
Record timeout and sandbox resources in scoreboard metadata
Adds --timeout, --sandbox-cpu, --sandbox-memory flags to record_results.py.
Re-recorded both existing runs with the new fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:24:56 -04:00
Bryan Helmkamp
002468316d
Record Sonnet 4.6 baseline: 55.7% on SWE-Bench-Lite ($39.78)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:23:23 -04:00
Bryan Helmkamp
951a37faf8
Add install.md for AI agent-driven installation
Self-contained installation instructions following the install.md spec.
Decoupled from install.sh — handles platform detection, binary download,
PATH setup in shell dotfiles, and verification independently. Prompts
the user to run `fabro install` interactively to complete setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:07:21 -04:00
Bryan Helmkamp
811f10f586
Skip shell config modification in non-interactive mode
Prevents install.sh from silently writing to dotfiles (.zshrc, .bashrc,
config.fish) when run non-interactively (e.g. by an AI coding agent).
In non-interactive mode, it now prints the manual PATH export instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:01:42 -04:00
Bryan Helmkamp
4245ab86a4
Add status.py for quick progress checks on generation and eval runs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:59:49 -04:00
Bryan Helmkamp
4520fd2001
Add Discord and Changelog links to marketing site nav and footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:56:10 -04:00
Bryan Helmkamp
10ba327aad
Switch display font from Barlow Condensed to Space Grotesk
Replace aggressive uppercase/wide-tracking headings with sentence-case
tight-tracking for a more natural, geometric-techy feel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:50:53 -04:00
Bryan Helmkamp
28dd481ed2
Redesign marketing site with industrial-refined visual identity
Replace Sora with Barlow Condensed uppercase headings, add cross-hatch grid
and noise atmosphere, swap emoji for custom SVG line-art icons, add scroll
animation variants (reveal-left/right/scale), animated trace bars and workflow
graph draw-in, convert images to WebP with picture fallbacks, expand footer
to 3-column layout, and consolidate sections from 12 to 8.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:48:52 -04:00
Bryan Helmkamp
dfddba87f6
Add best-effort Daytona sandbox cleanup on timeout and disable PR creation
On timeout, finds the orphaned sandbox via fabro ps --label and deletes
it. Non-fatal if cleanup fails. Also adds [pull_request] enabled=false
to generated workflow.toml configs to prevent eval runs from opening PRs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:42:47 -04:00
Bryan Helmkamp
0e9892b4e7
Add install.sh serving and /discord, /docs redirects to marketing site
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:33:25 -04:00
Bryan Helmkamp
e152bbfd37
Make SWE-bench patch generation and evaluation resumable
On restart, reads existing output JSONL files to find completed instance
IDs, skips them, and appends new results. Final summary recomputes from
the full results file so it reflects all runs combined.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:33:06 -04:00
Bryan Helmkamp
f1dab99e4a
Add screenshots, workflow diagram, lightbox, and fix logotype viewBox
- Add runs-board.png below hero as product showcase
- Add plan-implement.svg workflow diagram in Workflow-as-Code section
  above the DOT code block, with dark-mode contrast fix
- Replace verification 2x2 card grid with run-detail.png screenshot
- Add click-to-expand lightbox for all three visual assets
- Fix logotype SVG viewBox (0 0 1455 → 0 0 1500) across all 5 files
  to prevent "O" in FABRO from being clipped
- Increase Docs link contrast in nav (text-ice-100, font-medium)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:09:57 -04:00
Bryan Helmkamp
e3790ecff3
Add SWE-bench eval README with setup, usage, and monitoring instructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:59:03 -04:00
Bryan Helmkamp
c248aaf92e
Add scoreboard system and record Haiku 4.5 baseline: 54.0% on SWE-Bench-Lite
record_results.py combines generation + eval results into a git-tracked
scoreboard. Per run: README, meta.json, instances.jsonl. Auto-generates
leaderboard.json ranked by resolve rate.

Haiku 4.5 baseline: 162/300 (54.0%), $26.13 total ($0.087/instance).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:56:43 -04:00
Bryan Helmkamp
30049c9ed8
Use full logotype in nav/footer and improve header layout
Replace symbol+text logo with full FABRO logotype SVG in both nav and
footer. Move Docs link to left side next to logo. Replace GitHub text
link with GitHub SVG icon on the right side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:54:59 -04:00
Bryan Helmkamp
ddc1ff7112
Update marketing website with Fabro branding and current product content
Replace Arc logo/favicon with Fabro isometric symbol, update hero tagline
to "dark software factory", add install command, and rewrite all sections
to match current README and docs: use cases, key features (workflow graphs,
human-in-the-loop, multi-model routing, cloud sandboxes, git checkpointing,
retros), workflow-as-code example, CLI showcase, and sandbox section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:52:55 -04:00
Bryan Helmkamp
5c503565aa
Add Daytona-based SWE-bench evaluator and bump default concurrency to 100
evaluate_daytona.py runs the swebench test harness on Daytona sandboxes
instead of local Docker. Reuses the same snapshots from the generation
phase. Applies model patch + test patch, runs tests, grades with
swebench's log parsers. No local Docker needed.

Also bumps default --max-workers from 20 to 100 in run_eval.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:13:09 -04:00
Bryan Helmkamp
fdf7b3eb23
Fix run_eval.py: resolve output-dir to absolute path
When --output-dir is relative and fabro runs from /tmp, generated
workflow.toml paths were unresolvable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:39:24 -04:00
Bryan Helmkamp
b4ae9d8566
Add SWE-bench evaluation harness
Python scripts for running SWE-bench Lite evals against Fabro agent
in Daytona sandboxes: instance orchestration, Dockerfile generation,
and result evaluation via the official swebench harness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:37:24 -04:00
Bryan Helmkamp
430218ee4b
add background brand asset 2026-03-16 08:37:24 -04:00
Bryan Helmkamp
f39b37da64
Add auto-merge support for pull requests via GitHub GraphQL API
When `auto_merge = true` is set in `[pull_request]` config, Fabro enables
GitHub's auto-merge on created PRs using the `enablePullRequestAutoMerge`
GraphQL mutation. Auto-merge implies `draft = false` since GitHub doesn't
allow auto-merge on draft PRs. A `merge_strategy` field (squash/merge/rebase,
default squash) controls the merge method. Failures to enable auto-merge
(e.g. repo doesn't have the setting enabled) warn but don't fail the run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:37:24 -04:00
brynary-fabro[bot]
470fcfe120
Fix: Sub-agent file writes not tracked in API backend (#17)
This PR fixes a bug where files written by sub-agents were missing from
`outcome.files_touched`, causing downstream pipeline nodes like
`simplify_opus` to receive incomplete file lists. The root cause was
that `spawn_event_forwarder` only matched top-level
`ToolCallStarted`/`ToolCallCompleted` events, while sub-agent tool calls
arrived wrapped in `AgentEvent::SubAgentEvent` and fell through to the
`_ => {}` catch-all.

The fix extracts the file-tracking logic into a standalone
`track_file_event` function that recursively unwraps `SubAgentEvent`
layers before matching on the inner tool call events. This handles
arbitrarily nested sub-agent hierarchies (sub-sub-agents, etc.). The
three separate `Arc<Mutex<...>>` fields for pending calls, touched
files, and last file are consolidated into a single `FileTracking`
struct behind one lock, simplifying the forwarder signature and reducing
lock contention.

Four new unit tests verify the behavior: top-level write tracking,
single-level sub-agent unwrapping, double-nested sub-sub-agent
unwrapping, and proper cleanup of pending entries on tool call errors.

### Fabro Details

<details>
<summary>Ran 10 stages in 22m 1s for $4.18</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.91 | 0 |
| simplify_opus | 0s | $0.89 | 0 |
| simplify_gemini | 0s | $1.44 | 0 |
| simplify_gpt | 0s | $0.94 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **22m 1s** | **$4.18** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 23:18:40 -04:00
brynary-fabro[bot]
cb5b7bc3a7
Limit command stdout/stderr to last N lines in preamble (#16)
This PR limits command stdout/stderr output in the preamble to the last
N lines, reducing token waste from verbose build progress and download
noise. Command nodes like `cargo check` or `cargo clippy` can produce
300+ lines of output, but the useful content (errors, summaries) is
almost always at the tail. Compact and summary:medium fidelity levels
now keep the last 25 lines, while summary:high keeps the last 50 lines.
Truncated output displays an `(N lines omitted)` indicator consistent
with the existing omission pattern used for stages.

The implementation adds a `tail_lines` helper that skips leading lines
beyond the limit, prepends an omission notice, and applies the
appropriate indentation to each retained line. This is applied only to
inline stdout/stderr rendering—artifact pointer branches and other
fidelity levels (summary:low, truncate, full) are intentionally
untouched. The PR includes unit tests for the `tail_lines` helper itself
as well as integration tests verifying truncation behavior at compact
and summary:high fidelity, and confirming artifact pointers remain
untruncated.

### Fabro Details

<details>
<summary>Ran 10 stages in 18m 4s for $3.45</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.84 | 0 |
| simplify_opus | 0s | $0.95 | 0 |
| simplify_gemini | 0s | $0.81 | 0 |
| simplify_gpt | 0s | $0.85 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **18m 4s** | **$3.45** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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 -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -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. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 23:18:13 -04:00
brynary-fabro[bot]
2972e38bed
Add --direction flag to fabro graph (#15)
This PR adds a `--direction` (`-d`) flag to the `fabro graph` CLI
command, bringing it to parity with the web UI's LR/TB toggle buttons.
Users can now pass `--direction lr` or `--direction tb` to override the
`rankdir` declared in the DOT source before it's handed off to the `dot`
renderer.

The implementation introduces a `GraphDirection` enum (with `ValueEnum`
and `Display` derives for clap integration), an optional `--direction`
field on `GraphArgs`, and an `apply_direction` helper that uses a
lazy-compiled regex to rewrite `rankdir=…` in the DOT source—mirroring
the same approach used by the web UI. The CLI docs are updated with the
new flag, and two unit tests verify that the rewrite works correctly and
that omitting the flag leaves the source unchanged.

### Fabro Details

<details>
<summary>Ran 10 stages in 17m 6s for $3.60</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.68 | 0 |
| simplify_opus | 0s | $1.02 | 0 |
| simplify_gemini | 0s | $0.83 | 0 |
| simplify_gpt | 0s | $1.06 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **17m 6s** | **$3.60** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
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_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    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]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    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_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 23:18:05 -04:00
Bryan Helmkamp
b101ad78a2
Enable pull_request by default in fabro init
The generated fabro.toml now includes an uncommented [pull_request]
section with enabled=true and draft=true, so new projects auto-create
draft PRs on successful workflow runs out of the box.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:53:57 -04:00
Bryan Helmkamp
0ab27e40e6
Improve hooks docs: multi-layer config, clearer matchers table, sandbox context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:52:25 -04:00
Bryan Helmkamp
420c0874a0
Enable auto PR creation and add debug logging for skipped PR cases
The `[pull_request]` config in fabro.toml was missing `enabled = true`,
so workflow runs silently skipped PR creation. Additionally, four skip
paths in the PR creation logic had no logging at all, making it hard to
diagnose why a PR wasn't opened. Added debug-level logs for: config not
enabled, dry-run mode, engine error, and non-success run status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:51:52 -04:00
Bryan Helmkamp
e5e3d2b240
Update smoke workflow to use quiet clippy and nextest
Match the implement workflow commands: cargo clippy -q and
cargo nextest run --cargo-quiet --status-level fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:49:56 -04:00
Bryan Helmkamp
5ed947764d
Add cargo-nextest and quiet workflow commands
Install cargo-nextest in the sandbox Dockerfile and switch the
implement workflow to use -q/--workspace flags on cargo check/clippy
and cargo nextest with --status-level fail for less verbose output.
Bump snapshot to fabro-v6.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:47:11 -04:00
Bryan Helmkamp
8734936c1d
Add TDD instruction to implement workflow prompt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:17:07 -04:00
Bryan Helmkamp
edcbee5f5e
Fix agent SDK docs: correct tool names and AnthropicProfile constructor
- Tool names are shell/read_file/write_file/edit_file/glob/grep/web_fetch/web_search, not Bash/Read/Write/Edit etc.
- AnthropicProfile::new takes only model, not (model, config)
- Add missing web_search to tool list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:09:16 -04:00
Bryan Helmkamp
3f62afdfbf
Add fabro-agent SDK docs alongside existing fabro-llm reference
Restructure the SDK reference page to cover both crates. The page now
opens with an overview of Fabro's two Rust SDK entry points, followed
by full fabro-agent documentation (Session, SessionConfig, Sandbox,
provider profiles, events, tool hooks, error handling) and the existing
fabro-llm reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:06:07 -04:00
Bryan Helmkamp
51797f0e01
Add multi-model simplify stages to implement workflow
Run the simplify prompt sequentially through Opus, Gemini, and GPT-54
so each model reviews the implementation independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:02:11 -04:00
Bryan Helmkamp
46293f868c
Fix SDK docs: correct failover flags, StreamEvent fields, GenerateResult fields, adapter constructors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:00:53 -04:00
Bryan Helmkamp
e043762605
Add Fabro SDK reference page documenting the fabro-llm crate public API
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:56:08 -04:00
Bryan Helmkamp
b604bc4b60
Designate retros as experimental, disable by default via [features] flag
Move retro control from [fabro] retro to [features] retros in project
config. Default changes from true to false — retros are now opt-in.
Add retros field to server config Features struct, OpenAPI spec,
TypeScript client, and web app config. Update docs with experimental
warning and new enablement instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:36:32 -04:00
Bryan Helmkamp
ec9057a4c6
Add cargo-fmt post_tool_use hook and improve matcher docs
Add a blocking cargo-fmt hook to fabro.toml that auto-formats Rust
files after write_file, edit_file, or apply_patch tool calls. Improve
the hooks documentation with a detailed matcher field reference table,
tool name catalog, cross-field matching caveat, and additional examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:34:23 -04:00
Bryan Helmkamp
bd195175fd
Docs updates: reorder nav, rename DOT Language, simplify dark factory, add fork/upgrade/asset docs
- Move Comparison link below Troubleshooting in nav
- Rename "DOT Language" page to "Fabro Language"
- Remove five-tier table from dark factory page, keep link to Dan Shapiro's post
- Add fork command docs and checkpoints section
- Add upgrade, asset list, asset cp command docs
- Add upgrade_check config reference
- Add retros feature flag to server config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:13:23 -04:00
Bryan Helmkamp
49379f08d0
Regenerate March 15 changelog with fork, upgrade, and asset commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:59:30 -04:00
Bryan Helmkamp
4a37087953
Open workflow PRs as non-draft by default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:56:17 -04:00
brynary-fabro[bot]
da3610af59
Unified dry-run via Handler::simulate() (#14)
This PR introduces a unified dry-run mechanism by adding a
`Handler::simulate()` trait method and a `dispatch_handler()` routing
function that selects between `simulate()` and `execute()` based on
`services.dry_run`. Previously, dry-run behavior was scattered
inconsistently across handlers—`CommandHandler` checked
`services.dry_run` inline, `AgentHandler`/`PromptHandler`/`FanInHandler`
relied on the backend being `None`, and `WaitHandler`/`HumanHandler` had
no dry-run support at all (sleeping or blocking on input for real). This
made dry-run behavior fragile and difficult to extend to new handlers.

The new design adds an `Outcome::simulated(node_id)` factory for
standardized dry-run results, a default `simulate()` implementation on
the `Handler` trait that returns a generic simulated success, and
per-handler overrides where custom context updates are needed.
`CommandHandler` populates empty output/stderr, `AgentHandler` and
`PromptHandler` set simulated
`last_stage`/`last_response`/`response.{id}` context keys,
`FanInHandler` calls `heuristic_select()` without an LLM, `HumanHandler`
auto-selects the first choice, and `ParallelHandler` dispatches child
branches through `dispatch_handler()` while skipping git worktree
operations. The inline `dry_run` check in `CommandHandler::execute()` is
removed, and both call sites in the engine (`execute_with_retry` and
parallel branch dispatch) now route through `dispatch_handler()`.

All existing dry-run tests are updated to test `simulate()` directly,
and new tests verify that `dispatch_handler()` correctly routes based on
the `dry_run` flag and that each handler's `simulate()` produces the
expected context updates and outcome structure.

### Fabro Details

<details>
<summary>Ran 7 stages in 24m 57s for $4.72</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $3.29 | 0 |
| simplify | 0s | $1.42 | 0 |
| verify | 0s | – | 0 |
| **Total** | **24m 57s** | **$4.72** | **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: Fabro Assistant <assistant@fabro.dev>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:55:03 -04:00
brynary-fabro[bot]
7ce7a842cc
fabro upgrade command (#13)
This PR adds a `fabro upgrade` command that downloads and installs new
releases from GitHub, along with a passive daily auto-check that
notifies users when a newer version is available. The upgrade flow
supports two download backends: the `gh` CLI (preferred, for auth and
rate-limit benefits) with an automatic fallback to plain HTTPS via
`reqwest` when `gh` is missing or not authenticated. The command
includes SHA256 checksum verification, atomic binary replacement with
rollback on failure, downgrade protection with interactive confirmation,
and `--dry-run`/`--force` flags.

A background upgrade check runs automatically on common commands (`run`,
`exec`, `init`, `install`), caching results in
`~/.fabro/last_upgrade_check.json` to avoid hitting GitHub more than
once per 24 hours. Users can disable this via `upgrade_check = false` in
`~/.fabro/cli.toml` or the `--no-upgrade-check` global flag. The check
is spawned as an async task and its notice prints to stderr after the
main command completes, ensuring it never blocks or breaks normal
operation—all errors are silently swallowed.

The implementation follows a test-first approach with unit tests
covering platform detection, version parsing, SHA256 verification,
upgrade check state serialization/staleness, and the new `upgrade_check`
config field. Dependencies `tempfile` (promoted from dev-dependencies)
and `sha2` are added to `fabro-cli`.

### Fabro Details

<details>
<summary>Ran 7 stages in 18m 39s for $5.61</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $2.92 | 0 |
| simplify | 0s | $2.68 | 0 |
| verify | 0s | – | 0 |
| **Total** | **18m 39s** | **$5.61** | **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>
2026-03-15 19:54:57 -04:00
brynary-fabro[bot]
0d7ae73857
Random Edge Selection (#12)
This PR introduces a `selection="random"` node attribute that enables
weighted-random tiebreaking when choosing among candidate outgoing
edges. The existing deterministic behavior (highest weight, then lexical
node ID) remains the default. The cascade priority—conditions →
preferred label → suggested next → unconditional → fallback—is
unchanged; randomness only replaces the final pick-one-from-candidates
step within each tier. A new `weighted_random` function handles the
sampling, treating edges with weight ≤ 0 as weight 1, while a
`pick_edge` dispatcher routes to either the random or deterministic
strategy based on the node's `selection()` accessor.

A validation rule (`RandomSelectionNoConditionsRule`) rejects nodes that
combine `selection="random"` with conditional edges, since condition
evaluation order would conflict with random selection. A companion rule
(`SelectionValidRule`) warns on unrecognized selection values. Both are
registered as built-in lint rules with appropriate error/warning
severities and actionable fix suggestions.

Documentation is updated in the transitions guide with a new "Random
selection" section explaining the behavior and constraints, and the DOT
language reference gains a `selection` row in the node attributes table.
All changes were developed following red/green TDD cycles with
comprehensive test coverage for the accessor, weighted random sampling,
edge selection integration, and both validation rules.

### Fabro Details

<details>
<summary>Ran 7 stages in 17m 1s for $4.07</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $2.43 | 0 |
| simplify | 0s | $1.63 | 0 |
| verify | 0s | – | 0 |
| **Total** | **17m 1s** | **$4.07** | **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>
2026-03-15 19:54:48 -04:00
brynary-fabro[bot]
bcfd16b833
Rename [feature_flags] to [features] (#11)
This PR renames the `[feature_flags]` configuration section to
`[features]` and the corresponding `FeatureFlags` type to `Features`
across the entire stack. The change touches the OpenAPI spec (source of
truth), Rust config/API crates, the generated TypeScript API client, the
web application, and the demo config file to ensure consistency.

On the Rust side, the `FeatureFlags` struct becomes `Features` in
`fabro-config`, and all field references (`config.feature_flags` →
`config.features`) are updated in `fabro-api` demo/test code along with
the relevant unit tests. On the TypeScript side, the generated client
reflects the OpenAPI rename (`feature-flags.ts` → `features.ts`,
`FeatureFlags` → `Features`), and manual edits in `fabro-web` update the
`AppConfig` interface, defaults constant (`FEATURES_DEFAULTS`), loader
data destructuring, and template usage throughout `config.server.ts`,
`app-shell.tsx`, and `start.tsx`.

The regeneration also picked up an unrelated new `GitHubConfiguration`
schema that was already present in the OpenAPI spec but hadn't been
generated yet, resulting in the new `git-hub-configuration.ts` file and
its addition to the server configuration type.

### Fabro Details

<details>
<summary>Ran 7 stages in 15m 4s for $3.08</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $2.18 | 0 |
| simplify | 0s | $0.90 | 0 |
| verify | 0s | – | 0 |
| **Total** | **15m 4s** | **$3.08** | **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 19:54:40 -04:00
brynary-fabro[bot]
d90a805f1a
fabro asset CLI subcommands (#10)
This PR adds `fabro asset list` and `fabro asset cp` CLI subcommands for
inspecting and copying run artifacts (screenshots, test reports, traces)
that are collected to `~/.fabro/runs/{id}/artifacts/assets/`.
Previously, users had to manually browse the filesystem to find these
files.

The core implementation lives in a new `asset.rs` module that provides
`scan_assets()`, which walks the asset manifest files under each node's
retry directories to build structured `AssetEntry` records. The `list`
subcommand displays a formatted table (or JSON with `--json`) showing
node, retry number, size, and path for each asset. The `cp` subcommand
supports copying all assets or a specific file (via `RUN_ID:path`
syntax), with a `--tree` flag to preserve the `{node}/retry_{N}/`
directory structure and collision detection in flat mode.

The implementation reuses existing utilities throughout:
`split_run_path()` for parsing the colon-separated source syntax (made
`pub(crate)`), `resolve_run()` for run ID prefix matching,
`format_size()` for human-readable byte formatting (also made
`pub(crate)`), and `AssetCollectionSummary` for manifest
deserialization.

### Fabro Details

<details>
<summary>Ran 7 stages in 12m 31s for $3.61</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.33 | 0 |
| simplify | 0s | $2.28 | 0 |
| verify | 0s | – | 0 |
| **Total** | **12m 31s** | **$3.61** | **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 19:54:31 -04:00
brynary-fabro[bot]
872ead311d
fabro fork subcommand (#9)
This PR adds a new `fabro fork` subcommand that creates a new run
branching from an existing run at a specific checkpoint, without
modifying the original run. This is a non-destructive alternative to
`fabro rewind` — instead of moving branch refs backward and losing later
checkpoint history, fork preserves the source run entirely and creates
fresh run and metadata branches for the new run.

The implementation heavily reuses existing infrastructure from
`rewind.rs` (timeline building, target resolution, parallel map loading,
prefix-based run ID lookup) and follows the same CLI patterns. The core
`execute_fork` function generates a new ULID, creates a run branch ref
pointing at the target checkpoint's commit, then builds a new metadata
branch containing an updated manifest (with new run ID and branch name),
the original graph, and the checkpoint state from the target commit. It
supports the same target syntax as rewind (`@N`, `node_name`,
`node_name@N`), defaults to the latest checkpoint when no target is
specified, and optionally pushes new branches to the remote.

The PR also makes `load_parallel_map` public in `rewind.rs` so fork can
reuse it, and includes five tests covering run branch creation, metadata
branch correctness, preservation of the original run, default-to-latest
behavior, and forking at a specific ordinal.

### Fabro Details

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

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.94 | 0 |
| simplify | 0s | $2.44 | 0 |
| verify | 0s | – | 0 |
| **Total** | **15m 15s** | **$4.39** | **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 19:54:24 -04:00
brynary-fabro[bot]
54dcb35080
Add safeguards to asset collection and checkpoint commits (#8)
This PR adds safeguards to asset collection and checkpoint commits to
prevent collecting or committing excessive files from large, untracked
directories like Python virtual environments, build outputs, and tool
caches.

Specifically, it introduces a `MAX_FILE_COUNT` limit of 100 files in
`select_files_to_collect()` to cap asset collection regardless of total
size budget, expands the `EXCLUDE_DIRS` list with seven new entries
(`.venv`, `venv`, `.cache`, `.tox`, `.pytest_cache`, `.mypy_cache`,
`dist`) to match common project directory patterns that can contain
thousands of generated files, and makes the constant public for reuse.
Notably, `build` and `env`/`.env` were intentionally omitted as too
generic or potentially conflicting with legitimate project files.

The checkpoint commit logic in `git_checkpoint()` is updated to always
apply the built-in `EXCLUDE_DIRS` as git pathspec excludes (converted to
`**/dirname/**` glob format), merged with any user-configured exclude
globs. This ensures that even with no user configuration, checkpoint
`git add -A` commands won't inadvertently stage virtual environments,
caches, or build artifacts. All changes are covered by new tests
following red/green TDD.

### Fabro Details

<details>
<summary>Ran 7 stages in 10m 34s for $2.41</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.21 | 0 |
| simplify | 0s | $1.19 | 0 |
| verify | 0s | – | 0 |
| **Total** | **10m 34s** | **$2.41** | **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>
2026-03-15 19:54:17 -04:00
Bryan Helmkamp
882b4f35e9
Add cargo fmt step to implement workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:40:02 -04:00
Bryan Helmkamp
6ab2802654
Update web app icons with new Fabro logo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:38:20 -04:00
Bryan Helmkamp
742f02bb26
Update release script to regenerate Cargo.lock after version bump
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:50:43 -04:00
Bryan Helmkamp
6dbaa256d4
Cargo.lock 2026-03-15 18:49:46 -04:00
Bryan Helmkamp
250f45e047
Switch GHA workflows from self-hosted runners to GitHub runners
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:40:53 -04:00
Bryan Helmkamp
2affe99537
Use version as tag message in release script to skip editor prompt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:35:05 -04:00
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
637 changed files with 24044 additions and 5796 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
aa8ff368df36fc17a0799cd8b09297f85457aaa2

View file

@ -1 +1 @@
542adbf1e1b0a9d6b3149ae25a097addf3bfb61a
610aff744609268ae879a58acfe6c9a6bb0572e0

View file

@ -29,54 +29,44 @@ permissions:
contents: read
env:
CARGO_TERM_COLOR: always # trigger CI
CARGO_TERM_COLOR: always
jobs:
fmt:
name: Format
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- name: Add cargo bin to PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
clean: false
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- run: cargo fmt --check --all
clippy:
name: Clippy
runs-on: self-hosted
env:
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /home/bhelmkamp/.cache/sccache
runs-on: ubuntu-latest
steps:
- name: Add cargo bin to PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
clean: false
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
cache-on-failure: true
- run: cargo clippy --workspace -- -D warnings
- name: Show sccache stats
run: sccache --show-stats
test:
name: Test (Linux)
runs-on: self-hosted
env:
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /home/bhelmkamp/.cache/sccache
runs-on: ubuntu-latest
steps:
- name: Add cargo bin to PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
clean: false
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
cache-on-failure: true
- run: cargo test --workspace
- name: Show sccache stats
run: sccache --show-stats
test-macos:
name: Test (macOS)

View file

@ -29,7 +29,7 @@ permissions:
jobs:
typecheck:
name: Typecheck
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
@ -40,7 +40,7 @@ jobs:
test:
name: Test
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
@ -51,7 +51,7 @@ jobs:
build:
name: Build
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:

5
.gitignore vendored
View file

@ -3,3 +3,8 @@ target
.entire
node_modules
tmp
evals/swe-bench/repos/
evals/swe-bench/results/
evals/swe-bench/dockerfiles/
__pycache__
.vercel

View file

@ -18,6 +18,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `cd apps/fabro-web && bun run typecheck` — type check
- `cd apps/fabro-web && bun run build` — production build
### Marketing site (apps/marketing)
- `cd apps/marketing && bun run dev` — start Astro dev server
- `cd apps/marketing && bun run build` — production build
- `cd apps/marketing && vercel --prod` — deploy to Vercel (project: website, domain: fabro.sh)
### Dev servers
1. `fabro serve` — starts the Rust API server (demo mode is per-request via `X-Fabro-Demo: 1` header)
2. `cd apps/fabro-web && bun run dev` — starts the React dev server
@ -40,11 +45,11 @@ The OpenAPI spec at `docs/api-reference/fabro-api.yaml` is the source of truth f
## Architecture
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as DOT graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
### Rust crates (`lib/crates/`)
- **fabro-cli** — CLI entry point. Commands: `run`, `exec`, `serve`, `validate`, `parse`, `cp`, `model`, `doctor`, `init`, `install`, `ps`, `system prune`, `llm`
- **fabro-workflows** — Core workflow engine. Parses DOT graphs, runs stages, manages checkpoints/resume, hooks, retros, and human-in-the-loop interactions
- **fabro-workflows** — Core workflow engine. Parses Graphviz graphs, runs stages, manages checkpoints/resume, hooks, retros, and human-in-the-loop interactions
- **fabro-agent** — AI coding agent with tool use (Bash, Read, Write, Edit, Glob, Grep, WebFetch). `Sandbox` trait abstracts execution environments
- **fabro-api** — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header
- **fabro-exe** — SSH-based sandbox implementation (`ExeSandbox`)
@ -65,7 +70,7 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as
### Key design patterns
- **Sandbox trait** — Uniform interface for local, Docker, SSH (ExeSandbox), Sprites, and Daytona execution environments
- **DOT graph workflows** — Stages and transitions defined as DOT graph attributes
- **Graphviz graph workflows** — Stages and transitions defined as Graphviz graph attributes
- **OpenAPI-first**`fabro-api.yaml` drives both Rust type generation (typify) and TypeScript client generation (openapi-generator)
- **Checkpoint/resume** — Workflows can be paused, checkpointed, and resumed
@ -82,5 +87,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

524
Cargo.lock generated
View file

@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
@ -299,6 +314,21 @@ dependencies = [
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-link",
]
[[package]]
name = "base64"
version = "0.22.1"
@ -344,6 +374,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "bollard"
version = "0.18.1"
@ -516,6 +555,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"
@ -908,6 +957,16 @@ dependencies = [
"url",
]
[[package]]
name = "debugid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid",
]
[[package]]
name = "der"
version = "0.7.10"
@ -1016,6 +1075,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@ -1093,6 +1162,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno"
version = "0.3.14"
@ -1103,6 +1183,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "etcetera"
version = "0.8.0"
@ -1135,9 +1225,19 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "exec"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
dependencies = [
"errno 0.2.8",
"libc",
]
[[package]]
name = "fabro-agent"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"anyhow",
"async-trait",
@ -1170,7 +1270,7 @@ dependencies = [
[[package]]
name = "fabro-api"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"anyhow",
"axum",
@ -1218,9 +1318,18 @@ dependencies = [
"x509-parser",
]
[[package]]
name = "fabro-beastie"
version = "0.6.0"
dependencies = [
"core-foundation 0.9.4",
"libc",
"tracing",
]
[[package]]
name = "fabro-cli"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"anyhow",
"assert_cmd",
@ -1235,6 +1344,7 @@ dependencies = [
"dotenvy",
"fabro-agent",
"fabro-api",
"fabro-beastie",
"fabro-config",
"fabro-github",
"fabro-llm",
@ -1247,6 +1357,7 @@ dependencies = [
"indicatif",
"insta",
"jsonwebtoken",
"libc",
"open",
"predicates",
"rand 0.8.5",
@ -1257,6 +1368,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"sha2",
"tempfile",
"tokio",
"toml",
@ -1264,17 +1376,19 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"trycmd",
"ulid",
"x509-parser",
]
[[package]]
name = "fabro-config"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"anyhow",
"dirs",
"fabro-agent",
"fabro-mcp",
"fabro-util",
"fabro-workflows",
"serde",
"tempfile",
@ -1284,7 +1398,7 @@ dependencies = [
[[package]]
name = "fabro-db"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"chrono",
"sqlx",
@ -1295,7 +1409,7 @@ dependencies = [
[[package]]
name = "fabro-devcontainer"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"insta",
"reqwest 0.12.28",
@ -1310,7 +1424,7 @@ dependencies = [
[[package]]
name = "fabro-exe"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"async-trait",
"base64",
@ -1329,7 +1443,7 @@ dependencies = [
[[package]]
name = "fabro-git-storage"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"git2",
"tempfile",
@ -1340,7 +1454,7 @@ dependencies = [
[[package]]
name = "fabro-github"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"async-trait",
"base64",
@ -1357,7 +1471,7 @@ dependencies = [
[[package]]
name = "fabro-linear"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"async-trait",
"fabro-tracker",
@ -1371,13 +1485,14 @@ dependencies = [
[[package]]
name = "fabro-llm"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"anyhow",
"async-trait",
"base64",
"bytes",
"clap",
"cli-table",
"dialoguer",
"dotenvy",
"fabro-util",
@ -1399,7 +1514,7 @@ dependencies = [
[[package]]
name = "fabro-mcp"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"anyhow",
"futures",
@ -1413,7 +1528,7 @@ dependencies = [
[[package]]
name = "fabro-openai-oauth"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"axum",
"base64",
@ -1431,7 +1546,7 @@ dependencies = [
[[package]]
name = "fabro-slack"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"fabro-workflows",
"futures-util",
@ -1449,7 +1564,7 @@ dependencies = [
[[package]]
name = "fabro-sprites"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"async-trait",
"base64",
@ -1466,7 +1581,7 @@ dependencies = [
[[package]]
name = "fabro-ssh"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"async-trait",
"base64",
@ -1484,14 +1599,14 @@ dependencies = [
[[package]]
name = "fabro-tracker"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"async-trait",
]
[[package]]
name = "fabro-types"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"chrono",
"prettyplease",
@ -1506,7 +1621,7 @@ dependencies = [
[[package]]
name = "fabro-util"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"aho-corasick",
"anyhow",
@ -1514,12 +1629,15 @@ dependencies = [
"chrono",
"console 0.15.11",
"dirs",
"exec",
"fork",
"git2",
"insta",
"mac_address",
"md5",
"regex",
"reqwest 0.12.28",
"sentry",
"serde",
"serde_json",
"tempfile",
@ -1533,7 +1651,7 @@ dependencies = [
[[package]]
name = "fabro-workflows"
version = "0.4.0"
version = "0.6.0"
dependencies = [
"anyhow",
"assert_cmd",
@ -1541,6 +1659,7 @@ dependencies = [
"base64",
"chrono",
"clap",
"cli-table",
"console 0.15.11",
"daytona-api-client",
"daytona-sdk",
@ -1681,6 +1800,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "fork"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05dc8b302e04a1c27f4fe694439ef0f29779ca4edc205b7b58f00db04e29656d"
dependencies = [
"libc",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@ -1871,6 +1999,12 @@ dependencies = [
"wasip3",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "git2"
version = "0.20.4"
@ -2011,6 +2145,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "hostname"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
dependencies = [
"cfg-if",
"libc",
"windows-link",
]
[[package]]
name = "htmd"
version = "0.5.0"
@ -2838,6 +2983,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.1.1"
@ -2911,6 +3065,18 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nix"
version = "0.31.2"
@ -3050,6 +3216,174 @@ dependencies = [
"libm",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
dependencies = [
"bitflags",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-data"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
"dispatch2",
"objc2",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags",
"dispatch2",
"objc2",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-location"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [
"bitflags",
"block2",
"objc2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-core-location",
"objc2-core-text",
"objc2-foundation",
"objc2-quartz-core",
"objc2-user-notifications",
]
[[package]]
name = "objc2-user-notifications"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "oid-registry"
version = "0.7.1"
@ -3163,6 +3497,22 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_info"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224"
dependencies = [
"android_system_properties",
"log",
"nix 0.30.1",
"objc2",
"objc2-foundation",
"objc2-ui-kit",
"serde",
"windows-sys 0.61.2",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
@ -3501,9 +3851,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",
@ -3740,6 +4090,7 @@ dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
"h2",
@ -3906,6 +4257,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustc-demangle"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]]
name = "rustc-hash"
version = "2.1.1"
@ -3937,7 +4294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"errno 0.3.14",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
@ -4156,6 +4513,91 @@ version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "sentry"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016958f51b96861dead7c1e02290f138411d05e94fad175c8636a835dee6e51e"
dependencies = [
"httpdate",
"reqwest 0.12.28",
"rustls",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-tracing",
"ureq",
"webpki-roots 0.26.11",
]
[[package]]
name = "sentry-backtrace"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57712c24e99252ef175b4b06c485294f10ad6bc5b5e1567ff3803ee7a0b7d3f"
dependencies = [
"backtrace",
"once_cell",
"regex",
"sentry-core",
]
[[package]]
name = "sentry-contexts"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eba8754ec3b9279e00aa6d64916f211d44202370a1699afde1db2c16cbada089"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core",
"uname",
]
[[package]]
name = "sentry-core"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8b6dcd4fbae1e3e22b447f32670360b27e31b62ab040f7fb04e0f80c04d92"
dependencies = [
"once_cell",
"rand 0.8.5",
"sentry-types",
"serde",
"serde_json",
]
[[package]]
name = "sentry-tracing"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "263f73c757ed7915d3e1e34625eae18cad498a95b4261603d4ce3f87b159a6f0"
dependencies = [
"sentry-backtrace",
"sentry-core",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "sentry-types"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a71ed3a389948a6a6d92b98e997a2723ca22f09660c5a7b7388ecd509a70a527"
dependencies = [
"debugid",
"hex",
"rand 0.8.5",
"serde",
"serde_json",
"thiserror 1.0.69",
"time",
"url",
"uuid",
]
[[package]]
name = "serde"
version = "1.0.228"
@ -4393,7 +4835,7 @@ version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"errno 0.3.14",
"libc",
]
@ -4873,6 +5315,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"
@ -5377,6 +5828,15 @@ dependencies = [
"web-time",
]
[[package]]
name = "uname"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
dependencies = [
"libc",
]
[[package]]
name = "unicase"
version = "2.9.0"
@ -5464,6 +5924,21 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64",
"log",
"once_cell",
"rustls",
"rustls-pki-types",
"url",
"webpki-roots 0.26.11",
]
[[package]]
name = "url"
version = "2.5.8"
@ -5474,6 +5949,7 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
"serde_derive",
]
[[package]]

View file

@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
version = "0.4.0"
version = "0.6.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"
@ -58,11 +59,17 @@ futures-util = "0.3"
openssh = "0.11"
daytona-sdk = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "06033ca", package = "daytona-sdk" }
daytona-api-client = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "06033ca", package = "daytona-api-client" }
sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] }
fork = "0.2"
exec = "0.3"
[profile.release]
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,15 +1,22 @@
<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-fabro)
[![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.sh-357F9E)](https://docs.fabro.sh)
```bash
# With Claude Code
curl -fsSL https://fabro.sh/install.md | claude
# With Codex
codex "$(curl -fsSL https://fabro.sh/install.md)"
# With Bash
curl -fsSL https://fabro.sh/install.sh | bash
```
@ -53,6 +60,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 +87,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 [Graphviz 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
---
@ -99,14 +108,14 @@ Fabro ships with [comprehensive documentation](https://fabro.dev) covering every
### Install
```bash
# With Claude Code
curl -fsSL https://fabro.sh/install.md | claude
# With Codex
codex "$(curl -fsSL https://fabro.sh/install.md)"
# With Bash
curl -fsSL https://fabro.sh/install.sh | bash
# Initialize your project
cd my-repo/
fabro init
# Run your first workflow
fabro run hello
```
---

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

@ -42,13 +42,13 @@ export async function loader({ request }: Route.LoaderArgs) {
const { provider } = config.web.auth;
const demoMode = isDemoMode(request);
if (provider === "insecure_disabled") {
return { user: DEMO_USER, demoMode, feature_flags: config.feature_flags };
return { user: DEMO_USER, demoMode, features: config.features };
}
if (provider === "github" && !isGitHubAppConfigured()) {
throw redirect("/setup");
}
const user = await requireUser(request);
return { user, provider, demoMode, feature_flags: config.feature_flags };
return { user, provider, demoMode, features: config.features };
}
export async function action({ request }: Route.ActionArgs) {

View file

@ -20,8 +20,9 @@ interface GitConfig {
slug: string | null;
}
interface FeatureFlags {
interface Features {
session_sandboxes: boolean;
retros: boolean;
}
interface WebConfig {
@ -33,7 +34,7 @@ interface AppConfig {
web: WebConfig;
api: ApiConfig;
git: GitConfig;
feature_flags: FeatureFlags;
features: Features;
}
const AUTH_DEFAULTS: AuthConfig = {
@ -58,8 +59,9 @@ const GIT_DEFAULTS: GitConfig = {
slug: null,
};
const FEATURE_FLAGS_DEFAULTS: FeatureFlags = {
const FEATURES_DEFAULTS: Features = {
session_sandboxes: false,
retros: false,
};
export const FABRO_CONFIG_PATH = join(homedir(), ".fabro", "server.toml");
@ -78,7 +80,7 @@ function loadAppConfig(): AppConfig {
const rawWebAuth = (rawWeb.auth ?? {}) as Partial<AuthConfig>;
const rawApi = (raw.api ?? {}) as Partial<ApiConfig>;
const rawGit = (raw.git ?? {}) as Partial<GitConfig>;
const rawFeatureFlags = (raw.feature_flags ?? {}) as Partial<FeatureFlags>;
const rawFeatures = (raw.features ?? {}) as Partial<Features>;
const demo = process.env.FABRO_DEMO === "1";
@ -94,7 +96,7 @@ function loadAppConfig(): AppConfig {
? { ...API_DEFAULTS, ...rawApi, authentication_strategy: "insecure_disabled" }
: { ...API_DEFAULTS, ...rawApi },
git: { ...GIT_DEFAULTS, ...rawGit },
feature_flags: { ...FEATURE_FLAGS_DEFAULTS, ...rawFeatureFlags },
features: { ...FEATURES_DEFAULTS, ...rawFeatures },
};
}

View file

@ -33,12 +33,12 @@ export function meta({}: Route.MetaArgs) {
}
export async function loader({ request }: Route.LoaderArgs) {
const { feature_flags } = getAppConfig();
const { features } = getAppConfig();
const { data: apiSessions } = await apiJson<PaginatedSessionList>("/sessions", { request });
const sessionGroups = groupSessionsByDate(
apiSessions.map((s) => ({ id: s.id, title: s.title, created_at: s.created_at }))
);
return { sessionGroups, feature_flags };
return { sessionGroups, features };
}
const projects = [
@ -99,7 +99,7 @@ function SessionSidebar({ groups }: { groups: { label: string; sessions: { id: s
}
export default function Start({ loaderData }: Route.ComponentProps) {
const { sessionGroups, feature_flags } = loaderData;
const { sessionGroups, features } = loaderData;
const [prompt, setPrompt] = useState("");
const [project, setProject] = useState(projects[0]);
const [branch, setBranch] = useState(branches[0]);
@ -158,7 +158,7 @@ export default function Start({ loaderData }: Route.ComponentProps) {
/>
<div className="absolute bottom-3 inset-x-3 flex items-center justify-between">
{feature_flags.session_sandboxes && (
{features.session_sandboxes && (
<div className="flex items-center gap-1.5">
<Picker
value={project}

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: {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -1,3 +1,4 @@
dist/
.astro/
node_modules/
.vercel

View file

@ -4,7 +4,21 @@ import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
integrations: [react()],
redirects: {
"/discord": {
status: 302,
destination:
"https://discord.gg/KE6w49Vg",
},
"/docs": {
status: 302,
destination: "https://docs.fabro.sh",
},
},
vite: {
plugins: [tailwindcss()],
server: {
allowedHosts: [".ngrok-free.app"],
},
},
});

View file

@ -0,0 +1,673 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fabro — Font Treatment Comparison</title>
<!-- Current fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&family=Lexend:wght@400;500;600;700&family=Fira+Code:wght@400;500&family=Instrument+Serif:ital@0;1&family=Instrument+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:wght@400;500&family=Syne:wght@400;500;600;700;800&family=Manrope:wght@400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet" />
<style>
:root {
--navy-950: #0F1729;
--navy-900: #141C2F;
--navy-800: #252C3D;
--navy-600: #4B5768;
--teal-300: #B5DDEF;
--teal-500: #67B2D7;
--teal-700: #357F9E;
--ice-50: #F7F9FB;
--ice-100: #E8EDF3;
--ice-300: #A8B5C5;
--mint: #5AC8A8;
--amber: #F0A45B;
--coral: #E86B6B;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--navy-950);
color: var(--ice-50);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ── Page header ── */
.page-header {
text-align: center;
padding: 48px 24px 24px;
border-bottom: 1px solid var(--navy-800);
}
.page-header h1 {
font-family: 'Space Grotesk', sans-serif;
font-size: 28px;
font-weight: 600;
color: var(--ice-50);
margin-bottom: 8px;
}
.page-header p {
font-family: 'DM Sans', sans-serif;
font-size: 15px;
color: var(--ice-300);
}
/* ── Selector bar ── */
.selector-bar {
position: sticky;
top: 0;
z-index: 50;
display: flex;
justify-content: center;
gap: 6px;
padding: 16px 24px;
background: var(--navy-950);
border-bottom: 1px solid var(--navy-800);
backdrop-filter: blur(16px);
}
.selector-btn {
font-family: 'DM Sans', sans-serif;
font-size: 13px;
font-weight: 600;
padding: 8px 20px;
border-radius: 8px;
border: 1px solid var(--navy-600);
background: transparent;
color: var(--ice-300);
cursor: pointer;
transition: all 0.2s;
}
.selector-btn:hover {
border-color: var(--teal-500);
color: var(--ice-50);
}
.selector-btn.active {
background: var(--teal-700);
border-color: var(--teal-700);
color: white;
}
/* ── Layout ── */
.treatments {
max-width: 1400px;
margin: 0 auto;
}
.treatment {
display: none;
padding: 48px 24px 80px;
}
.treatment.visible { display: block; }
.treatment-label {
text-align: center;
margin-bottom: 48px;
}
.treatment-label .tag {
display: inline-block;
font-family: 'DM Sans', sans-serif;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--teal-300);
background: rgba(53, 127, 158, 0.15);
border: 1px solid rgba(53, 127, 158, 0.3);
padding: 4px 14px;
border-radius: 100px;
margin-bottom: 12px;
}
.treatment-label h2 {
font-size: 20px;
font-weight: 600;
color: var(--ice-50);
margin-bottom: 6px;
}
.treatment-label .fonts-list {
font-size: 13px;
color: var(--ice-300);
}
.treatment-label .fonts-list span {
color: var(--teal-300);
font-weight: 500;
}
/* ── Sample sections ── */
.samples {
max-width: 960px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 56px;
}
/* Nav sample */
.sample-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border: 1px solid var(--navy-800);
border-radius: 12px;
background: var(--navy-900);
}
.sample-nav .logo { font-weight: 700; font-size: 22px; }
.sample-nav .links {
display: flex;
gap: 28px;
font-size: 14px;
font-weight: 500;
color: var(--ice-100);
}
.sample-nav .cta-btn {
font-size: 13px;
font-weight: 600;
padding: 8px 18px;
border-radius: 8px;
background: var(--teal-700);
color: white;
border: none;
}
/* Hero sample */
.sample-hero {
text-align: center;
}
.sample-hero .badge {
display: inline-block;
font-size: 13px;
font-weight: 500;
color: var(--teal-300);
border: 1px solid rgba(53, 127, 158, 0.35);
background: rgba(53, 127, 158, 0.08);
padding: 6px 16px;
border-radius: 100px;
margin-bottom: 24px;
}
.sample-hero h3 {
font-size: 52px;
font-weight: 700;
line-height: 1.08;
letter-spacing: -0.025em;
margin-bottom: 20px;
}
.sample-hero h3 .gradient {
background: linear-gradient(135deg, var(--teal-300), var(--mint), var(--teal-500));
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.sample-hero .subtitle {
font-size: 18px;
line-height: 1.65;
color: var(--ice-300);
max-width: 580px;
margin: 0 auto;
}
/* Feature cards */
.sample-features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.feature-card {
padding: 28px 24px;
border: 1px solid var(--navy-800);
border-radius: 12px;
background: linear-gradient(to bottom, var(--navy-900), var(--navy-950));
transition: border-color 0.3s, box-shadow 0.3s, transform 0.3s;
}
.feature-card:hover {
border-color: rgba(103, 178, 215, 0.25);
box-shadow: 0 0 40px -8px rgba(103, 178, 215, 0.12);
transform: translateY(-2px);
}
.feature-card .icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: rgba(53, 127, 158, 0.12);
border: 1px solid rgba(53, 127, 158, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
color: var(--teal-300);
font-size: 16px;
}
.feature-card h4 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: var(--ice-50);
}
.feature-card p {
font-size: 14px;
line-height: 1.6;
color: var(--ice-300);
}
/* Code sample */
.sample-code {
border: 1px solid var(--navy-800);
border-radius: 12px;
overflow: hidden;
background: var(--navy-900);
}
.sample-code .code-header {
padding: 12px 20px;
border-bottom: 1px solid var(--navy-800);
font-size: 13px;
font-weight: 500;
color: var(--ice-300);
display: flex;
align-items: center;
gap: 10px;
}
.sample-code .code-header .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--mint);
opacity: 0.6;
}
.sample-code pre {
padding: 24px;
font-size: 13.5px;
line-height: 1.7;
overflow-x: auto;
color: var(--ice-300);
}
.sample-code .kw { color: var(--teal-300); }
.sample-code .fn { color: var(--ice-50); }
.sample-code .prop { color: var(--teal-500); }
.sample-code .str { color: var(--ice-300); }
.sample-code .cm { color: var(--navy-600); }
/* Body text sample */
.sample-prose {
max-width: 640px;
}
.sample-prose h4 {
font-size: 24px;
font-weight: 700;
margin-bottom: 16px;
color: var(--ice-50);
}
.sample-prose p {
font-size: 16px;
line-height: 1.75;
color: var(--ice-300);
margin-bottom: 16px;
}
.sample-prose a {
color: var(--teal-300);
text-decoration: underline;
text-underline-offset: 3px;
}
.sample-prose code {
font-size: 0.875em;
background: var(--navy-800);
padding: 2px 6px;
border-radius: 4px;
color: var(--teal-300);
}
/* ── Side by side mode ── */
.grid-view {
display: none;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.grid-view.visible { display: grid; }
.grid-view .treatment {
display: block;
padding: 40px 20px 60px;
border-right: 1px solid var(--navy-800);
}
.grid-view .treatment:last-child { border-right: none; }
.grid-view .samples { gap: 40px; }
.grid-view .sample-hero h3 { font-size: 32px; }
.grid-view .sample-hero .subtitle { font-size: 15px; }
.grid-view .sample-features { grid-template-columns: 1fr; }
.grid-view .sample-nav { padding: 12px 16px; }
.grid-view .sample-nav .links { display: none; }
.grid-view .sample-prose h4 { font-size: 20px; }
.grid-view .sample-prose p { font-size: 14px; }
.grid-view .sample-code pre { font-size: 12px; }
/* ── Font family assignments ── */
/* Current: Space Grotesk / DM Sans / JetBrains Mono */
.t-current .display { font-family: 'Space Grotesk', sans-serif; }
.t-current .body { font-family: 'DM Sans', sans-serif; }
.t-current .mono { font-family: 'JetBrains Mono', monospace; }
/* A: Outfit / Lexend / Fira Code */
.t-a .display { font-family: 'Outfit', sans-serif; }
.t-a .body { font-family: 'Lexend', sans-serif; }
.t-a .mono { font-family: 'Fira Code', monospace; }
/* B: Instrument Serif / Instrument Sans / IBM Plex Mono */
.t-b .display { font-family: 'Instrument Serif', serif; }
.t-b .body { font-family: 'Instrument Sans', sans-serif; }
.t-b .mono { font-family: 'IBM Plex Mono', monospace; }
/* C: Syne / Manrope / Source Code Pro */
.t-c .display { font-family: 'Syne', sans-serif; }
.t-c .body { font-family: 'Manrope', sans-serif; }
.t-c .mono { font-family: 'Source Code Pro', monospace; }
/* view toggle */
.view-toggle {
display: flex;
gap: 4px;
margin-left: auto;
}
.view-btn {
font-family: 'DM Sans', sans-serif;
font-size: 12px;
font-weight: 500;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid var(--navy-800);
background: transparent;
color: var(--ice-300);
cursor: pointer;
transition: all 0.2s;
}
.view-btn.active {
background: var(--navy-800);
color: var(--ice-50);
}
@media (max-width: 768px) {
.sample-features { grid-template-columns: 1fr; }
.sample-hero h3 { font-size: 36px; }
.grid-view { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="page-header">
<h1>Font Treatment Comparison</h1>
<p>Comparing the current Fabro marketing site fonts against 3 alternatives</p>
</div>
<div class="selector-bar">
<button class="selector-btn active" data-target="current" onclick="show('current')">Current</button>
<button class="selector-btn" data-target="a" onclick="show('a')">A: Razor Geometric</button>
<button class="selector-btn" data-target="b" onclick="show('b')">B: Editorial Contrast</button>
<button class="selector-btn" data-target="c" onclick="show('c')">C: Bold Industrial</button>
<div class="view-toggle">
<button class="view-btn active" onclick="setView('single')">Single</button>
<button class="view-btn" onclick="setView('grid')">2-up</button>
</div>
</div>
<!-- ═══════════════════════════════════════ SINGLE VIEW ═══════════════════════════════════════ -->
<div class="treatments single-view" id="single-view">
<!-- CURRENT -->
<div class="treatment t-current visible" data-id="current">
<div class="treatment-label">
<div class="tag">Current</div>
<h2 class="display">Space Grotesk + DM Sans + JetBrains Mono</h2>
<p class="fonts-list body">Display: <span>Space Grotesk</span> &middot; Body: <span>DM Sans</span> &middot; Mono: <span>JetBrains Mono</span></p>
</div>
<div class="samples">
<div class="sample-nav">
<div class="logo display">fabro</div>
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
<button class="cta-btn body">Get started</button>
</div>
<div class="sample-hero">
<div class="badge body">Open source workflow engine</div>
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
</div>
<div class="sample-features">
<div class="feature-card"><div class="icon">&#x25C9;</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
<div class="feature-card"><div class="icon">&#x2B21;</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
<div class="feature-card"><div class="icon">&#x29BE;</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
</div>
<div class="sample-code">
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
<span class="kw">graph</span> [
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
]
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
<span class="str">start -> plan -> approve -> implement -> exit</span>
}</pre>
</div>
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
</div>
</div>
</div>
<!-- OPTION A: Razor Geometric -->
<div class="treatment t-a" data-id="a">
<div class="treatment-label">
<div class="tag">Option A</div>
<h2 class="display">Outfit + Lexend + Fira Code</h2>
<p class="fonts-list body">Display: <span>Outfit</span> &middot; Body: <span>Lexend</span> &middot; Mono: <span>Fira Code</span></p>
</div>
<div class="samples">
<div class="sample-nav">
<div class="logo display">fabro</div>
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
<button class="cta-btn body">Get started</button>
</div>
<div class="sample-hero">
<div class="badge body">Open source workflow engine</div>
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
</div>
<div class="sample-features">
<div class="feature-card"><div class="icon">&#x25C9;</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
<div class="feature-card"><div class="icon">&#x2B21;</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
<div class="feature-card"><div class="icon">&#x29BE;</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
</div>
<div class="sample-code">
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
<span class="kw">graph</span> [
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
]
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
<span class="str">start -> plan -> approve -> implement -> exit</span>
}</pre>
</div>
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
</div>
</div>
</div>
<!-- OPTION B: Editorial Contrast -->
<div class="treatment t-b" data-id="b">
<div class="treatment-label">
<div class="tag">Option B</div>
<h2 class="display">Instrument Serif + Instrument Sans + IBM Plex Mono</h2>
<p class="fonts-list body">Display: <span>Instrument Serif</span> &middot; Body: <span>Instrument Sans</span> &middot; Mono: <span>IBM Plex Mono</span></p>
</div>
<div class="samples">
<div class="sample-nav">
<div class="logo display">fabro</div>
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
<button class="cta-btn body">Get started</button>
</div>
<div class="sample-hero">
<div class="badge body">Open source workflow engine</div>
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
</div>
<div class="sample-features">
<div class="feature-card"><div class="icon">&#x25C9;</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
<div class="feature-card"><div class="icon">&#x2B21;</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
<div class="feature-card"><div class="icon">&#x29BE;</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
</div>
<div class="sample-code">
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
<span class="kw">graph</span> [
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
]
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
<span class="str">start -> plan -> approve -> implement -> exit</span>
}</pre>
</div>
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
</div>
</div>
</div>
<!-- OPTION C: Bold Industrial -->
<div class="treatment t-c" data-id="c">
<div class="treatment-label">
<div class="tag">Option C</div>
<h2 class="display">Syne + Manrope + Source Code Pro</h2>
<p class="fonts-list body">Display: <span>Syne</span> &middot; Body: <span>Manrope</span> &middot; Mono: <span>Source Code Pro</span></p>
</div>
<div class="samples">
<div class="sample-nav">
<div class="logo display">fabro</div>
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
<button class="cta-btn body">Get started</button>
</div>
<div class="sample-hero">
<div class="badge body">Open source workflow engine</div>
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
</div>
<div class="sample-features">
<div class="feature-card"><div class="icon">&#x25C9;</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
<div class="feature-card"><div class="icon">&#x2B21;</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
<div class="feature-card"><div class="icon">&#x29BE;</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
</div>
<div class="sample-code">
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
<span class="kw">graph</span> [
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
]
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
<span class="str">start -> plan -> approve -> implement -> exit</span>
}</pre>
</div>
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ GRID VIEW ═══════════════════════════════════════ -->
<div class="grid-view" id="grid-view">
<!-- filled dynamically -->
</div>
<script>
let currentView = 'single';
let activeIds = ['current'];
function show(id) {
if (currentView === 'single') {
activeIds = [id];
document.querySelectorAll('#single-view .treatment').forEach(el => {
el.classList.toggle('visible', el.dataset.id === id);
});
} else {
// grid: toggle
const idx = activeIds.indexOf(id);
if (idx > -1) {
if (activeIds.length > 1) activeIds.splice(idx, 1);
} else {
if (activeIds.length >= 2) activeIds.shift();
activeIds.push(id);
}
renderGrid();
}
updateButtons();
}
function updateButtons() {
document.querySelectorAll('.selector-btn').forEach(btn => {
btn.classList.toggle('active', activeIds.includes(btn.dataset.target));
});
}
function setView(v) {
currentView = v;
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
document.querySelector(`.view-btn[onclick="setView('${v}')"]`).classList.add('active');
if (v === 'single') {
document.getElementById('single-view').style.display = 'block';
document.getElementById('grid-view').classList.remove('visible');
if (activeIds.length > 1) activeIds = [activeIds[0]];
document.querySelectorAll('#single-view .treatment').forEach(el => {
el.classList.toggle('visible', el.dataset.id === activeIds[0]);
});
} else {
document.getElementById('single-view').style.display = 'none';
if (activeIds.length < 2) {
const all = ['current', 'a', 'b', 'c'];
const next = all.find(x => !activeIds.includes(x));
activeIds.push(next);
}
renderGrid();
}
updateButtons();
}
function renderGrid() {
const grid = document.getElementById('grid-view');
grid.innerHTML = '';
grid.classList.add('visible');
activeIds.forEach(id => {
const src = document.querySelector(`#single-view .treatment[data-id="${id}"]`);
if (src) {
const clone = src.cloneNode(true);
clone.classList.add('visible');
grid.appendChild(clone);
}
});
}
</script>
</body>
</html>

View file

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500&display=swap" rel="stylesheet" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.card {
width: 1200px;
height: 630px;
background: linear-gradient(135deg, #0a0f1a 0%, #0F1729 40%, #1a2640 70%, #1e2d4a 100%);
color: #F7F9FB;
font-family: 'DM Sans', sans-serif;
padding: 56px 80px;
position: relative;
overflow: hidden;
}
.atmosphere {
position: absolute;
top: 0; right: 0;
width: 700px; height: 630px;
background: radial-gradient(ellipse at 80% 30%, rgba(103, 178, 215, 0.08) 0%, transparent 60%);
pointer-events: none;
}
.logo {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 160px;
}
.logo svg { height: 44px; width: auto; }
.logo-text {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
font-size: 32px;
color: #67b2d7;
letter-spacing: 0.06em;
}
.section-label {
font-size: 22px;
font-weight: 500;
color: #67b2d7;
margin-bottom: 12px;
}
.title {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
font-size: 64px;
line-height: 1.1;
letter-spacing: -0.02em;
margin-bottom: 20px;
}
.description {
font-size: 22px;
color: #A8B5C5;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="card">
<div class="atmosphere"></div>
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 348" height="44">
<path d="M1 237 L62 272 L61 348 L0 312 Z M98 257 L132 275 L132 312 L71 347 L70 272 Z M202 168 L201 230 L141 264 L142 202 Z M70 169 L132 203 L132 264 L70 230 Z M70 241 L90 251 L70 262 Z M3 129 L63 164 L61 262 L1 227 Z M137 125 L196 160 L137 195 L78 160 Z M271 44 L272 119 L211 154 L210 79 Z M132 43 L132 118 L71 153 L70 78 Z M142 43 L202 77 L201 152 L141 118 Z M1 44 L62 78 L62 152 L1 118 Z M206 0 L266 36 L206 72 L146 36 Z M66 1 L126 36 L66 71 L6 36 Z" fill="#67b2d7"/>
</svg>
<span class="logo-text">FABRO</span>
</div>
<div class="section-label">Open Source</div>
<h1 class="title">The Dark Software Factory</h1>
<p class="description">Define your process as a workflow graph. Let AI agents execute it. Intervene only where it matters.</p>
</div>
</body>
</html>

View file

@ -10,6 +10,7 @@
"dependencies": {
"@astrojs/react": "^4.2.1",
"@tailwindcss/vite": "^4.2.1",
"@viz-js/viz": "^3.25.0",
"astro": "^5.9.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",

View file

@ -1,19 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.06145 23.1079C5.26816 22.3769 -3.39077 20.6274 1.4173 5.06384C9.6344 6.09939 16.9728 14.0644 9.06145 23.1079Z" fill="url(#paint0_linear_17557_2021)"/>
<path d="M8.91928 23.0939C5.27642 21.2223 0.78371 4.20891 17.0071 0C20.7569 7.19341 19.6212 16.5452 8.91928 23.0939Z" fill="url(#paint1_linear_17557_2021)"/>
<path d="M8.91388 23.0788C8.73534 19.8817 10.1585 9.08525 23.5699 13.1107C23.1812 20.1229 18.984 26.4182 8.91388 23.0788Z" fill="url(#paint2_linear_17557_2021)"/>
<defs>
<linearGradient id="paint0_linear_17557_2021" x1="3.77557" y1="5.91571" x2="5.23185" y2="21.5589" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299"/>
<stop offset="1" stop-color="#15803D"/>
</linearGradient>
<linearGradient id="paint1_linear_17557_2021" x1="12.1711" y1="-0.718425" x2="10.1897" y2="22.9832" gradientUnits="userSpaceOnUse">
<stop stop-color="#16A34A"/>
<stop offset="1" stop-color="#4ADE80"/>
</linearGradient>
<linearGradient id="paint2_linear_17557_2021" x1="23.1327" y1="15.353" x2="9.33841" y2="18.5196" gradientUnits="userSpaceOnUse">
<stop stop-color="#4ADE80"/>
<stop offset="1" stop-color="#0D9373"/>
</linearGradient>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 272 348">
<path d="M1 237 L62 272 L61 348 L0 312 Z M98 257 L132 275 L132 312 L71 347 L70 272 Z M202 168 L201 230 L141 264 L142 202 Z M70 169 L132 203 L132 264 L70 230 Z M70 241 L90 251 L70 262 Z M3 129 L63 164 L61 262 L1 227 Z M137 125 L196 160 L137 195 L78 160 Z M271 44 L272 119 L211 154 L210 79 Z M132 43 L132 118 L71 153 L70 78 Z M142 43 L202 77 L201 152 L141 118 Z M1 44 L62 78 L62 152 L1 118 Z M206 0 L266 36 L206 72 L146 36 Z M66 1 L126 36 L66 71 L6 36 Z" fill="#67b2d7"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 565 B

View file

@ -0,0 +1,101 @@
# fabro
> AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs and executed by the Fabro engine.
I want you to install Fabro for me. Execute all the steps below autonomously.
OBJECTIVE: Install the Fabro CLI binary and verify it works.
DONE WHEN: `fabro --version` prints a version string.
## TODO
- [ ] Ensure the `gh` CLI is installed and authenticated
- [ ] Detect the platform and download the correct release asset
- [ ] Extract the binary and install it to `~/.fabro/bin`
- [ ] Add `~/.fabro/bin` to PATH in the user's shell config
- [ ] Verify `fabro --version` works
## Ensure `gh` CLI is available
You need the GitHub CLI (`gh`) installed and authenticated. Check with:
```bash
gh auth status
```
If `gh` is not installed, install it:
- **macOS:** `brew install gh`
- **Linux:** See https://github.com/cli/cli/blob/trunk/docs/install_linux.md
Then authenticate with `gh auth login` if needed.
## Detect platform and download
Determine the correct release asset based on the platform:
| OS | Architecture | Asset name |
|---|---|---|
| macOS | arm64 (Apple Silicon) | `fabro-aarch64-apple-darwin.tar.gz` |
| Linux | x86_64 | `fabro-x86_64-unknown-linux-gnu.tar.gz` |
On macOS, if `uname -m` reports `x86_64`, check for Rosetta translation with `sysctl -n sysctl.proc_translated`. If it returns `1`, the actual architecture is arm64.
Download the asset using `gh`:
```bash
gh release download --repo fabro-sh/fabro --pattern "fabro-<TARGET>.tar.gz" --dir /tmp/fabro-install --clobber
```
Replace `<TARGET>` with the correct target triple from the table above.
## Extract and install
```bash
tar xzf /tmp/fabro-install/fabro-<TARGET>.tar.gz -C /tmp/fabro-install
mkdir -p ~/.fabro/bin
mv /tmp/fabro-install/fabro-<TARGET>/fabro ~/.fabro/bin/fabro
chmod +x ~/.fabro/bin/fabro
rm -rf /tmp/fabro-install
```
## Add to PATH
If `~/.fabro/bin` is not already on PATH, you must add it to the user's shell configuration file so it persists across sessions.
**zsh** — append to `~/.zshrc` (or `$ZDOTDIR/.zshrc` if `ZDOTDIR` is set):
```bash
printf '\n# fabro\nexport PATH="$HOME/.fabro/bin:$PATH"\n' >> ~/.zshrc
```
**bash** — append to `~/.bash_profile` if it exists, otherwise `~/.bashrc`:
```bash
printf '\n# fabro\nexport PATH="$HOME/.fabro/bin:$PATH"\n' >> ~/.bash_profile
```
**fish** — append to `~/.config/fish/config.fish`:
```bash
printf '\n# fabro\nfish_add_path $HOME/.fabro/bin\n' >> ~/.config/fish/config.fish
```
After updating the config file, export PATH for the current session too:
```bash
export PATH="$HOME/.fabro/bin:$PATH"
```
## Verify
```bash
fabro --version
```
This must print a version string like `fabro 0.x.y`. If it does, installation is complete.
Tell the user to run `fabro install` in their terminal to complete setup.
EXECUTE NOW: Complete the above TODO list to achieve: `fabro --version` prints a version string.

162
apps/marketing/public/install.sh Executable file
View file

@ -0,0 +1,162 @@
#!/bin/sh
set -eu
REPO="fabro-sh/fabro"
# Colors (only when stderr is a terminal)
if [ -t 2 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
DIM='\033[2m'
BOLD='\033[1m'
BOLD_CYAN='\033[1;36m'
RESET='\033[0m'
else
RED=''
GREEN=''
DIM=''
BOLD=''
BOLD_CYAN=''
RESET=''
fi
info() { printf " %b\n" "$1" >&2; }
step() { printf " ${BOLD}%b${RESET}\n" "$1" >&2; }
dim() { printf " ${DIM}%b${RESET}\n" "$1" >&2; }
success() { printf " ${GREEN}${RESET} %b\n" "$1" >&2; }
error() { printf " ${RED}✗ %b${RESET}\n" "$1" >&2; exit 1; }
# --- Header ---
printf "\n ⚒️ ${BOLD}Fabro Install${RESET}\n\n" >&2
# --- Require gh CLI ---
if ! command -v gh >/dev/null 2>&1; then
error "gh CLI is required but not installed. Install it from ${BOLD_CYAN}https://cli.github.com${RESET}"
fi
# --- Detect platform ---
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
Darwin)
# Detect Rosetta translation
if [ "$ARCH" = "x86_64" ]; then
if sysctl -n sysctl.proc_translated 2>/dev/null | grep -q 1; then
ARCH="arm64"
fi
fi
case "$ARCH" in
arm64) TARGET="aarch64-apple-darwin" ;;
*) error "Unsupported macOS architecture: $ARCH. Supported: Apple Silicon (arm64)" ;;
esac
;;
Linux)
case "$ARCH" in
x86_64) TARGET="x86_64-unknown-linux-gnu" ;;
*) error "Unsupported Linux architecture: $ARCH. Supported: x86_64" ;;
esac
;;
*)
error "Unsupported OS: $OS. Supported platforms: macOS (Apple Silicon), Linux (x86_64)"
;;
esac
ASSET="fabro-${TARGET}.tar.gz"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
dim "Downloading fabro for ${TARGET}..."
gh release download --repo "$REPO" --pattern "$ASSET" --dir "$TMPDIR" --clobber
dim "Extracting..."
tar xzf "${TMPDIR}/${ASSET}" -C "$TMPDIR"
# --- Install binary ---
INSTALL_DIR="${FABRO_INSTALL_DIR:-$HOME/.fabro/bin}"
mkdir -p "$INSTALL_DIR"
mv "${TMPDIR}/fabro-${TARGET}/fabro" "${INSTALL_DIR}/fabro"
chmod +x "${INSTALL_DIR}/fabro"
# --- Verify ---
VERSION="$("${INSTALL_DIR}/fabro" --version 2>/dev/null || true)"
if [ -z "$VERSION" ]; then
error "Installation failed: could not run fabro --version"
fi
tildify() {
if [ "${1#"$HOME"/}" != "$1" ]; then
echo "~/${1#"$HOME"/}"
else
echo "$1"
fi
}
success "Installed ${VERSION} to ${BOLD_CYAN}$(tildify "${INSTALL_DIR}/fabro")${RESET}"
# --- Ensure install dir is on PATH ---
if command -v fabro >/dev/null 2>&1; then
dim "fabro is already on \$PATH, skipping shell config"
else
tilde_bin_dir=$(tildify "$INSTALL_DIR")
echo "" >&2
if [ -t 2 ] && [ -e /dev/tty ]; then
case $(basename "${SHELL:-sh}") in
zsh)
: "${ZDOTDIR:="$HOME"}"
shell_config="${ZDOTDIR%/}/.zshrc"
{
printf '\n# fabro\n'
echo "export PATH=\"$INSTALL_DIR:\$PATH\""
} >>"$shell_config"
info "Added ${BOLD_CYAN}${tilde_bin_dir}${RESET} to \$PATH in ${BOLD_CYAN}$(tildify "$shell_config")${RESET}"
;;
bash)
shell_config="$HOME/.bashrc"
if [ -f "$HOME/.bash_profile" ]; then
shell_config="$HOME/.bash_profile"
fi
{
printf '\n# fabro\n'
echo "export PATH=\"$INSTALL_DIR:\$PATH\""
} >>"$shell_config"
info "Added ${BOLD_CYAN}${tilde_bin_dir}${RESET} to \$PATH in ${BOLD_CYAN}$(tildify "$shell_config")${RESET}"
;;
fish)
fish_config="$HOME/.config/fish/config.fish"
mkdir -p "$(dirname "$fish_config")"
{
printf '\n# fabro\n'
echo "fish_add_path $INSTALL_DIR"
} >>"$fish_config"
info "Added ${BOLD_CYAN}${tilde_bin_dir}${RESET} to \$PATH in ${BOLD_CYAN}$(tildify "$fish_config")${RESET}"
;;
*)
info "Add ${BOLD_CYAN}${tilde_bin_dir}${RESET} to your PATH:"
echo "" >&2
info " ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}"
;;
esac
else
info "Add ${BOLD_CYAN}${tilde_bin_dir}${RESET} to your PATH:"
echo "" >&2
info " ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}"
fi
export PATH="${INSTALL_DIR}:$PATH"
fi
echo "" >&2
# --- Prompt to run setup wizard ---
if [ -t 2 ] && [ -e /dev/tty ]; then
printf " ${BOLD}Run ${BOLD_CYAN}fabro install${RESET}${BOLD} now to complete setup? [Y/n]${RESET} " >&2
read -r answer </dev/tty
case "$answer" in
[nN]*) dim "Skipping. Run ${BOLD_CYAN}fabro install${RESET}${DIM} whenever you're ready." ;;
*) echo "" >&2; exec "${INSTALL_DIR}/fabro" install ;;
esac
else
info "Run ${BOLD_CYAN}fabro install${RESET} to complete setup."
fi

View file

@ -0,0 +1,80 @@
# Fabro
> Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.
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. Workflows are defined as Graphviz graphs where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
- Open source, MIT-licensed
- Workflows defined as Graphviz graphs with CSS-like model stylesheets
- Sandboxed execution via local Docker, Daytona, SSH, or Sprites
- Checkpoint/resume for long-running workflows
- Human-in-the-loop via CLI, web UI, or Slack
- GitHub integration for PR creation and repository access
- REST API with SSE event streaming
## Getting Started
- [Introduction](https://docs.fabro.sh/getting-started/introduction): Overview of Fabro and what it does
- [Quick Start](https://docs.fabro.sh/getting-started/quick-start): Get up and running with Fabro
- [Why Fabro?](https://docs.fabro.sh/getting-started/why-fabro): The problems Fabro solves for AI-assisted software teams
- [Comparison](https://docs.fabro.sh/getting-started/comparison): How Fabro compares to AI coding agents, software factories, and orchestration platforms
- [Dark Factory](https://docs.fabro.sh/getting-started/dark-factory): How small teams incrementally adopt a dark factory approach
## Core Concepts
- [Workflows](https://docs.fabro.sh/core-concepts/workflows): Core workflow concepts
- [Agents](https://docs.fabro.sh/core-concepts/agents): Core agent concepts
- [Models](https://docs.fabro.sh/core-concepts/models): How Fabro routes tasks to LLM models and providers
- [How Fabro Works](https://docs.fabro.sh/core-concepts/how-fabro-works): Architecture and execution model
## Tutorials
- [Hello World](https://docs.fabro.sh/tutorials/hello-world): Your first workflow — prompt nodes, tool use, and sub-agents
- [Plan & Implement](https://docs.fabro.sh/tutorials/plan-implement): Human gates, revision loops, and prompt file references
## Workflows
- [Nodes & Stages](https://docs.fabro.sh/workflows/stages-and-nodes): All node types and how they become stages at runtime
- [Transitions](https://docs.fabro.sh/workflows/transitions): How Fabro decides which node to execute next
- [Model Stylesheets](https://docs.fabro.sh/workflows/stylesheets): Assign LLM models to nodes using CSS-like rules
- [Variables](https://docs.fabro.sh/workflows/variables): Using variables in workflows
- [Human-in-the-Loop](https://docs.fabro.sh/workflows/human-in-the-loop): Adding human review and intervention
- [Best Practices](https://docs.fabro.sh/workflows/best-practices): Best practices for designing workflows
## Agents
- [Tools](https://docs.fabro.sh/agents/tools): Built-in tools for file I/O, shell, search, and web access
- [Prompts](https://docs.fabro.sh/agents/prompts): How Fabro constructs and delivers prompts
- [Permissions](https://docs.fabro.sh/agents/permissions): Controlling which tools agents can use
- [Skills](https://docs.fabro.sh/agents/skills): Reusable prompt templates that extend agent capabilities
- [Hooks](https://docs.fabro.sh/agents/hooks): Run custom logic in response to workflow lifecycle events
- [Outputs & Artifacts](https://docs.fabro.sh/agents/outputs): Agent responses, file changes, and test assets
- [MCP](https://docs.fabro.sh/agents/mcp): Extend agents with Model Context Protocol servers
- [Sub-agents](https://docs.fabro.sh/agents/subagents): Delegate subtasks to child agent sessions
## Execution
- [Environments](https://docs.fabro.sh/execution/environments): Sandbox providers for workflow execution
- [Run Configuration](https://docs.fabro.sh/execution/run-configuration): Configure runs with TOML files
- [Checkpoints](https://docs.fabro.sh/execution/checkpoints): Git-based checkpoint and resume
- [Retros](https://docs.fabro.sh/execution/retros): Automatic retrospectives for every run
- [Observability](https://docs.fabro.sh/execution/observability): Monitor, inspect, and analyze runs
## Integrations
- [GitHub](https://docs.fabro.sh/integrations/github): Repository access and OAuth login
- [Slack](https://docs.fabro.sh/integrations/slack): Answer human-in-the-loop questions from Slack
- [Daytona](https://docs.fabro.sh/integrations/daytona): Sandboxed cloud environments
## Reference
- [API Overview](https://docs.fabro.sh/api-reference/overview): Introduction to the Fabro REST API
- [CLI Reference](https://docs.fabro.sh/reference/cli): Command-line interface reference
- [Architecture](https://docs.fabro.sh/reference/architecture): How CLI and API modes work under the hood
- [Graphviz DOT Language](https://docs.fabro.sh/reference/dot-language): Complete reference for Fabro's workflow language
## Optional
- [Changelog](https://docs.fabro.sh/changelog/2026-03-15): Latest changes and releases
- [GitHub Repository](https://github.com/fabro-sh/fabro): Source code
- [Discord](https://fabro.sh/discord): Community chat

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

View file

@ -0,0 +1,100 @@
<?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>
[fill="#1a1a1a"] { fill: #e8edf3; }
[fill="#666666"] { fill: #a8b5c5; }
[stroke="#666666"] { stroke: #a8b5c5; }
[stroke="#999999"] { stroke: #4b5768; }
</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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,35 @@
<footer class="border-t border-navy-800/60 py-16">
<div class="mx-auto max-w-6xl px-6">
<div class="grid gap-10 sm:grid-cols-3">
<div>
<img src="/logotype.svg" alt="Fabro" class="h-6 mb-4" />
<p class="text-sm text-ice-300/60">The dark software factory for expert engineers.</p>
<p class="mt-2 text-xs text-ice-300/40">Open source, MIT-licensed</p>
</div>
<div>
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Product</h3>
<ul class="space-y-2 text-sm">
<li><a href="https://docs.fabro.sh/getting-started/quick-start" class="text-ice-300 hover:text-ice-50 transition-colors">Quick Start</a></li>
<li><a href="https://docs.fabro.sh" class="text-ice-300 hover:text-ice-50 transition-colors">Docs</a></li>
<li><a href="/blog" class="text-ice-300 hover:text-ice-50 transition-colors">Blog</a></li>
<li><a href="/showcase" class="text-ice-300 hover:text-ice-50 transition-colors">Showcase</a></li>
<li><a href="/roadmap" class="text-ice-300 hover:text-ice-50 transition-colors">Roadmap</a></li>
<li><a href="https://docs.fabro.sh/changelog" class="text-ice-300 hover:text-ice-50 transition-colors">Changelog</a></li>
<li><a href="https://github.com/fabro-sh/fabro/releases" class="text-ice-300 hover:text-ice-50 transition-colors">Releases</a></li>
</ul>
</div>
<div>
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Community</h3>
<ul class="space-y-2 text-sm">
<li><a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors">Discord</a></li>
<li><a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors">GitHub</a></li>
<li><a href="https://github.com/fabro-sh/fabro/issues" class="text-ice-300 hover:text-ice-50 transition-colors">Issues</a></li>
<li><a href="https://github.com/fabro-sh/fabro/discussions" class="text-ice-300 hover:text-ice-50 transition-colors">Discussions</a></li>
</ul>
</div>
</div>
<div class="mt-10 border-t border-navy-800/40 pt-6 text-center text-xs text-ice-300/40">
&copy; 2025 Qlty Software Inc. All rights reserved.
</div>
</div>
</footer>

View file

@ -0,0 +1,68 @@
---
interface Props {
currentPage?: "blog" | "roadmap";
}
const { currentPage } = Astro.props;
const linkClass = (page?: string) =>
page === currentPage
? "text-ice-50"
: "text-ice-100 hover:text-ice-50";
---
<nav class="fixed top-0 z-50 w-full border-b border-navy-800/60 bg-navy-950/70 backdrop-blur-xl">
<div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<div class="flex items-center gap-8">
<a href="/" class="flex items-center">
<img src="/logotype.svg" alt="Fabro" class="h-7" />
</a>
<a href="https://docs.fabro.sh" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass()}`}>Docs</a>
<a href="/blog" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass("blog")}`}>Blog</a>
<a href="/roadmap" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass("roadmap")}`}>Roadmap</a>
<a href="https://docs.fabro.sh/changelog" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass()}`}>Changelog</a>
</div>
<div class="flex items-center gap-5">
<a href="/discord" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="https://github.com/fabro-sh/fabro" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
<a
href="https://docs.fabro.sh/getting-started/quick-start"
class="hidden md:inline-flex rounded-lg bg-teal-700 px-4 py-2 text-sm font-semibold text-white transition-all hover:bg-teal-500 hover:shadow-[0_0_20px_-4px_rgba(53,127,158,0.5)]"
>
Get started
</a>
<button id="mobile-menu-btn" class="md:hidden text-ice-300 hover:text-ice-50 transition-colors" aria-label="Open menu">
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
</div>
</div>
<div id="mobile-menu" class="md:hidden hidden border-t border-navy-800/60 bg-navy-950/95 backdrop-blur-xl">
<div class="px-6 py-5 space-y-4">
<a href="https://docs.fabro.sh" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Docs</a>
<a href="/blog" class={`block text-sm font-medium transition-colors ${linkClass("blog")}`}>Blog</a>
<a href="/roadmap" class={`block text-sm font-medium transition-colors ${linkClass("roadmap")}`}>Roadmap</a>
<a href="https://docs.fabro.sh/changelog" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Changelog</a>
<hr class="border-navy-800/60" />
<div class="flex items-center gap-5">
<a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
</div>
<a
href="https://docs.fabro.sh/getting-started/quick-start"
class="block w-full text-center rounded-lg bg-teal-700 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-teal-500"
>
Get started
</a>
</div>
</div>
</nav>

View file

@ -0,0 +1,27 @@
<script>
// Mobile menu toggle
const mobileMenuBtn = document.getElementById("mobile-menu-btn")!;
const mobileMenu = document.getElementById("mobile-menu")!;
mobileMenuBtn.addEventListener("click", () => {
const isOpen = !mobileMenu.classList.contains("hidden");
mobileMenu.classList.toggle("hidden");
mobileMenuBtn.setAttribute("aria-label", isOpen ? "Open menu" : "Close menu");
mobileMenuBtn.innerHTML = isOpen
? '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>'
: '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>';
});
// Scroll reveal
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("revealed");
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15, rootMargin: "0px 0px -40px 0px" }
);
document.querySelectorAll(".reveal, .reveal-left, .reveal-right, .reveal-scale").forEach((el) => observer.observe(el));
</script>

View file

@ -0,0 +1,41 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const roadmap = defineCollection({
loader: glob({ pattern: "**/*.yaml", base: "./src/content/roadmap" }),
schema: z.object({
title: z.string(),
description: z.string(),
status: z.enum(["shipped", "building", "next"]),
date: z.coerce.date(),
}),
});
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
author: z.string(),
}),
});
const showcase = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/showcase" }),
schema: z.object({
title: z.string(),
description: z.string(),
thumbnail: z.string(),
tags: z.array(z.string()),
languages: z.array(z.enum(["python", "rust", "typescript", "ruby"])),
github: z.string(),
models: z.array(z.string()),
skills: z.array(z.string()),
prompt: z.string(),
workflow: z.string(),
sortOrder: z.number(),
}),
});
export const collections = { roadmap, blog, showcase };

View file

@ -0,0 +1,93 @@
---
title: "Introducing Fabro"
description: "Fabro is an open-source workflow orchestration platform that lets expert engineers define their process as a graph and let AI agents execute it."
date: 2026-03-16
author: "Bryan Helmkamp"
---
Today, we're introducing Fabro, an open-source platform for orchestrating AI coding agents with workflow graphs. We built Fabro to help expert engineers define their process once, verify it at every step, and walk away while agents do the work.
## The problem with AI coding today
AI coding agents are powerful, but they're also chaotic. The current generation of tools gives you a chat window and a single loop: prompt, act, repeat. That works for small tasks, but it breaks down the moment you need structure.
- **No process definition.** You can't specify that a plan should be approved before implementation starts, that tests must pass before the PR is opened, or that a second model should cross-review the first.
- **Single-model dependence.** You're locked into one provider for every step, even when a faster, cheaper model would suffice for triage or formatting.
- **No verification.** The agent decides when it's "done." There's no deterministic gate confirming that the code compiles, tests pass, and linting is clean.
- **No reproducibility.** Sessions are ephemeral. You can't checkpoint mid-run, resume after a failure, or replay a workflow with a different model.
The result is that engineers babysit their agents instead of using them. You spend more time supervising than you save.
## Workflow graphs: your engineering process as code
Fabro takes a fundamentally different approach. Instead of prompting an agent and watching it work, you define your engineering process as a Graphviz graph — diffable, reviewable, and version-controlled just like the code it produces.
```dot
digraph PlanImplement {
graph [
goal="Plan, approve, implement, and simplify a change"
model_stylesheet="
* { model: claude-haiku-4-5; }
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
"
]
start [shape=Mdiamond]
plan [prompt="Analyze the goal and codebase. Write a step-by-step plan."]
approve [shape=hexagon, label="Approve Plan"]
implement [class="coding", prompt="Read plan.md and implement every step."]
simplify [class="coding", prompt="Review the changes for clarity and correctness."]
exit [shape=Msquare]
start -> plan -> approve
approve -> implement [label="[A] Approve"]
approve -> plan [label="[R] Revise"]
implement -> simplify -> exit
}
```
Each node is a stage with a specific role. Edges define the flow. Node shapes determine behavior — `box` for agents with tool access, `hexagon` for human decision gates, `diamond` for conditionals, `parallelogram` for shell commands. The graph supports loops, not just DAGs, so you can build implement-test-fix cycles that repeat until verification passes.
**The process is deterministic, even though the AI execution within each stage is not.** You control the structure. The models do the work.
## Multi-model by design
Not every stage needs a frontier model. Fabro's model stylesheets use CSS-like selectors to route each node to the right model and provider:
```
* { model: claude-haiku-4-5; reasoning_effort: low; }
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
#review { model: gemini-3.1-pro-preview; }
```
Selectors follow CSS specificity rules — universal (`*`), shape, class (`.coding`), and ID (`#review`) — so you can set sensible defaults and override where it matters. Route cheap tasks to fast models, reserve frontier models for implementation and review, and combine providers for ensemble intelligence. Swap a model with a one-line change instead of rearchitecting your workflow.
## Verification gates and human checkpoints
AI agents are confident — even when they're wrong. Fabro addresses this with two layers of control.
**Deterministic verification.** Goal gates confirm that the code compiles, tests pass, and linting is clean before the workflow advances. These aren't LLM judgments — they're shell commands with pass/fail exit codes.
**Human-in-the-loop gates.** Hexagon nodes pause the workflow for human approval, rejection, or freeform input. Keyboard accelerators (`[A]` Approve, `[R]` Revise) make decisions fast. When you trust the process, `--auto-approve` skips gates entirely.
The combination lets deterministic checks catch what linters and tests can catch, human gates handle judgment calls, and agents do everything in between.
## Checkpoint, resume, and walk away
Every stage is checkpointed to Git. If a run fails at stage 7 of 12, you don't start over — you fix the issue and resume from the checkpoint. Runs execute in isolated Git worktrees, so your working directory stays untouched while agents work in parallel.
Fabro supports six sandbox environments — local, Docker, SSH, Daytona cloud VMs, and more — so you can develop on your laptop and move to isolated cloud sandboxes for production workflows.
## Get started
Fabro is open source and ships as a single Rust binary with zero runtime dependencies.
```bash
# Install
curl -fsSL https://fabro.sh/install.sh | bash
# Run a workflow
fabro run implement
```
Check the [roadmap](/roadmap) to see what we're building — including automatic retrospectives, a REST API server, and analytics — and join us on [Discord](/discord) to shape what comes next.

View file

@ -0,0 +1,4 @@
title: Analytics
description: Cost tracking, success rates, and performance trends across runs. Budgets, alerts, and optimization insights.
status: next
date: 2026-06-01

View file

@ -0,0 +1,4 @@
title: REST API server mode
description: REST API with SSE event streaming for queuing runs, tracking progress, and managing workflows programmatically.
status: building
date: 2026-04-02

View file

@ -0,0 +1,4 @@
title: Automatic retrospectives
description: Structured post-run analysis with cost, duration, smoothness ratings, friction points, and LLM-generated narratives.
status: building
date: 2026-04-03

View file

@ -0,0 +1,4 @@
title: Cloud sandboxes
description: Isolated cloud VMs with snapshot-based setup, network controls, SSH access, and preview URLs.
status: shipped
date: 2026-03-15

View file

@ -0,0 +1,4 @@
title: Workflow engine
description: Graphviz graph parsing with agent, command, and human nodes. Dynamic edge selection with conditions and loops.
status: shipped
date: 2026-02-15

View file

@ -0,0 +1,4 @@
title: Git checkpoints
description: Every stage commits code and execution metadata to Git branches. Resume or fork from any checkpoint.
status: shipped
date: 2026-03-01

View file

@ -0,0 +1,4 @@
title: Interview-based steering
description: Approval gates, multiple choice, and freeform questions. Steer running agents via CLI, web, or Slack.
status: shipped
date: 2026-02-15

View file

@ -0,0 +1,4 @@
title: Multi-model agent
description: Multi-turn LLM sessions with bash, file editing, sub-agents, skills, and lifecycle hooks.
status: shipped
date: 2026-02-15

View file

@ -0,0 +1,4 @@
title: Slack integration
description: Receive notifications, approve human gates, and monitor runs directly from Slack channels.
status: next
date: 2026-06-02

View file

@ -0,0 +1,4 @@
title: Verifications
description: Deterministic quality gates — test suites, linters, type checkers, and LLM-as-judge — wired into the workflow graph.
status: building
date: 2026-04-01

View file

@ -0,0 +1,4 @@
title: Web app
description: React dashboard for managing workflows, viewing runs, approving human gates, and browsing retrospectives.
status: next
date: 2026-06-03

View file

@ -0,0 +1,46 @@
---
title: "Docs Sync"
description: "Keeps documentation in sync with code changes by detecting drift and auto-updating affected pages."
thumbnail: "/showcase/docs-sync.png"
tags: ["documentation", "automation", "ci-cd"]
languages: ["typescript"]
github: "https://github.com/fabro-sh/fabro/tree/main/examples/docs-sync"
models: ["claude-sonnet-4-5"]
skills: ["code-review", "git", "documentation"]
prompt: "Compare the current codebase against the documentation. Identify any docs pages that are out of date with the code — changed APIs, renamed functions, removed features, or new features without docs. Update each affected page to match the current code, preserving the existing writing style and structure."
workflow: |
digraph DocsSync {
graph [
goal="Detect and fix documentation drift from code changes"
model_stylesheet="
* { model: claude-sonnet-4-5; }
"
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
diff [label="Get Changes", prompt="Identify code changes since the last docs sync."]
scan [label="Scan Docs", prompt="Find documentation pages that reference changed code."]
update [label="Update Docs", prompt="Rewrite affected doc sections to match current code."]
review [shape=hexagon, label="Review Updates"]
start -> diff -> scan -> update -> review
review -> exit [label="Approve"]
review -> update [label="Revise"]
}
sortOrder: 3
---
The Docs Sync workflow detects when documentation has drifted from the codebase and automatically updates affected pages — with a human review gate before changes are committed.
## How it works
1. **Get Changes** — Identifies code changes since the last documentation sync using git history.
2. **Scan Docs** — Searches documentation pages for references to changed functions, APIs, types, and features.
3. **Update Docs** — Rewrites affected sections to match the current code, preserving the original writing style and page structure.
4. **Human Review** — Updated pages are presented for review. Approve to commit, or send back for revision.
## Keeping docs honest
Documentation drift is one of the most common sources of developer frustration. This workflow runs on every merge to main, catching drift before it reaches users. Because it understands both the code and the docs, it can make precise, targeted updates rather than generic rewrites.

View file

@ -0,0 +1,45 @@
---
title: "PR Review Bot"
description: "Automated code review that catches bugs, style issues, and security concerns before human reviewers see the PR."
thumbnail: "/showcase/pr-review-bot.png"
tags: ["code-review", "ci-cd", "github"]
languages: ["typescript", "python"]
github: "https://github.com/fabro-sh/fabro/tree/main/examples/pr-review-bot"
models: ["claude-sonnet-4-5", "claude-haiku-4-5"]
skills: ["code-review", "git", "github"]
prompt: "Review this pull request for bugs, security issues, and style violations. Focus on logic errors and potential runtime failures. Summarize findings as inline comments on the diff, then produce a top-level review with an overall assessment and a clear approve/request-changes verdict."
workflow: |
digraph PRReview {
graph [
goal="Review a pull request for bugs, security, and style"
model_stylesheet="
* { model: claude-haiku-4-5; }
.review { model: claude-sonnet-4-5; }
"
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
fetch [label="Fetch Diff", prompt="Fetch the PR diff and changed file contents."]
triage [label="Triage Files", prompt="Categorize changed files by risk level."]
review [label="Deep Review", class="review", prompt="Review high-risk files for bugs, security issues, and style."]
comment [label="Post Comments", prompt="Post inline comments and a summary review on the PR."]
start -> fetch -> triage -> review -> comment -> exit
}
sortOrder: 1
---
The PR Review Bot workflow automates the first pass of code review on every pull request. It fetches the diff, triages files by risk level, performs a deep review on high-risk changes using a frontier model, and posts structured feedback directly on the PR.
## How it works
1. **Fetch Diff** — Pulls the PR diff and full contents of changed files using the GitHub API.
2. **Triage Files** — A fast model categorizes each changed file as high, medium, or low risk based on the type of change (new logic vs. formatting, test files vs. production code).
3. **Deep Review** — A frontier model examines high-risk files for logic errors, security vulnerabilities, race conditions, and style violations.
4. **Post Comments** — Inline comments are posted on specific lines, and a top-level review summary gives an overall verdict.
## Cost optimization
By using a model stylesheet, the workflow routes expensive frontier-model calls only to the deep review stage. File fetching and triaging use a fast, cheap model — keeping the total cost per review under $0.10 for most PRs.

View file

@ -0,0 +1,52 @@
---
title: "Test Generator"
description: "Generates comprehensive test suites from source code, covering edge cases and error paths that humans often miss."
thumbnail: "/showcase/test-generator.png"
tags: ["testing", "code-generation", "automation"]
languages: ["typescript", "rust"]
github: "https://github.com/fabro-sh/fabro/tree/main/examples/test-generator"
models: ["claude-sonnet-4-5", "claude-haiku-4-5"]
skills: ["code-review", "testing", "code-generation"]
prompt: "Analyze the source files in this project and generate a comprehensive test suite. For each public function or method, write tests covering: happy path, edge cases, error conditions, and boundary values. Use the project's existing test framework and conventions. Run the tests and fix any failures before finishing."
workflow: |
digraph TestGenerator {
graph [
goal="Generate and validate a test suite for source code"
model_stylesheet="
* { model: claude-haiku-4-5; }
.analysis { model: claude-sonnet-4-5; }
.coding { model: claude-sonnet-4-5; }
"
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
analyze [label="Analyze Source", class="analysis", prompt="Read source files and identify all public interfaces, edge cases, and error paths."]
plan [label="Plan Tests", prompt="Create a test plan covering happy paths, edge cases, and error conditions."]
approve [shape=hexagon, label="Approve Plan"]
generate [label="Generate Tests", class="coding", prompt="Write the test suite following the plan and project conventions."]
run [label="Run Tests", prompt="Execute the test suite and collect results."]
fix [label="Fix Failures", class="coding", prompt="Fix any failing tests, ensuring they test the right behavior."]
start -> analyze -> plan -> approve
approve -> generate [label="Approve"]
approve -> plan [label="Revise"]
generate -> run -> fix -> run
run -> exit [label="All pass"]
}
sortOrder: 2
---
The Test Generator workflow reads your source code, plans a comprehensive test suite, and writes tests that actually pass — with a human checkpoint to approve the plan before generation begins.
## How it works
1. **Analyze Source** — A frontier model reads the codebase and identifies all public functions, methods, and types that need test coverage.
2. **Plan Tests** — Generates a structured test plan covering happy paths, edge cases, boundary values, and error conditions.
3. **Human Approval** — The plan is presented for review. You can approve it or send it back for revision.
4. **Generate & Validate** — Tests are written following your project's conventions, then executed. Any failures are automatically fixed in a retry loop.
## Why a workflow beats a single prompt
A single "write tests" prompt often produces tests that don't compile or test the wrong things. By separating analysis, planning, and generation into distinct stages — and adding a human gate — this workflow produces tests that are both comprehensive and correct.

View file

@ -3,27 +3,50 @@ import "../styles/global.css";
interface Props {
title: string;
description?: string;
}
const { title } = Astro.props;
const { title, description = "Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters." } = Astro.props;
---
<!doctype html>
<html lang="en" class="bg-navy-950 text-ice-50 antialiased">
<html lang="en" class="bg-navy-950 text-ice-50 antialiased overflow-x-hidden">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Fabro is an open-source AI software factory for expert engineering teams. Ship production code through deterministic workflows, built-in verification, and full observability." />
<meta name="description" content={description} />
<meta name="application-name" content="Fabro" />
<meta name="apple-mobile-web-app-title" content="Fabro" />
<link rel="canonical" href={`https://fabro.sh${Astro.url.pathname}`} />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content="https://fabro.sh/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://fabro.sh${Astro.url.pathname}`} />
<meta property="og:site_name" content="Fabro" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content="https://fabro.sh/og-image.png" />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="630" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&family=Sora:wght@400;500;600;700;800&display=swap"
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Lexend:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap"
rel="stylesheet"
/>
<title>{title}</title>
</head>
<body class="min-h-screen">
<body class="min-h-screen overflow-x-hidden">
<slot />
</body>
</html>

View file

@ -0,0 +1,18 @@
export const langIcons: Record<string, { label: string; path: string }> = {
python: {
label: "Python",
path: "M12 1.5c-5.1 0-4.8 2.2-4.8 2.2l.006 2.3H12.4v.7H4.8S1.5 6.3 1.5 11.5s2.9 5 2.9 5h1.7V14.1s-.1-2.9 2.8-2.9h4.9s2.8 0 2.8-2.7V3.8s.4-2.3-4.6-2.3zm-2.7 1.3a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zM12 22.5c5.1 0 4.8-2.2 4.8-2.2l-.006-2.3H11.6v-.7h7.6s3.3.4 3.3-4.8-2.9-5-2.9-5h-1.7v2.4s.1 2.9-2.8 2.9h-4.9s-2.8 0-2.8 2.7v4.7s-.4 2.3 4.6 2.3zm2.7-1.3a.9.9 0 1 1 0-1.8.9.9 0 0 1 0 1.8z",
},
rust: {
label: "Rust",
path: "M23.8 14.1l-1.2-.7a10.5 10.5 0 0 0 0-2.8l1.2-.7a.3.3 0 0 0 .1-.4 12 12 0 0 0-2.6-4.5.3.3 0 0 0-.4 0l-1.2.7a10.2 10.2 0 0 0-2.4-1.4V3a.3.3 0 0 0-.2-.3 12 12 0 0 0-5.2 0 .3.3 0 0 0-.2.3v1.3a10.2 10.2 0 0 0-2.4 1.4l-1.2-.7a.3.3 0 0 0-.4 0A12 12 0 0 0 5.1 9.5a.3.3 0 0 0 .1.4l1.2.7a10.5 10.5 0 0 0 0 2.8l-1.2.7a.3.3 0 0 0-.1.4 12 12 0 0 0 2.6 4.5.3.3 0 0 0 .4 0l1.2-.7c.7.6 1.5 1 2.4 1.4V21a.3.3 0 0 0 .2.3 12 12 0 0 0 5.2 0 .3.3 0 0 0 .2-.3v-1.3c.9-.4 1.7-.8 2.4-1.4l1.2.7a.3.3 0 0 0 .4 0 12 12 0 0 0 2.6-4.5.3.3 0 0 0-.1-.4zM12 16.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9z",
},
typescript: {
label: "TypeScript",
path: "M1.5 1.5h21v21h-21V1.5zm10.2 10.2v-1.8h6.6v1.8h-2.3v6.6h-2v-6.6h-2.3zm-4.1-1.8h2v4.5c0 .7.1 1.2.3 1.5.4.5 1 .8 1.8.8s1.4-.3 1.8-.8c.2-.3.3-.8.3-1.5V9.9h2v4.7c0 1-.2 1.8-.7 2.4-.7.9-1.8 1.3-3.4 1.3s-2.7-.4-3.4-1.3c-.5-.6-.7-1.4-.7-2.4V9.9z",
},
ruby: {
label: "Ruby",
path: "M20.6 18.3L21.7 5l-5.5 1.5-2.5-3.5-3 3.2L4.3 3 3 10.5l3.3 2-3 4.2 5.3.8 1.5 4 4.5-2.8 4 3.6 2-4z",
},
};

View file

@ -0,0 +1,60 @@
import { instance } from "@viz-js/viz";
let vizInstance: Awaited<ReturnType<typeof instance>> | undefined;
async function getViz() {
if (!vizInstance) {
vizInstance = await instance();
}
return vizInstance;
}
/**
* Renders a Graphviz DOT string to an SVG string, styled to match the site's
* teal-on-navy palette. The returned SVG has no fixed dimensions (uses viewBox)
* so it can be sized by its container.
*/
export async function renderWorkflow(dot: string): Promise<string> {
const viz = await getViz();
const styledDot = injectGraphStyle(dot);
let svg = viz.renderString(styledDot, { format: "svg", engine: "dot" });
// Make SVG responsive: remove fixed width/height, keep viewBox
svg = svg.replace(/\s*width="[^"]*"/, "");
svg = svg.replace(/\s*height="[^"]*"/, "");
return svg;
}
/**
* Injects graph-level styling attributes into the DOT string so the rendered
* SVG uses the site's color palette without post-processing.
*/
function injectGraphStyle(dot: string): string {
const styleBlock = `
bgcolor="transparent"
node [
fontname="Space Grotesk"
fontsize=13
fontcolor="#e8edf3"
style="filled"
fillcolor="#141c2f"
color="#357f9e"
penwidth=1.5
shape=box
margin="0.15,0.1"
]
edge [
color="#4b5768"
fontname="DM Sans"
fontsize=10
fontcolor="#a8b5c5"
arrowsize=0.7
penwidth=1.2
]
`;
// Insert style after the opening brace of the digraph
return dot.replace(/\{/, `{\n${styleBlock}\n`);
}

View file

@ -0,0 +1,64 @@
---
import Layout from "../../layouts/Layout.astro";
import Nav from "../../components/Nav.astro";
import Footer from "../../components/Footer.astro";
import PageScripts from "../../components/PageScripts.astro";
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
const formatDate = (date: Date) =>
date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
const wordCount = post.body?.split(/\s+/).length ?? 0;
const readingTime = Math.max(1, Math.round(wordCount / 250));
---
<Layout title={`${post.data.title} — Fabro`} description={post.data.description}>
<Nav currentPage="blog" />
<main>
<article class="relative pt-24 sm:pt-28 pb-24">
<div class="relative mx-auto max-w-3xl px-6">
<!-- Back link -->
<a href="/blog" class="reveal inline-flex items-center gap-2 text-sm text-ice-300/60 hover:text-teal-300 transition-colors mb-10 group">
<svg class="h-4 w-4 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<polyline points="15,18 9,12 15,6" />
</svg>
Blog
</a>
<!-- Header -->
<header class="mb-10 border-b border-navy-800/40 pb-8">
<h1 class="reveal reveal-d1 font-display text-3xl font-bold tracking-tight text-ice-50 sm:text-4xl leading-[1.15]">
{post.data.title}
</h1>
<div class="reveal reveal-d2 mt-4 flex items-center gap-3 text-sm text-ice-300/50">
<time class="font-mono">{formatDate(post.data.date)}</time>
<span>&middot;</span>
<span>{post.data.author}</span>
<span>&middot;</span>
<span>{readingTime} min read</span>
</div>
</header>
<!-- Content -->
<div class="reveal reveal-d3 prose">
<Content />
</div>
</div>
</article>
</main>
<Footer />
<PageScripts />
</Layout>

View file

@ -0,0 +1,82 @@
---
import Layout from "../../layouts/Layout.astro";
import Nav from "../../components/Nav.astro";
import Footer from "../../components/Footer.astro";
import PageScripts from "../../components/PageScripts.astro";
import { getCollection } from "astro:content";
const posts = (await getCollection("blog")).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
const [featured, ...older] = posts;
const formatDate = (date: Date) =>
date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
---
<Layout title="Blog — Fabro" description="News, updates, and insights from the Fabro team.">
<Nav currentPage="blog" />
<main>
<!-- Header — compact, content-forward -->
<section class="relative pt-24 sm:pt-28 pb-10">
<div class="absolute inset-0 noise-overlay"></div>
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
<div class="relative mx-auto max-w-3xl px-6">
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
Blog
</h1>
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
News, updates, and insights from the Fabro team.
</p>
</div>
</section>
<!-- Featured post -->
{featured && (
<section class="relative pb-8">
<div class="mx-auto max-w-3xl px-6">
<a href={`/blog/${featured.id}`} class="reveal reveal-d2 group block">
<article class="rounded-xl border border-teal-700/30 bg-gradient-to-br from-navy-800/40 to-navy-900/20 p-8 relative overflow-hidden transition-all duration-400 hover:border-teal-700/50 hover:shadow-[0_0_40px_-8px_rgba(53,127,158,0.15)]">
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/40 to-transparent" />
<time class="text-xs font-mono text-teal-500/70">{formatDate(featured.data.date)}</time>
<h2 class="mt-3 font-display text-2xl font-bold text-ice-50 group-hover:text-teal-300 transition-colors sm:text-3xl">
{featured.data.title}
</h2>
<p class="mt-3 text-base leading-relaxed text-ice-300">{featured.data.description}</p>
<div class="mt-5 flex items-center gap-3">
<span class="text-sm text-ice-300/50">By {featured.data.author}</span>
<span class="text-ice-300/30">&middot;</span>
<span class="text-sm font-medium text-teal-500 group-hover:text-teal-300 transition-colors">
Read &rarr;
</span>
</div>
</article>
</a>
</div>
</section>
)}
<!-- Older posts -->
{older.length > 0 && (
<section class="relative pb-24">
<div class="mx-auto max-w-3xl px-6 space-y-1">
{older.map((post, i) => (
<a href={`/blog/${post.id}`} class={`reveal reveal-d${Math.min(i + 3, 5)} group flex items-baseline gap-4 py-4 border-b border-navy-800/40 transition-colors hover:border-teal-700/30`}>
<time class="shrink-0 w-28 text-xs font-mono text-ice-300/40">{formatDate(post.data.date)}</time>
<span class="font-display text-base font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">{post.data.title}</span>
<span class="hidden sm:inline text-sm text-ice-300/50">&mdash; {post.data.description}</span>
</a>
))}
</div>
</section>
)}
{older.length === 0 && <div class="pb-24" />}
</main>
<Footer />
<PageScripts />
</Layout>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,258 @@
---
import Layout from "../layouts/Layout.astro";
import Nav from "../components/Nav.astro";
import Footer from "../components/Footer.astro";
import PageScripts from "../components/PageScripts.astro";
import { getCollection } from "astro:content";
const allItems = await getCollection("roadmap");
const byDate = (status: string) =>
allItems
.filter((item) => item.data.status === status)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const formatDate = (date: Date) =>
date.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: "UTC" });
const shipped = byDate("shipped");
const SHIPPED_VISIBLE = 3;
const shippedVisible = shipped.slice(0, SHIPPED_VISIBLE);
const shippedHidden = shipped.slice(SHIPPED_VISIBLE);
const building = byDate("building");
const next = byDate("next");
const statusConfig = {
building: {
dot: "bg-amber",
border: "via-amber/30",
label: "text-amber",
track: "bg-amber/30",
cardBorder: "border-amber/15",
cardBg: "bg-amber/5",
},
next: {
dot: "bg-teal-500",
border: "via-teal-500/30",
label: "text-teal-500",
track: "bg-teal-500/30",
cardBorder: "border-teal-500/15",
cardBg: "bg-teal-500/5",
},
};
---
<Layout title="Roadmap — Fabro">
<Nav currentPage="roadmap" />
<main>
<!-- Header -->
<section class="relative overflow-hidden pt-24 sm:pt-28 pb-10">
<div class="absolute inset-0 noise-overlay"></div>
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
<div class="relative mx-auto max-w-3xl px-6">
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
Roadmap
</h1>
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
What we've shipped, what we're building, and where Fabro is going next.
</p>
</div>
</section>
<!-- Shipped — compact rows -->
<section class="relative pb-12">
<div class="mx-auto max-w-3xl px-6">
<div class="reveal flex items-center gap-4">
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
<svg class="h-4 w-4 text-mint" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<polyline points="20,6 9,17 4,12" />
</svg>
<span class="font-display text-sm font-semibold uppercase tracking-wider text-mint">Shipped</span>
</div>
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
</div>
<div class="mt-6 space-y-0">
{shippedVisible.map((item, i) => (
<div class={`reveal reveal-d${i + 1} flex items-center gap-4 py-3 border-b border-navy-800/40`}>
<span class="shrink-0 w-20 text-xs font-mono text-mint/60">{item.data.date && formatDate(item.data.date)}</span>
<span class="font-display text-sm font-semibold text-ice-50">{item.data.title}</span>
<span class="shipped-info relative ml-auto shrink-0">
<svg class="h-4 w-4 text-ice-300/30 hover:text-ice-300/60 transition-colors cursor-help" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span class="shipped-info-tip">{item.data.description}</span>
</span>
</div>
))}
</div>
{shippedHidden.length > 0 && (
<div class="reveal reveal-d4">
<button
id="shipped-toggle"
class="group mt-4 flex items-center gap-2 text-sm text-ice-300/50 hover:text-mint transition-colors cursor-pointer"
aria-expanded="false"
aria-controls="shipped-older"
>
<svg
id="shipped-chevron"
class="h-4 w-4 transition-transform duration-300"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
viewBox="0 0 24 24"
>
<polyline points="6,9 12,15 18,9" />
</svg>
<span id="shipped-toggle-label">{shippedHidden.length} older features</span>
</button>
<div
id="shipped-older"
class="grid overflow-hidden transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
style="grid-template-rows: 0fr;"
>
<div class="min-h-0">
<div class="space-y-0">
{shippedHidden.map((item) => (
<div class="flex items-center gap-4 py-3 border-b border-navy-800/40">
<span class="shrink-0 w-20 text-xs font-mono text-mint/60">{item.data.date && formatDate(item.data.date)}</span>
<span class="font-display text-sm font-semibold text-ice-50">{item.data.title}</span>
<span class="shipped-info relative ml-auto shrink-0">
<svg class="h-4 w-4 text-ice-300/30 hover:text-ice-300/60 transition-colors cursor-help" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span class="shipped-info-tip">{item.data.description}</span>
</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
</section>
<!-- Building — only render if items exist -->
{building.length > 0 && (
<section class="relative pb-12">
<div class="mx-auto max-w-3xl px-6">
<div class="reveal flex items-center gap-4">
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
<div class="h-3 w-3 rounded-full bg-amber animate-pulse-glow" />
<span class="font-display text-sm font-semibold uppercase tracking-wider text-amber">Building</span>
</div>
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
</div>
<div class="relative mt-8 ml-5">
<div class="absolute top-0 bottom-0 left-0 w-px bg-amber/30" />
<div class="space-y-0">
{building.map((item, i) => (
<div class={`reveal reveal-d${Math.min(i + 1, 5)} relative pl-10 pb-8 group`}>
<div class="absolute left-0 top-1.5 -translate-x-1/2 z-10">
<div class="h-2.5 w-2.5 rounded-full bg-amber ring-4 ring-navy-950 transition-all group-hover:scale-125" />
</div>
<div class="rounded-xl border border-amber/15 bg-amber/5 p-6 relative overflow-hidden transition-all duration-400 hover:border-amber/25 hover:bg-amber/8">
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-amber/30 to-transparent" />
<h3 class="font-display text-base font-semibold text-ice-50">{item.data.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-ice-300">{item.data.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
)}
<!-- Next — only render if items exist -->
{next.length > 0 && (
<section class="relative pb-24">
<div class="mx-auto max-w-3xl px-6">
<div class="reveal flex items-center gap-4">
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
<svg class="h-4 w-4 text-teal-500" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<polyline points="12,6 12,12 16,14" />
</svg>
<span class="font-display text-sm font-semibold uppercase tracking-wider text-teal-500">Next</span>
</div>
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
</div>
<div class="relative mt-8 ml-5">
<div class="absolute top-0 bottom-0 left-0 w-px bg-teal-500/30" />
<div class="space-y-0">
{next.map((item, i) => (
<div class={`reveal reveal-d${Math.min(i + 1, 5)} relative pl-10 pb-8 group`}>
<div class="absolute left-0 top-1.5 -translate-x-1/2 z-10">
<div class="h-2.5 w-2.5 rounded-full bg-teal-500 ring-4 ring-navy-950 transition-all group-hover:scale-125" />
</div>
<div class="rounded-xl border border-teal-500/15 bg-teal-500/5 p-6 relative overflow-hidden transition-all duration-400 hover:border-teal-500/25 hover:bg-teal-500/8">
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/30 to-transparent" />
<h3 class="font-display text-base font-semibold text-ice-50">{item.data.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-ice-300">{item.data.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
)}
<!-- CTA -->
<section class="relative overflow-hidden border-t border-navy-800 py-24">
<div class="absolute inset-0 grid-overlay"></div>
<div class="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[400px] w-[400px] rounded-full bg-teal-700/6 blur-[120px]"></div>
<div class="relative mx-auto max-w-2xl px-6 text-center">
<h2 class="reveal font-display text-3xl font-bold tracking-tight text-ice-50">
Shape what we build
</h2>
<p class="reveal reveal-d1 mt-4 text-lg leading-relaxed text-ice-300">
Have a feature request or want to influence priorities? Join the conversation.
</p>
<div class="reveal reveal-d2 mt-8 flex items-center justify-center gap-4">
<a
href="https://github.com/fabro-sh/fabro/discussions"
class="rounded-xl bg-teal-700 px-7 py-3.5 text-sm font-semibold text-white transition-all hover:bg-teal-500 hover:shadow-[0_0_24px_-4px_rgba(53,127,158,0.5)]"
>
Join Discussions
</a>
<a
href="/discord"
class="rounded-xl border border-navy-600 px-7 py-3.5 text-sm font-semibold text-ice-300 transition-all hover:border-teal-700/60 hover:text-ice-50"
>
Discord
</a>
</div>
</div>
</section>
</main>
<Footer />
<PageScripts />
<script>
// Shipped older items toggle
const toggle = document.getElementById("shipped-toggle");
const older = document.getElementById("shipped-older");
const chevron = document.getElementById("shipped-chevron");
if (toggle && older && chevron) {
toggle.addEventListener("click", () => {
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", String(!isExpanded));
older.style.gridTemplateRows = isExpanded ? "0fr" : "1fr";
chevron.style.transform = isExpanded ? "" : "rotate(180deg)";
});
}
</script>
</Layout>

View file

@ -0,0 +1,192 @@
---
import Layout from "../../layouts/Layout.astro";
import Nav from "../../components/Nav.astro";
import Footer from "../../components/Footer.astro";
import PageScripts from "../../components/PageScripts.astro";
import { getCollection, render } from "astro:content";
import { langIcons } from "../../lib/langIcons";
import { renderWorkflow } from "../../lib/renderWorkflow";
export async function getStaticPaths() {
const entries = (await getCollection("showcase")).sort(
(a, b) => a.data.sortOrder - b.data.sortOrder
);
return entries.map((entry, i) => ({
params: { slug: entry.id },
props: {
entry,
prev: entries[i - 1] ?? null,
next: entries[i + 1] ?? null,
},
}));
}
const { entry, prev, next } = Astro.props;
const { Content } = await render(entry);
const workflowSvg = await renderWorkflow(entry.data.workflow);
---
<Layout title={`${entry.data.title} — Showcase — Fabro`} description={entry.data.description}>
<Nav currentPage="showcase" />
<main>
<article class="relative pt-24 sm:pt-28 pb-24">
<div class="relative mx-auto max-w-3xl px-6">
<!-- Back link -->
<a href="/showcase" class="reveal inline-flex items-center gap-2 text-sm text-ice-300/60 hover:text-teal-300 transition-colors mb-10 group">
<svg class="h-4 w-4 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<polyline points="15,18 9,12 15,6" />
</svg>
Showcase
</a>
<!-- Header -->
<header class="mb-10">
<h1 class="reveal reveal-d1 font-display text-3xl font-bold tracking-tight text-ice-50 sm:text-4xl leading-[1.15]">
{entry.data.title}
</h1>
<p class="reveal reveal-d2 mt-3 text-lg text-ice-300">
{entry.data.description}
</p>
{/* Compact metadata strip */}
<div class="reveal reveal-d2 mt-5 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-ice-300/60">
{entry.data.languages.map((lang) => {
const icon = langIcons[lang];
return icon ? (
<span class="flex items-center gap-1.5" title={icon.label}>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d={icon.path} />
</svg>
{icon.label}
</span>
) : null;
})}
<span class="text-ice-300/20">|</span>
{entry.data.models.map((model) => (
<span class="font-mono text-xs text-teal-300/70">{model}</span>
))}
<span class="text-ice-300/20">|</span>
{entry.data.skills.map((skill) => (
<span class="font-mono text-xs text-ice-300/50">{skill}</span>
))}
</div>
{/* GitHub button */}
<div class="reveal reveal-d3 mt-5">
<a
href={entry.data.github}
class="inline-flex items-center gap-2 rounded-lg border border-navy-600 px-4 py-2 text-sm font-medium text-ice-300 transition-all hover:border-teal-700/60 hover:text-ice-50"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
View on GitHub
</a>
</div>
</header>
<!-- Workflow graph — hero element -->
<section class="reveal-scale reveal-d3 mb-10">
<h2 class="font-display text-lg font-semibold text-ice-50 mb-4">Workflow</h2>
<div class="showcase-graph-hero rounded-xl border border-navy-800/60 bg-navy-950/80 p-6 flex items-center justify-center overflow-x-auto" set:html={workflowSvg} />
</section>
<!-- Prompt section -->
<section class="reveal reveal-d4 mb-10">
<h2 class="font-display text-lg font-semibold text-ice-50 mb-4">Prompt</h2>
<div class="rounded-xl border border-navy-800/60 bg-navy-900/30 p-6 relative">
<div
id="prompt-container"
class="overflow-hidden transition-[max-height] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] max-h-[4.5em]"
>
<p class="font-mono text-sm leading-relaxed text-ice-300/80 whitespace-pre-wrap">{entry.data.prompt}</p>
</div>
<button
id="prompt-toggle"
class="group mt-4 flex items-center gap-2 text-sm text-ice-300/50 hover:text-teal-300 transition-colors cursor-pointer"
aria-expanded="false"
>
<svg
id="prompt-chevron"
class="h-4 w-4 transition-transform duration-300"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
viewBox="0 0 24 24"
>
<polyline points="6,9 12,15 18,9" />
</svg>
<span id="prompt-toggle-label">Show full prompt</span>
</button>
</div>
</section>
<!-- About (rendered markdown body) -->
<section class="reveal-left reveal-d5">
<h2 class="font-display text-lg font-semibold text-ice-50 mb-4">About</h2>
<div class="prose">
<Content />
</div>
</section>
<!-- Prev / Next navigation -->
{(prev || next) && (
<nav class="reveal reveal-d5 mt-16 grid gap-4 sm:grid-cols-2 border-t border-navy-800/40 pt-8">
{prev ? (
<a href={`/showcase/${prev.id}`} class="group flex items-start gap-3 rounded-lg border border-navy-800/60 bg-navy-900/30 p-4 transition-all hover:border-teal-700/40">
<svg class="mt-0.5 h-5 w-5 shrink-0 text-ice-300/40 transition-transform group-hover:-translate-x-1 group-hover:text-teal-300" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<polyline points="15,18 9,12 15,6" />
</svg>
<div>
<span class="text-xs text-ice-300/40">Previous</span>
<span class="block font-display text-sm font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">{prev.data.title}</span>
</div>
</a>
) : <div />}
{next ? (
<a href={`/showcase/${next.id}`} class="group flex items-start gap-3 rounded-lg border border-navy-800/60 bg-navy-900/30 p-4 transition-all hover:border-teal-700/40 text-right sm:flex-row-reverse">
<svg class="mt-0.5 h-5 w-5 shrink-0 text-ice-300/40 transition-transform group-hover:translate-x-1 group-hover:text-teal-300" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<polyline points="9,6 15,12 9,18" />
</svg>
<div>
<span class="text-xs text-ice-300/40">Next</span>
<span class="block font-display text-sm font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">{next.data.title}</span>
</div>
</a>
) : <div />}
</nav>
)}
</div>
</article>
</main>
<Footer />
<PageScripts />
<script>
const toggle = document.getElementById("prompt-toggle");
const container = document.getElementById("prompt-container");
const chevron = document.getElementById("prompt-chevron");
const label = document.getElementById("prompt-toggle-label");
if (toggle && container && chevron && label) {
toggle.addEventListener("click", () => {
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", String(!isExpanded));
container.style.maxHeight = isExpanded ? "4.5em" : `${container.scrollHeight}px`;
chevron.style.transform = isExpanded ? "" : "rotate(180deg)";
label.textContent = isExpanded ? "Show full prompt" : "Hide prompt";
});
}
</script>
</Layout>
<style>
.showcase-graph-hero :global(svg) {
max-width: 100%;
height: auto;
}
</style>

View file

@ -0,0 +1,105 @@
---
import Layout from "../../layouts/Layout.astro";
import Nav from "../../components/Nav.astro";
import Footer from "../../components/Footer.astro";
import PageScripts from "../../components/PageScripts.astro";
import { getCollection } from "astro:content";
import { langIcons } from "../../lib/langIcons";
import { renderWorkflow } from "../../lib/renderWorkflow";
const entries = (await getCollection("showcase")).sort(
(a, b) => a.data.sortOrder - b.data.sortOrder
);
const workflowSvgs = new Map<string, string>();
for (const entry of entries) {
workflowSvgs.set(entry.id, await renderWorkflow(entry.data.workflow));
}
---
<Layout title="Showcase — Fabro" description="Projects and recipes built with Fabro workflows.">
<Nav currentPage="showcase" />
<main>
<!-- Header -->
<section class="relative pt-24 sm:pt-28 pb-10">
<div class="absolute inset-0 noise-overlay"></div>
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
<div class="relative mx-auto max-w-4xl px-6">
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
Showcase
</h1>
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
Reproducible workflow recipes built with Fabro. Clone, customize, and run.
</p>
</div>
</section>
<!-- Card grid -->
<section class="relative pb-24">
<div class="mx-auto max-w-4xl px-6">
<div class="grid gap-6 sm:grid-cols-2">
{entries.map((entry, i) => (
<a
href={`/showcase/${entry.id}`}
class={`reveal reveal-d${Math.min(i + 2, 5)} glow-card group block rounded-xl border border-navy-800/60 bg-navy-900/30 overflow-hidden transition-all duration-400 hover:border-teal-700/40 hover:shadow-[0_0_40px_-8px_rgba(53,127,158,0.12)]`}
>
{/* Workflow graph thumbnail */}
<div class="relative h-36 bg-navy-950/80 flex items-center justify-center p-4 overflow-hidden">
<div class="showcase-graph-thumb w-full h-full flex items-center justify-center opacity-60 group-hover:opacity-90 transition-opacity duration-400" set:html={workflowSvgs.get(entry.id)} />
<div class="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/20 to-transparent" />
</div>
{/* Card body */}
<div class="p-6">
<h2 class="font-display text-lg font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">
{entry.data.title}
</h2>
<p class="mt-2 text-sm leading-relaxed text-ice-300/70 line-clamp-2">
{entry.data.description}
</p>
{/* Language icons */}
<div class="mt-4 flex items-center gap-3">
{entry.data.languages.map((lang) => {
const icon = langIcons[lang];
return icon ? (
<span class="flex items-center gap-1.5 text-xs text-ice-300/50" title={icon.label}>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d={icon.path} />
</svg>
{icon.label}
</span>
) : null;
})}
</div>
{/* Tags */}
<div class="mt-3 flex flex-wrap gap-1.5">
{entry.data.tags.map((tag) => (
<span class="rounded-full border border-navy-800 bg-navy-900/60 px-3 py-1 text-xs text-ice-300/70">
{tag}
</span>
))}
</div>
</div>
</a>
))}
</div>
</div>
</section>
</main>
<Footer />
<PageScripts />
</Layout>
<style>
.showcase-graph-thumb :global(svg) {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
</style>

View file

@ -18,9 +18,9 @@
--color-amber: #F0A45B;
--color-coral: #E86B6B;
--font-display: "Sora", ui-sans-serif, system-ui, sans-serif;
--font-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--font-display: "Outfit", ui-sans-serif, system-ui, sans-serif;
--font-sans: "Lexend", ui-sans-serif, system-ui, sans-serif;
--font-mono: "Fira Code", ui-monospace, monospace;
--animate-float: float 8s ease-in-out infinite;
--animate-pulse-glow: pulse-glow 4s ease-in-out infinite;
@ -51,14 +51,39 @@
animation: gradient-shift 6s ease infinite;
}
/* Subtle dot grid overlay */
.dot-grid {
background-image: radial-gradient(var(--color-teal-500) 0.5px, transparent 0.5px);
background-size: 24px 24px;
opacity: 0.05;
/* Cross-hatch grid overlay */
.grid-overlay {
background-image:
linear-gradient(var(--color-navy-800) 1px, transparent 1px),
linear-gradient(90deg, var(--color-navy-800) 1px, transparent 1px);
background-size: 48px 48px;
opacity: 0.12;
pointer-events: none;
}
/* Noise texture */
.noise-overlay {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
}
/* Scanlines */
.scanlines {
background-image: repeating-linear-gradient(
0deg,
rgba(255, 255, 255, 0.015) 0px,
rgba(255, 255, 255, 0.015) 1px,
transparent 1px,
transparent 4px
);
pointer-events: none;
}
/* Prevent grid/flex blowout from wide children (SVGs, pre blocks) */
.grid > * {
min-width: 0;
}
/* Feature card hover glow */
.glow-card {
transition: border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s ease;
@ -69,7 +94,7 @@
transform: translateY(-2px);
}
/* Scroll reveal */
/* Scroll reveal — fade up (default) */
.reveal {
opacity: 0;
transform: translateY(28px);
@ -79,14 +104,195 @@
opacity: 1;
transform: translateY(0);
}
/* Scroll reveal — slide from left */
.reveal-left {
opacity: 0;
transform: translateX(-40px);
transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.reveal-left.revealed {
opacity: 1;
transform: translateX(0);
}
/* Scroll reveal — slide from right */
.reveal-right {
opacity: 0;
transform: translateX(40px);
transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.reveal-right.revealed {
opacity: 1;
transform: translateX(0);
}
/* Scroll reveal — scale up */
.reveal-scale {
opacity: 0;
transform: scale(0.95);
transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.reveal-scale.revealed {
opacity: 1;
transform: scale(1);
}
/* Stagger delays */
.reveal-d1 { transition-delay: 100ms; }
.reveal-d2 { transition-delay: 200ms; }
.reveal-d3 { transition-delay: 300ms; }
.reveal-d4 { transition-delay: 400ms; }
.reveal-d5 { transition-delay: 500ms; }
/* Section gradient divider */
/* Section gradient divider (used sparingly) */
.section-divider {
height: 1px;
background: linear-gradient(to right, transparent, var(--color-navy-600), transparent);
}
/* Trace bar animation */
.trace-bar {
transform: scaleX(0);
transform-origin: left;
transition: transform 1s cubic-bezier(0.22, 1, 0.36, 1);
}
.trace-bar.revealed {
transform: scaleX(1);
}
/* Lightbox transition */
.lightbox-overlay {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.lightbox-overlay.active {
opacity: 1;
pointer-events: auto;
}
.lightbox-overlay img {
transform: scale(0.95);
transition: transform 0.3s ease;
}
.lightbox-overlay.active img {
transform: scale(1);
}
/* Workflow graph path draw-in */
@keyframes draw-path {
to { stroke-dashoffset: 0; }
}
.graph-edge {
animation: draw-path 1.2s cubic-bezier(0.22, 1, 0.36, 1) forwards;
animation-play-state: paused;
}
.graph-draw .graph-edge {
animation-play-state: running;
}
/* Install widget tabs */
.install-tab {
color: var(--color-ice-300);
background: transparent;
cursor: pointer;
}
.install-tab:hover {
color: var(--color-ice-50);
}
.install-tab.active {
color: var(--color-teal-300);
background: var(--color-navy-800);
}
.install-panel {
display: none;
}
.install-panel.active {
display: block;
}
/* Blog prose styles */
.prose {
color: var(--color-ice-300);
line-height: 1.75;
}
.prose h1, .prose h2, .prose h3, .prose h4 {
color: var(--color-ice-50);
font-family: var(--font-display);
font-weight: 700;
margin-top: 2em;
margin-bottom: 0.75em;
}
.prose h2 { font-size: 1.5rem; }
.prose h3 { font-size: 1.25rem; }
.prose p { margin-bottom: 1.25em; }
.prose a {
color: var(--color-teal-300);
text-decoration: underline;
text-underline-offset: 2px;
transition: color 0.2s;
}
.prose a:hover { color: var(--color-teal-500); }
.prose ul, .prose ol { padding-left: 1.5em; margin-bottom: 1.25em; }
.prose li { margin-bottom: 0.5em; }
.prose ul { list-style-type: disc; }
.prose ol { list-style-type: decimal; }
.prose blockquote {
border-left: 3px solid var(--color-teal-700);
padding-left: 1em;
color: var(--color-ice-100);
font-style: italic;
margin: 1.5em 0;
}
.prose code {
font-family: var(--font-mono);
font-size: 0.875em;
background: var(--color-navy-800);
padding: 0.15em 0.4em;
border-radius: 0.25rem;
color: var(--color-teal-300);
}
.prose pre {
background: var(--color-navy-900);
border: 1px solid var(--color-navy-800);
border-radius: 0.75rem;
padding: 1.25em;
overflow-x: auto;
margin: 1.5em 0;
}
.prose pre code {
background: none;
padding: 0;
border-radius: 0;
}
.prose img {
border-radius: 0.75rem;
margin: 1.5em 0;
}
.prose hr {
border-color: var(--color-navy-800);
margin: 2em 0;
}
/* Shipped feature info tooltip */
.shipped-info-tip {
display: none;
position: absolute;
right: 0;
bottom: calc(100% + 8px);
width: 280px;
padding: 0.625rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid var(--color-navy-800);
background: var(--color-navy-900);
color: var(--color-ice-300);
font-family: var(--font-sans);
font-size: 0.8125rem;
font-weight: 400;
line-height: 1.5;
box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.4);
z-index: 20;
}
.shipped-info:hover .shipped-info-tip {
display: block;
}

View file

@ -0,0 +1,15 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"redirects": [
{
"source": "/discord",
"destination": "https://discord.gg/KE6w49Vg",
"statusCode": 302
},
{
"source": "/docs",
"destination": "https://docs.fabro.sh",
"statusCode": 302
}
]
}

View file

@ -41,9 +41,12 @@ fi
sed -i '' "s/^version = \"$current_version\"/version = \"$new_version\"/" "$CARGO_TOML"
echo "Updated $CARGO_TOML"
git add "$CARGO_TOML"
cargo update --workspace
echo "Updated Cargo.lock"
git add "$CARGO_TOML" Cargo.lock
git commit -m "Bump version to $new_version"
git tag "$tag"
git tag -a "$tag" -m "$tag"
git push origin main "$tag"
echo ""

View file

@ -43,6 +43,7 @@
"dependencies": {
"@astrojs/react": "^4.2.1",
"@tailwindcss/vite": "^4.2.1",
"@viz-js/viz": "^3.25.0",
"astro": "^5.9.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@ -1338,6 +1339,8 @@
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"marketing/@viz-js/viz": ["@viz-js/viz@3.25.0", "", {}, "sha512-dM7zAYMdf7mcRz5Kdb+YJb6+qv5Rjk0rPZ18gROdpMrP/3S7RFOp8uxybeiz5RypHrE1zo1vccA8Twh4mIcLZw=="],
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],

View file

@ -1,7 +1,7 @@
[api]
base_url = "http://api:3000"
[feature_flags]
[features]
session_sandboxes = false
[checkpoint]

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg" width="882" height="211" viewBox="0 0 1455 348">
<svg xmlns="http://www.w3.org/2000/svg" width="909" height="211" viewBox="0 0 1500 348">
<defs>
<linearGradient id="topGradient" gradientUnits="userSpaceOnUse" x1="1200" y1="0" x2="0" y2="400">
<stop offset="0" stop-color="#c4dede" />

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

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