fabro/lib/crates/fabro-workflow
fabro-sh-0530[bot] 01892185ff
Replace bare unwrap() with documented expect() across production runtim… (#415)
Audit and remediation pass enforcing the project's
no-panic-in-production policy. Every `unwrap()` on a mutex/RwLock in
reachable runtime code is replaced with `expect()` carrying a message
that explains *why* the lock cannot be poisoned (no code panics while
holding it). Bare `unreachable!()` and `panic!()` calls are updated with
messages that name the invariant being asserted. One genuine bug is
fixed in the process.

## What changed

**`unwrap()` → `expect()` on locks** (`fabro-core`, `fabro-oauth`,
`fabro-util`, `fabro-workflow/*`, `fabro-server`): Every
`Mutex`/`RwLock` `.unwrap()` in production paths now carries the
standard justification pattern: `"<name> mutex/RwLock should not be
poisoned: no code panics while holding this lock"`.

**`unreachable!()` and `panic!()` message quality**: Bare
`unreachable!()` calls in `subagent.rs`, `wait.rs`, `condition.rs`,
`event/convert.rs`, and `server.rs` now name the structural invariant
(e.g. "outer match arm already verified…"). The `panic!` in `tools.rs`
now includes the offending name and the expected format, making it
actionable.

**`sha_newtype` / `short_sha_newtype` in `run_files.rs` — actual bug
fix**: These helpers previously called `unwrap_or_else(|e| panic!(…))`
on git output, meaning a malformed SHA from a real git subprocess would
panic in a request handler. They now return `Result<T, ApiError>` and
propagate errors to callers, which in turn propagate with `?`. This is
the only change that alters observable behavior under failure.

**Demo-only panics in `fabro-server/src/demo/mod.rs`**: Panic messages
updated to clarify that these paths operate on hardcoded compile-time
constants, so the panic is a programming-error guard rather than a
runtime failure guard.

## Design note

The lock-poisoning `expect` messages all follow a single template so
reviewers can quickly verify the claim: if you ever add code that can
panic inside a lock guard scope, the message becomes a lie and that must
be caught in review. The uniformity is intentional.


### Fabro Details

<details>
<summary>Ran 0 stages in 64m 54s for $14.28</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **64m 54s** | **$14.28** | **0** |

</details>

<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>

```dot
digraph Goal {
    graph [
        goal="Complete the user-provided goal",
        rankdir=LR,
        max_node_visits=30
    ]

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

    work [
        label="Work",
        thread_id="goal",
        fidelity="full",
        max_visits=12,
        prompt="@prompts/continue.md"
    ]

    audit [
        label="Completion Audit",
        thread_id="goal",
        fidelity="full",
        goal_gate=true,
        retry_target="work",
        output_schema="routing",
        output_retries=2,
        max_visits=12,
        prompt="@prompts/audit.md"
    ]

    start -> work -> audit

    audit -> exit [label="Done", condition="outcome=succeeded"]
    audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
    audit -> work [label="No clear verdict"]
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-26 17:46:39 -04:00
..
src Replace bare unwrap() with documented expect() across production runtim… (#415) 2026-05-26 17:46:39 -04:00
tests Add provider-backed sandbox inventory API and rename SandboxProvider to… (#409) 2026-05-25 22:41:57 -04:00
Cargo.toml Add output_schema validation with same-context repair for agent and pro… (#374) 2026-05-23 19:45:16 -04:00
README.md refactor(workflow): remove retro stage (#230) 2026-05-09 10:18:20 -04:00

fabro-workflow

A DOT-based pipeline runner for multi-stage AI workflows. Define workflows as Graphviz digraph files and execute them with pluggable handlers, conditional routing, human-in-the-loop gates, parallel branching, retry policies, and checkpoint-based recovery.

Key Concepts

  • Graph -- A directed graph parsed from DOT syntax containing nodes, edges, and attributes. The graph carries a goal describing the pipeline's purpose.
  • Node -- A workflow step. Graphviz shapes map to handler types (e.g., Mdiamond = start, Msquare = exit, box = agent, tab = prompt, diamond = conditional, hexagon = human gate, component = parallel).
  • Edge -- A connection between nodes with optional condition, label, weight, and fidelity attributes that control routing.
  • Handler -- An async trait implementation that executes a node and returns an Outcome. Built-in handlers include StartHandler, ExitHandler, AgentHandler, PromptHandler, ConditionalHandler, HumanHandler, ParallelHandler, FanInHandler, CommandHandler, and SubWorkflowHandler.
  • Outcome -- The result of executing a handler, carrying a StageOutcome (Success, Fail, PartialSuccess, Retry, Skipped), optional routing hints (preferred_label, suggested_next_ids), and context updates.
  • Context -- A thread-safe key-value store shared across pipeline stages, supporting snapshots and isolated cloning for parallel branches.
  • Interviewer -- A trait for human-in-the-loop interactions. Implementations include AutoApproveInterviewer, QueueInterviewer, CallbackInterviewer, ConsoleInterviewer, and RecordingInterviewer.
  • Checkpoint -- A serializable snapshot of execution state (completed nodes, context values) for crash recovery and resume.

Pipeline Definition

Pipelines are defined using Graphviz DOT syntax:

digraph MyPipeline {
    graph [goal="Implement and validate a feature"]
    rankdir=LR
    node [shape=box, timeout="900s"]

    start     [shape=Mdiamond, label="Start"]
    exit      [shape=Msquare, label="Exit"]
    plan      [label="Plan", prompt="Plan the implementation"]
    implement [label="Implement", prompt="Implement the plan"]
    validate  [label="Validate", prompt="Run tests"]
    gate      [shape=diamond, label="Tests passing?"]

    start -> plan -> implement -> validate -> gate
    gate -> exit      [label="Yes", condition="outcome=succeeded"]
    gate -> implement [label="No", condition="outcome!=succeeded"]
}

Usage

Parsing and Validating a Pipeline

use fabro_workflow::operations::{create, CreateOptions};

let dot_source = r#"digraph Simple {
    graph [goal="Run tests"]
    start [shape=Mdiamond]
    exit  [shape=Msquare]
    work  [shape=box, prompt="Run the test suite"]
    start -> work -> exit
}"#;

let validated = create(dot_source, CreateOptions::default())
    .expect("pipeline should parse");
validated.raise_on_errors().expect("pipeline should validate");
let (graph, _, _) = validated.into_parts();
assert_eq!(graph.name, "Simple");
assert_eq!(graph.goal(), "Run tests");

operations::create parses the DOT source, applies built-in transforms (variable expansion, stylesheet application, preamble injection), and returns diagnostics through Validated.

Running a Pipeline

use fabro_workflow::operations::start;
use fabro_workflow::pipeline;

// Use `operations::start(...)` for the full initialize -> execute -> finalize flow.
// Use `pipeline::initialize(...)` + `pipeline::execute(...)` when you need partial lifecycle control.

Custom Handlers

Implement the Handler trait to add custom node behavior:

use arc_workflows::handler::Handler;
use arc_workflows::context::Context;
use arc_workflows::graph::{Graph, Node};
use arc_workflows::outcome::Outcome;
use arc_workflows::error::ArcError;
use async_trait::async_trait;
use std::path::Path;

struct MyHandler;

#[async_trait]
impl Handler for MyHandler {
    async fn execute(
        &self,
        node: &Node,
        context: &Context,
        graph: &Graph,
        run_dir: &Path,
    ) -> Result<Outcome, ArcError> {
        // Custom logic here
        Ok(Outcome::success())
    }
}

Model Stylesheets

CSS-like stylesheets control LLM model assignment with specificity-based cascading:

digraph Styled {
    graph [
        goal="Build feature",
        model_stylesheet="
            * { model: claude-sonnet-4-5;}
            .code { model: claude-opus-4-6; }
            #critical_review { model: gpt-5.2;}
        "
    ]
    // ...
}

Selectors by specificity: * (universal, 0) < shape (1) < .class (2) < #id (3). Explicit node attributes are never overridden.

Condition Expressions

Edge conditions use a simple expression syntax for routing:

outcome=succeeded
outcome!=failed
outcome=succeeded && context.tests_passed=true
my_flag

Clauses support =, !=, and bare key truthiness checks, joined with &&.

Human-in-the-Loop Gates

Nodes with shape=hexagon or type="human" pause execution for human input. Outgoing edge labels become selectable options, with accelerator key parsing for patterns like [A] Approve and F) Fix.

Parallel Execution

Nodes with shape=component fan out to branches concurrently. Configurable join policies: wait_all (default), first_success.

Checkpoints and Resume

The engine saves a checkpoint after each node. Resume from a checkpoint with engine.run_from_checkpoint(&graph, &config, &checkpoint).

Architecture

parser (DOT -> AST -> Graph)
  -> transform (variable expansion, stylesheet, preamble)
    -> validation (14 lint rules)
      -> engine (execution loop with retry, edge selection, goal gates)
        -> handler (pluggable node executors)
          -> interviewer (human-in-the-loop I/O)