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>
This commit is contained in:
Bryan Helmkamp 2026-03-13 22:38:25 -04:00
parent 8cb6c4f26f
commit 35bd79a2ed
No known key found for this signature in database
58 changed files with 323 additions and 174 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -184,7 +184,7 @@ Artifact data is persisted on the Git [metadata branch](/execution/checkpoints#m
```
refs/fabro/{run_id}
manifest.json
graph.dot
graph.fabro
checkpoint.json
artifacts/
response.plan.json

View file

@ -47,7 +47,7 @@ The `@` prefix tells the engine to read the file contents and use them as the pr
Prompts support `$variable` placeholders that expand at runtime. Currently the only built-in variable is `$goal`, which resolves to the graph-level `goal` attribute:
```dot title="pipeline.dot"
```dot title="pipeline.fabro"
digraph Pipeline {
graph [goal="Add a /health endpoint to the API server"]

View file

@ -2924,7 +2924,7 @@ components:
filename:
type: string
description: DOT graph filename.
example: fix_build.dot
example: fix_build.fabro
last_run:
$ref: "#/components/schemas/WorkflowLastRun"
schedule:
@ -2952,7 +2952,7 @@ components:
filename:
type: string
description: DOT graph filename.
example: fix_build.dot
example: fix_build.fabro
description:
type: string
description: Prose description of what the workflow does.
@ -4007,7 +4007,7 @@ components:
graph:
type: string
description: DOT graph filename.
example: fix_build.dot
example: fix_build.fabro
directory:
type: string
description: Working directory for the run.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,7 @@ Both modes parse the same DOT files, use the same execution engine, and support
You provide three inputs:
1. **Workflow graph** (`.dot`) — A Graphviz DOT file defining nodes, edges, and their attributes. This is the core of what Fabro executes. See [Workflows](/core-concepts/workflows).
1. **Workflow graph** (`.fabro`) — A Graphviz DOT file defining nodes, edges, and their attributes. This is the core of what Fabro executes. See [Workflows](/core-concepts/workflows).
2. **Run config** (`.toml`, optional) — Overrides for the default model, sandbox provider, setup commands, and variables. See [Run Configuration](/execution/run-configuration).
3. **API keys** (`.env`) — Provider credentials for LLM APIs. See [Quick Start](/getting-started/quick-start).

View file

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

View file

@ -15,7 +15,7 @@ Both modes use the same workflow engine, the same DOT files, and the same sandbo
| | Standalone | Server |
|---|---|---|
| **Command** | `fabro run workflow.dot` | `fabro serve` |
| **Command** | `fabro run workflow.fabro` | `fabro serve` |
| **Best for** | Local development, one-off runs, CI/CD | Production, team use, running at scale |
| **Execution** | Synchronous, one run per process | Asynchronous, queued with configurable concurrency |
| **Human-in-the-loop** | Terminal prompts | Web UI or HTTP endpoints |

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -44,7 +44,7 @@ The `Fabro-Checkpoint` trailer links each run branch commit to its metadata bran
The metadata branch (`refs/fabro/{run_id}`) is an orphan branch that stores structured run data using Git's object storage directly (via `git2`). It is initialized at run start with:
- **`manifest.json`** — Run metadata: run ID, graph name, node/edge counts, base SHA, and branch name
- **`graph.dot`** — The workflow DOT source as it was parsed
- **`graph.fabro`** — The workflow DOT source as it was parsed
After each node, the metadata branch is updated with:
@ -98,7 +98,7 @@ There are two ways to resume an interrupted run:
Resume from a `checkpoint.json` saved in the run directory:
```bash
fabro run workflow.dot --resume path/to/logs/checkpoint.json
fabro run workflow.fabro --resume path/to/logs/checkpoint.json
```
Fabro loads the checkpoint, restores the context and execution state, and continues from the next node after the checkpoint.
@ -115,7 +115,7 @@ This reads the checkpoint, manifest, and graph DOT from the metadata branch (`re
<Accordion title="What happens during resume">
1. Fabro reads `checkpoint.json` from the metadata branch
2. Reads `manifest.json` and `graph.dot` to reconstruct the workflow
2. Reads `manifest.json` and `graph.fabro` to reconstruct the workflow
3. Creates a fresh worktree attached to the existing run branch
4. Restores the full context, completed node list, retry counts, and failure signatures
5. If the checkpointed node used `full` fidelity, downgrades the first resumed node to `summary:high` (since the original conversation thread no longer exists in memory)

View file

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

View file

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

View file

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

View file

@ -140,7 +140,7 @@ cat ~/.fabro/runs/01JKXYZ.../live.json
Fabro uses the `tracing` crate to write structured logs to `~/.fabro/logs/YYYY-MM-DD.log`. Control the log level with the `FABRO_LOG` environment variable:
```bash
FABRO_LOG=debug fabro run workflow.dot
FABRO_LOG=debug fabro run workflow.fabro
```
| Level | What's logged |

View file

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

View file

@ -15,7 +15,7 @@ A run config requires two fields:
```toml title="run.toml"
version = 1
graph = "workflow.dot"
graph = "workflow.fabro"
goal = "Implement the login feature"
```
@ -32,7 +32,7 @@ Goal precedence: CLI `--goal` > TOML `goal` > DOT graph attribute.
```toml title="run.toml"
version = 1
goal = "Run the CI pipeline for $repo_name"
graph = "fabro/workflows/ci.dot"
graph = "fabro/workflows/ci.fabro"
directory = "/tmp/workdir"
[llm]
@ -286,7 +286,7 @@ language = "rust"
Variables can be used anywhere in the DOT file with `$name` syntax:
```dot title="c-i.dot"
```dot title="c-i.fabro"
digraph CI {
graph [goal="Run tests for $repo_name"]
clone [shape=parallelogram, script="git clone $repo_url repo"]
@ -394,8 +394,8 @@ The `graph` path is resolved relative to the TOML file's parent directory, not t
```
project/
runs/
ci.toml # graph = "ci.dot"
ci.dot
ci.toml # graph = "ci.fabro"
ci.fabro
```
Absolute paths are used as-is.

View file

@ -52,7 +52,7 @@ Workflows are defined in Graphviz DOT, a simple graph description language. Here
<img src="/images/plan-implement-workflow.svg" alt="Plan-Implement workflow graph" />
</Frame>
```dot title="plan-implement.dot"
```dot title="plan-implement.fabro"
digraph PlanImplement {
graph [goal="Plan, approve, implement, and simplify a change"]

View file

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

View file

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

View file

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

View file

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

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -56,7 +56,7 @@ A workflow that researches a topic before writing about it:
<img src="/images/brave-search-research.svg" alt="Research workflow: Start → Research → Summarize → Exit" />
</Frame>
```dot title="research.dot"
```dot title="research.fabro"
digraph Research {
graph [goal="Research and summarize a topic"]
rankdir=LR

View file

@ -25,7 +25,7 @@ description: "Run Fabro workflows in sandboxed Daytona cloud environments"
Set the sandbox provider in your run config TOML or via CLI flag:
```bash
fabro run workflow.dot --sandbox daytona
fabro run workflow.fabro --sandbox daytona
```
```toml title="run.toml"
@ -114,7 +114,7 @@ for your organization.
Connect to a running Daytona sandbox via SSH for live debugging:
```bash
fabro run workflow.dot --sandbox daytona --ssh
fabro run workflow.fabro --sandbox daytona --ssh
```
This creates temporary SSH credentials (valid for 60 minutes) and prints the connection command:
@ -136,7 +136,7 @@ Each sandbox gets a unique timestamped name (e.g. `fabro-20260307-143022-a3f2`)
To keep a sandbox alive for debugging:
```bash
fabro run workflow.dot --sandbox daytona --preserve-sandbox
fabro run workflow.fabro --sandbox daytona --preserve-sandbox
```
Or in the run config:

View file

@ -21,7 +21,7 @@ The exe.dev sandbox provider is **under development**. It requires building Fabr
## Configuration
```bash
fabro run workflow.dot --sandbox exe
fabro run workflow.fabro --sandbox exe
```
```toml title="run.toml"
@ -41,7 +41,7 @@ image = "my-custom-image:latest"
Connect to a running exe.dev sandbox via SSH for live debugging:
```bash
fabro run workflow.dot --sandbox exe --ssh
fabro run workflow.fabro --sandbox exe --ssh
```
This prints the SSH connection command so you can connect to the VM while the workflow runs.

View file

@ -21,7 +21,7 @@ The Sprites sandbox provider is **in progress** and not yet available for use. T
## Configuration
```bash
fabro run workflow.dot --sandbox sprites
fabro run workflow.fabro --sandbox sprites
```
```toml title="run.toml"

View file

@ -1,7 +1,7 @@
{
"name": "dot",
"scopeName": "source.dot",
"fileTypes": ["dot", "DOT", "gv"],
"fileTypes": ["dot", "DOT", "gv", "fabro"],
"firstLineMatch": "digraph.*",
"patterns": [
{

View file

@ -12,7 +12,7 @@ At the core of both modes is the `WorkflowRunEngine`. It parses the DOT graph, w
## CLI mode
```bash
fabro run workflow.dot --goal "Implement the login feature"
fabro run workflow.fabro --goal "Implement the login feature"
```
The CLI parses the workflow, creates the engine with a `ConsoleInterviewer`, and executes synchronously. Events are printed to stderr, progress is shown with terminal indicators, and human-in-the-loop questions are answered via interactive terminal prompts. When the run finishes, the process exits.

View file

@ -185,7 +185,7 @@ ca = "~/.fabro/tls/ca.crt"
## `[pull_request]`
Enable auto-PR globally so workflows open a GitHub pull request on successful completion — even when running with a `.dot` file instead of a `run.toml`.
Enable auto-PR globally so workflows open a GitHub pull request on successful completion — even when running with a `.fabro` file instead of a `run.toml`.
```toml title="cli.toml"
[pull_request]

View file

@ -36,7 +36,7 @@ CLI flags always override `cli.toml` values, which override hardcoded defaults.
## `fabro run`
Launch a workflow from a `.dot` workflow file or `.toml` task config.
Launch a workflow from a `.fabro` workflow file or `.toml` task config.
```bash
fabro run <WORKFLOW>
@ -46,7 +46,7 @@ fabro run --run-branch arc/run/abc123
| Argument / Flag | Description |
|---|---|
| `<WORKFLOW>` | Path to a `.dot` workflow file or `.toml` task config. Not required when using `--run-branch`. |
| `<WORKFLOW>` | Path to a `.fabro` workflow file or `.toml` task config. Not required when using `--run-branch`. |
| `--run-dir <DIR>` | Run output directory |
| `--dry-run` | Execute with a simulated LLM backend |
| `--preflight` | Validate run configuration without executing |
@ -343,14 +343,14 @@ fabro system df -v
Render a workflow DOT graph as SVG or PNG. Requires [Graphviz](https://graphviz.org) (`dot`) to be installed.
```bash
fabro graph workflow.dot
fabro graph workflow.dot --format png -o diagram.png
fabro graph workflow.fabro
fabro graph workflow.fabro --format png -o diagram.png
fabro graph run.toml --format svg
```
| Argument / Flag | Description |
|---|---|
| `<WORKFLOW>` | Path to a `.dot` workflow file or `.toml` run config (required) |
| `<WORKFLOW>` | Path to a `.fabro` workflow file or `.toml` run config (required) |
| `--format <FORMAT>` | Output format: `svg` (default) or `png` |
| `-o, --output <FILE>` | Output file path. Defaults to stdout. |
@ -374,28 +374,28 @@ fabro skill install --for user --dir claude
## `fabro validate`
Validate a workflow file without executing it. Reports errors and warnings. Accepts both `.dot` workflow files and `.toml` run configs.
Validate a workflow file without executing it. Reports errors and warnings. Accepts both `.fabro` workflow files and `.toml` run configs.
```bash
fabro validate workflow.dot
fabro validate workflow.fabro
fabro validate run.toml
```
| Argument | Description |
|---|---|
| `<WORKFLOW>` | Path to a `.dot` workflow file or `.toml` run config (required) |
| `<WORKFLOW>` | Path to a `.fabro` workflow file or `.toml` run config (required) |
## `fabro parse`
Parse a DOT file and print its AST as JSON. Useful for debugging workflow definitions.
```bash
fabro parse workflow.dot
fabro parse workflow.fabro
```
| Argument | Description |
|---|---|
| `<WORKFLOW>` | Path to the `.dot` workflow file (required) |
| `<WORKFLOW>` | Path to the `.fabro` workflow file (required) |
---
@ -409,7 +409,7 @@ fabro init
The command must be run inside a git repository. It creates:
- `fabro.toml` — project configuration with comments and a link to docs
- `fabro/workflows/hello/workflow.dot` — a simple greeting workflow
- `fabro/workflows/hello/workflow.fabro` — a simple greeting workflow
- `fabro/workflows/hello/workflow.toml` — run config for the hello workflow
## `fabro diff`

View file

@ -9,7 +9,7 @@ Fabro workflows are written in a subset of the [Graphviz DOT language](https://g
Every workflow is a `digraph` (directed graph) with a name and a body of statements:
```dot title="my-workflow.dot"
```dot title="my-workflow.fabro"
digraph MyWorkflow {
graph [goal="Describe the project"]
rankdir=LR
@ -345,7 +345,7 @@ Fabro validates workflows at parse time and reports diagnostics. Key rules:
## Complete example
```dot title="implement-feature.dot"
```dot title="implement-feature.fabro"
digraph ImplementFeature {
graph [
goal="Implement a feature with tests and code review",

View file

@ -16,7 +16,7 @@ The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to
| File | Format | When written | Description |
|---|---|---|---|
| `manifest.json` | JSON | Run start | Run metadata — `run_id`, `workflow_name`, `goal`, `start_time`, `node_count`, `edge_count`, `run_branch`, `base_sha`, `labels` |
| `graph.dot` | DOT | Run start | Copy of the workflow graph |
| `graph.fabro` | DOT | Run start | Copy of the workflow graph |
| `run.pid` | Text | Run start | Process ID of the running CLI process. Presence indicates the run is active; an orphaned file indicates a crash. |
| `run.toml` | TOML | Run start | Copy of the original workflow file (only when the workflow is defined in TOML) |
| `progress.jsonl` | JSONL | Continuous | Event stream — one JSON object per line for every significant event (stage starts, completions, tool calls, retries, etc.). See [Observability](/execution/observability) for the full event catalog. |
@ -82,7 +82,7 @@ fabro ps --filter workflow=my-workflow
~/.fabro/runs/
├── 20260307-01JQXYZ123ABC456DEF789/ # One directory per run
│ ├── manifest.json
│ ├── graph.dot
│ ├── graph.fabro
│ ├── run.pid
│ ├── run.toml
│ ├── progress.jsonl

View file

@ -11,7 +11,7 @@ This tutorial builds an implement-test-fix loop — the agent writes code, runs
<img src="/images/tutorial-branch-loop.svg" alt="Branch-Loop workflow: Start → Plan → Implement → Validate → Tests passing? → Pass to Exit or Fix back to Implement" />
</Frame>
```dot title="branch-loop.dot"
```dot title="branch-loop.fabro"
digraph BranchLoop {
graph [goal="Create a Python script that passes its test suite"]
rankdir=LR
@ -31,7 +31,7 @@ digraph BranchLoop {
```
```bash
fabro run files-internal/demo/05-branch-loop.dot
fabro run files-internal/demo/05-branch-loop.fabro
```
## Command nodes

View file

@ -11,7 +11,7 @@ This tutorial combines parallel execution with multi-model routing to get indepe
<img src="/images/tutorial-ensemble.svg" alt="Ensemble workflow: Start → Fan Out → Opus, Gemini, Codex, Mercury → Merge → Synthesize → Exit" />
</Frame>
```dot title="ensemble.dot"
```dot title="ensemble.fabro"
digraph Ensemble {
graph [
goal="Get independent opinions from multiple providers, then synthesize",
@ -52,7 +52,7 @@ digraph Ensemble {
```
```bash
fabro run files-internal/demo/11-ensemble.dot
fabro run files-internal/demo/11-ensemble.fabro
```
<Note>

View file

@ -17,7 +17,7 @@ The simplest possible workflow has one node that sends a prompt to an LLM and ex
<img src="/images/tutorial-hello.svg" alt="Hello World workflow: Start → Compose → Exit" />
</Frame>
```dot title="hello.dot"
```dot title="hello.fabro"
digraph Hello {
graph [goal="Write a haiku about software workflows"]
rankdir=LR
@ -34,7 +34,7 @@ digraph Hello {
Run it:
```bash
fabro run files-internal/demo/01-hello.dot
fabro run files-internal/demo/01-hello.fabro
```
### What's happening
@ -53,7 +53,7 @@ An **agent node** (the default `box` shape) runs an LLM in a loop with access to
<img src="/images/tutorial-tool-use.svg" alt="Tool Use workflow: Start → Explore → Exit" />
</Frame>
```dot title="tool-use.dot"
```dot title="tool-use.fabro"
digraph ToolUse {
graph [goal="Explore the current directory using shell tools"]
rankdir=LR
@ -68,7 +68,7 @@ digraph ToolUse {
```
```bash
fabro run files-internal/demo/02-tool-use.dot
fabro run files-internal/demo/02-tool-use.fabro
```
### What's happening
@ -93,7 +93,7 @@ An agent can spawn **sub-agents** to delegate work. Sub-agents run in their own
<img src="/images/tutorial-subagent.svg" alt="Sub-agent workflow: Start → Research → Exit" />
</Frame>
```dot title="sub-agent.dot"
```dot title="sub-agent.fabro"
digraph SubAgent {
graph [goal="Research and summarize using a sub-agent"]
rankdir=LR
@ -108,7 +108,7 @@ digraph SubAgent {
```
```bash
fabro run files-internal/demo/03-subagent.dot
fabro run files-internal/demo/03-subagent.fabro
```
### What's happening

View file

@ -11,7 +11,7 @@ This tutorial assigns different models to different tasks in a single workflow
<img src="/images/tutorial-multi-model.svg" alt="Multi-Model workflow: Start → Write Spec (Haiku) → Implement (Sonnet) → Write Tests (Sonnet) → Code Review (Sonnet) → Exit" />
</Frame>
```dot title="multi-model.dot"
```dot title="multi-model.fabro"
digraph MultiModel {
graph [
goal="Build and review a utility function using multiple models",
@ -36,7 +36,7 @@ digraph MultiModel {
```
```bash
fabro run files-internal/demo/08-multi-model.dot
fabro run files-internal/demo/08-multi-model.fabro
```
## Model stylesheets

View file

@ -11,7 +11,7 @@ This tutorial runs three code review perspectives in parallel — security, arch
<img src="/images/tutorial-parallel-review.svg" alt="Parallel Review workflow: Start → Fan Out → Security Audit, Architecture Review, Code Quality → Merge → Final Report → Exit" />
</Frame>
```dot title="parallel.dot"
```dot title="parallel.fabro"
digraph Parallel {
graph [goal="Perform a multi-perspective code review"]
rankdir=LR
@ -40,7 +40,7 @@ digraph Parallel {
```
```bash
fabro run files-internal/demo/06-parallel.dot
fabro run files-internal/demo/06-parallel.fabro
```
## Fan-out with the fork node

View file

@ -11,7 +11,7 @@ This tutorial builds a plan-approve-implement workflow where a human reviews the
<img src="/images/tutorial-plan-implement.svg" alt="Plan-Implement workflow: Start → Plan → Approve Plan → Implement → Simplify → Exit, with Revise loop back to Plan" />
</Frame>
```dot title="plan-implement.dot"
```dot title="plan-implement.fabro"
digraph PlanImplement {
graph [goal="Plan, approve, implement, and simplify a change"]
rankdir=LR
@ -32,7 +32,7 @@ digraph PlanImplement {
```
```bash
fabro run files-internal/demo/10-plan-implement.dot
fabro run files-internal/demo/10-plan-implement.fabro
```
## Human gates

View file

@ -17,7 +17,7 @@ First, create a standalone implement-and-test workflow. This is a normal workflo
<img src="/images/tutorial-sub-workflow-child.svg" alt="Child workflow: Start → Implement → Validate → Tests passing? → Pass to Exit or Fix back to Implement" />
</Frame>
```dot title="implement-and-test.dot"
```dot title="implement-and-test.fabro"
digraph ImplementAndTest {
graph [goal="Implement the plan and make tests pass"]
rankdir=LR
@ -45,7 +45,7 @@ Now create a parent workflow that delegates to the child:
<img src="/images/tutorial-sub-workflow.svg" alt="Sub-Workflow: Start → Plan (agent) → Implement & Test (house) → Review (agent) → Exit" />
</Frame>
```dot title="sub-workflow.dot"
```dot title="sub-workflow.fabro"
digraph SubWorkflow {
graph [goal="Create a Python module (tempconv.py) that converts between Celsius, Fahrenheit, and Kelvin, with pytest tests"]
rankdir=LR
@ -54,7 +54,7 @@ digraph SubWorkflow {
exit [shape=Msquare, label="Exit"]
plan [label="Plan", prompt="Analyze the goal. List the functions needed, their signatures, and edge cases. Write the plan to plan.md."]
impl [label="Implement & Test", shape=house, stack.child_dotfile="implement-and-test.dot", manager.max_cycles=50]
impl [label="Implement & Test", shape=house, stack.child_workflow="implement-and-test.fabro", manager.max_cycles=50]
review [label="Review", prompt="Read every file the child workflow created. Run the tests yourself with 'python3 -m pytest -v'. Verify the implementation matches plan.md and all tests pass. Write a short verdict to review.md."]
start -> plan -> impl -> review -> exit
@ -62,15 +62,15 @@ digraph SubWorkflow {
```
```bash
fabro run files-internal/demo/12-sub-workflow.dot
fabro run files-internal/demo/12-sub-workflow.fabro
```
## The house node
The `impl` node has `shape=house`, which makes it a **sub-workflow node**. Instead of running an LLM or a script, it launches an entirely separate workflow engine to execute the child DOT file:
The `impl` node has `shape=house`, which makes it a **sub-workflow node**. Instead of running an LLM or a script, it launches an entirely separate workflow engine to execute the child workflow file:
```dot
impl [label="Implement & Test", shape=house, stack.child_dotfile="implement-and-test.dot", manager.max_cycles=50]
impl [label="Implement & Test", shape=house, stack.child_workflow="implement-and-test.fabro", manager.max_cycles=50]
```
The child workflow runs through its own start → implement → validate → gate → exit sequence. When it finishes, execution returns to the parent and continues to the `review` node.
@ -79,13 +79,14 @@ The child workflow runs through its own start → implement → validate → gat
| Attribute | Description |
|---|---|
| `stack.child_dotfile` | Path to the child DOT file (resolved relative to the parent) |
| `stack.child_dot_source` | Inline child DOT source (alternative to `child_dotfile`) |
| `stack.child_workflow` | Path to the child workflow file (resolved relative to the parent). Preferred. |
| `stack.child_dotfile` | Backward-compatible alias for `stack.child_workflow`. |
| `stack.child_dot_source` | Inline child DOT source (alternative to `child_workflow`) |
| `manager.max_cycles` | Safety limit on poll cycles before the child is cancelled (default: 1000) |
| `manager.poll_interval` | How often to check for completion or stop conditions (default: `45s`) |
| `manager.stop_condition` | Condition expression that, when true, cancels the child early |
Use `stack.child_dotfile` when you want to reuse the child workflow across multiple parents. Use `stack.child_dot_source` for one-off child workflows that are specific to the parent.
Use `stack.child_workflow` when you want to reuse the child workflow across multiple parents. Use `stack.child_dot_source` for one-off child workflows that are specific to the parent.
## Context flow
@ -102,7 +103,7 @@ For long-running child workflows, you can set a stop condition that cancels the
```dot
impl [shape=house,
stack.child_dotfile="implement-and-test.dot",
stack.child_workflow="implement-and-test.fabro",
manager.stop_condition="context.deploy_ready=true",
manager.max_cycles=100]
```
@ -122,7 +123,7 @@ For simpler cases, just add more nodes to a single workflow. Sub-workflows add a
## What you've learned
- **Sub-workflow nodes** (`shape=house`) run a child workflow inside a parent
- **`stack.child_dotfile`** references an external DOT file for reuse
- **`stack.child_workflow`** references an external workflow file for reuse (`stack.child_dotfile` is also supported for backward compatibility)
- **Context flows** from parent to child and back via diff merging
- **`manager.max_cycles`** prevents runaway child workflows
- **`manager.stop_condition`** cancels the child when an external signal arrives

View file

@ -60,7 +60,7 @@ If the timeout elapses without a response, the workflow continues to the default
For testing or fully automated runs, pass `--auto-approve` to skip all human gates:
```bash
fabro run workflow.dot --auto-approve
fabro run workflow.fabro --auto-approve
```
Auto-approve selects `Yes` for yes/no gates and the first option for multiple-choice gates.

View file

@ -9,7 +9,7 @@ Model stylesheets let you assign LLM models, providers, and settings to workflow
Stylesheets are set in the `model_stylesheet` graph attribute:
```dot title="example.dot"
```dot title="example.fabro"
digraph Example {
graph [
goal="Build and review a utility function",

View file

@ -12,7 +12,7 @@ Define variables in the `[vars]` section of a run config TOML file:
```toml title="run.toml"
version = 1
goal = "Run tests for $repo_name"
graph = "check.dot"
graph = "check.fabro"
[vars]
repo_name = "fabro"
@ -22,7 +22,7 @@ language = "rust"
These variables are expanded into the DOT source **before** the graph is parsed. You can use `$variable` anywhere in the DOT file — goals, prompts, labels, scripts, or any other attribute:
```dot title="check.dot"
```dot title="check.fabro"
digraph Check {
graph [goal="Run tests for $repo_name"]
@ -56,7 +56,7 @@ This produces `The env var is $HOME` without treating `$HOME` as a variable refe
Inside agent and prompt node prompts, Fabro automatically expands `$goal` to the workflow's `goal` attribute. This happens at runtime, after graph parsing:
```dot title="example.dot"
```dot title="example.fabro"
digraph Example {
graph [goal="Implement the login feature"]

View file

@ -50,7 +50,7 @@ Bootstrap product context from raw inputs. Run once per product.
![Seed pipeline](seed.png)
[seed.dot](seed.dot) | Prompts: [ingest](prompts/seed/ingest.md), [draft](prompts/seed/draft.md)
[seed.fabro](seed.fabro) | Prompts: [ingest](prompts/seed/ingest.md), [draft](prompts/seed/draft.md)
---
@ -63,7 +63,7 @@ Define what a feature does in implementation-agnostic terms. Run once per featur
![Specify pipeline](specify.png)
[specify.dot](specify.dot) | Prompts: [clarify](prompts/specify/clarify.md), [require](prompts/specify/require.md)
[specify.fabro](specify.fabro) | Prompts: [clarify](prompts/specify/clarify.md), [require](prompts/specify/require.md)
---
@ -76,7 +76,7 @@ Translate approved requirements into a technical blueprint. Run once per feature
![Architect pipeline](architect.png)
[architect.dot](architect.dot) | Prompts: [blueprint](prompts/architect/blueprint.md), [diagram](prompts/architect/diagram.md)
[architect.fabro](architect.fabro) | Prompts: [blueprint](prompts/architect/blueprint.md), [diagram](prompts/architect/diagram.md)
---
@ -89,7 +89,7 @@ Generate working code from a feature blueprint. Run once per feature, after Arch
![Implement pipeline](implement.png)
[implement.dot](implement.dot) | Prompts: [plan](prompts/implement/plan.md), [implement](prompts/implement/implement.md), [validate](prompts/implement/validate.md), [fix](prompts/implement/fix.md)
[implement.fabro](implement.fabro) | Prompts: [plan](prompts/implement/plan.md), [implement](prompts/implement/implement.md), [validate](prompts/implement/validate.md), [fix](prompts/implement/fix.md)
All nodes share `fidelity="full"` with `thread_id="impl"` so the agent maintains full context across the loop. `goal_gate=true` on Validate ensures the pipeline cannot exit until all acceptance criteria pass.
@ -104,7 +104,7 @@ Detect and resolve drift between the three layers. Run continuously (after merge
![Sync pipeline](sync.png)
[sync.dot](sync.dot) | Prompts: [detect](prompts/sync/detect.md), [propose](prompts/sync/propose.md), [apply](prompts/sync/apply.md)
[sync.fabro](sync.fabro) | Prompts: [detect](prompts/sync/detect.md), [propose](prompts/sync/propose.md), [apply](prompts/sync/apply.md)
Short-circuits to Exit when no drift is detected, avoiding unnecessary human interaction.
@ -119,7 +119,7 @@ Evolve the product by adding, modifying, or removing features. Run as needed.
![Expand pipeline](expand.png)
[expand.dot](expand.dot) | Prompts: [propose](prompts/expand/propose.md), [execute](prompts/expand/execute.md)
[expand.fabro](expand.fabro) | Prompts: [propose](prompts/expand/propose.md), [execute](prompts/expand/execute.md)
---

View file

@ -90,6 +90,14 @@ pub fn resolve_workflow_arg(arg: &Path) -> anyhow::Result<PathBuf> {
}
fn resolve_workflow_arg_from(arg: &Path, start_dir: &Path) -> anyhow::Result<PathBuf> {
resolve_workflow_arg_impl(arg, start_dir, user_workflows_dir().as_deref())
}
fn resolve_workflow_arg_impl(
arg: &Path,
start_dir: &Path,
user_workflows: Option<&Path>,
) -> anyhow::Result<PathBuf> {
if arg.extension().is_some() {
tracing::debug!(arg = %arg.display(), "Workflow arg has extension, returning as-is");
return Ok(arg.to_path_buf());
@ -99,32 +107,48 @@ fn resolve_workflow_arg_from(arg: &Path, start_dir: &Path) -> anyhow::Result<Pat
match discover_project_config(start_dir) {
Ok(Some((config_path, config))) => {
let fabro_root = resolve_fabro_root(&config_path, &config);
let candidate = fabro_root
let project_candidate = fabro_root
.join("workflows")
.join(&*name)
.join("workflow.toml");
if candidate.is_file() {
tracing::debug!(arg = %arg.display(), resolved = %candidate.display(), "Resolved workflow name via project config");
Ok(candidate)
} else {
let available = list_available_workflows(&fabro_root);
if available.is_empty() {
bail!(
"Unknown workflow '{name}'\n\nNo workflows found in {}",
fabro_root.join("workflows").display()
);
}
let mut msg = format!(
"Unknown workflow '{name}'\n\nAvailable workflows: {}",
available.join(", ")
);
if let Some(suggestion) = find_closest_match(&name, &available) {
msg.push_str(&format!("\n\nDid you mean '{suggestion}'?"));
}
bail!("{msg}");
if project_candidate.is_file() {
tracing::debug!(arg = %arg.display(), resolved = %project_candidate.display(), "Resolved workflow name via project config");
return Ok(project_candidate);
}
if let Some(user_wf) = user_workflows {
let user_candidate = user_wf.join(&*name).join("workflow.toml");
if user_candidate.is_file() {
tracing::debug!(arg = %arg.display(), resolved = %user_candidate.display(), "Resolved workflow name via user workflows");
return Ok(user_candidate);
}
}
let project_wf_dir = fabro_root.join("workflows");
let available = list_available_workflows(Some(&project_wf_dir), user_workflows);
if available.is_empty() {
bail!(
"Unknown workflow '{name}'\n\nNo workflows found in {}",
project_wf_dir.display()
);
}
let mut msg = format!(
"Unknown workflow '{name}'\n\nAvailable workflows: {}",
available.join(", ")
);
if let Some(suggestion) = find_closest_match(&name, &available) {
msg.push_str(&format!("\n\nDid you mean '{suggestion}'?"));
}
bail!("{msg}");
}
Ok(None) => {
if let Some(user_wf) = user_workflows {
let user_candidate = user_wf.join(&*name).join("workflow.toml");
if user_candidate.is_file() {
tracing::debug!(arg = %arg.display(), resolved = %user_candidate.display(), "Resolved workflow name via user workflows (no project config)");
return Ok(user_candidate);
}
}
tracing::debug!(arg = %arg.display(), "No project config found, returning literal");
Ok(arg.to_path_buf())
}
@ -135,13 +159,17 @@ fn resolve_workflow_arg_from(arg: &Path, start_dir: &Path) -> anyhow::Result<Pat
}
}
/// List workflow names by scanning `{fabro_root}/workflows/` for dirs containing `workflow.toml`.
fn list_available_workflows(fabro_root: &Path) -> Vec<String> {
let workflows_dir = fabro_root.join("workflows");
let Ok(entries) = std::fs::read_dir(&workflows_dir) else {
/// Return the user-level workflows directory (`~/.fabro/workflows/`).
fn user_workflows_dir() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".fabro").join("workflows"))
}
/// List workflow names in a single directory by scanning for subdirs containing `workflow.toml`.
fn list_workflows_in(workflows_dir: &Path) -> Vec<String> {
let Ok(entries) = std::fs::read_dir(workflows_dir) else {
return Vec::new();
};
let mut names: Vec<String> = entries
entries
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
@ -151,7 +179,28 @@ fn list_available_workflows(fabro_root: &Path) -> Vec<String> {
None
}
})
.collect();
.collect()
}
/// List workflow names by scanning project and user workflow directories.
/// Project workflows appear first; user workflows are deduplicated.
fn list_available_workflows(
project_workflows_dir: Option<&Path>,
user_workflows_dir: Option<&Path>,
) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
if let Some(dir) = project_workflows_dir {
names.extend(list_workflows_in(dir));
}
if let Some(dir) = user_workflows_dir {
for name in list_workflows_in(dir) {
if !names.contains(&name) {
names.push(name);
}
}
}
names.sort();
names
}
@ -489,4 +538,103 @@ mod tests {
assert_eq!(dot_path, expected_dot);
assert!(cfg.is_none(), "expected None for .fabro path");
}
/// Helper: create a workflow dir with workflow.toml + workflow.fabro inside `base/workflows/{name}/`
fn create_workflow_in(base: &Path, name: &str) {
let wf_dir = base.join("workflows").join(name);
fs::create_dir_all(&wf_dir).unwrap();
fs::write(
wf_dir.join("workflow.toml"),
"version = 1\ngraph = \"workflow.fabro\"\n",
)
.unwrap();
fs::write(
wf_dir.join("workflow.fabro"),
"digraph G { start [shape=Mdiamond]; exit [shape=Msquare]; start -> exit }",
)
.unwrap();
}
#[test]
fn resolve_workflow_arg_user_workflow_found() {
let project_dir = TempDir::new().unwrap();
// No fabro.toml in project_dir
let user_dir = TempDir::new().unwrap();
create_workflow_in(user_dir.path(), "my-wf");
let result = resolve_workflow_arg_impl(
Path::new("my-wf"),
project_dir.path(),
Some(user_dir.path().join("workflows").as_path()),
)
.unwrap();
assert_eq!(
result,
user_dir.path().join("workflows/my-wf/workflow.toml")
);
}
#[test]
fn resolve_workflow_arg_project_takes_precedence() {
let project_dir = TempDir::new().unwrap();
fs::write(project_dir.path().join("fabro.toml"), "version = 1\n").unwrap();
create_workflow_in(project_dir.path(), "shared");
let user_dir = TempDir::new().unwrap();
create_workflow_in(user_dir.path(), "shared");
let result = resolve_workflow_arg_impl(
Path::new("shared"),
project_dir.path(),
Some(user_dir.path().join("workflows").as_path()),
)
.unwrap();
// Should resolve to project, not user
assert_eq!(
result,
project_dir.path().join("workflows/shared/workflow.toml")
);
}
#[test]
fn resolve_workflow_arg_user_fallback_when_project_missing() {
let project_dir = TempDir::new().unwrap();
fs::write(project_dir.path().join("fabro.toml"), "version = 1\n").unwrap();
// Project has a different workflow
create_workflow_in(project_dir.path(), "other");
let user_dir = TempDir::new().unwrap();
create_workflow_in(user_dir.path(), "my-wf");
let result = resolve_workflow_arg_impl(
Path::new("my-wf"),
project_dir.path(),
Some(user_dir.path().join("workflows").as_path()),
)
.unwrap();
assert_eq!(
result,
user_dir.path().join("workflows/my-wf/workflow.toml")
);
}
#[test]
fn resolve_workflow_arg_user_workflow_listed_in_error() {
let project_dir = TempDir::new().unwrap();
fs::write(project_dir.path().join("fabro.toml"), "version = 1\n").unwrap();
create_workflow_in(project_dir.path(), "proj-wf");
let user_dir = TempDir::new().unwrap();
create_workflow_in(user_dir.path(), "user-wf");
let err = resolve_workflow_arg_impl(
Path::new("nonexistent"),
project_dir.path(),
Some(user_dir.path().join("workflows").as_path()),
)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("proj-wf"), "expected proj-wf in: {msg}");
assert!(msg.contains("user-wf"), "expected user-wf in: {msg}");
}
}

View file

@ -1,11 +1,11 @@
---
name: fabro-create-workflow
description: Create Fabro workflow DOT graphs and TOML run configurations from natural language requirements. Use when the user wants to create a new workflow, build a pipeline, design a multi-step agent process, or write a .dot or .toml file for Fabro. Covers topology selection, node types, model assignment, edge routing, and run configuration.
description: Create Fabro workflow DOT graphs and TOML run configurations from natural language requirements. Use when the user wants to create a new workflow, build a pipeline, design a multi-step agent process, or write a .fabro or .toml file for Fabro. Covers topology selection, node types, model assignment, edge routing, and run configuration.
---
# Fabro Create Workflow
Turn requirements into a runnable Fabro workflow: a `.dot` graph file defining the pipeline structure and an optional `.toml` run configuration.
Turn requirements into a runnable Fabro workflow: a `.fabro` graph file defining the pipeline structure and an optional `.toml` run configuration.
## Workflow
@ -103,7 +103,7 @@ graph [model_stylesheet="
See `references/run-configuration.md` for the full reference.
A TOML file is optional for simple workflows (you can run `fabro run workflow.dot` directly). Create one when you need:
A TOML file is optional for simple workflows (you can run `fabro run workflow.fabro` directly). Create one when you need:
- Sandbox configuration (provider, environment variables)
- Setup commands (install dependencies)
- Variable definitions
@ -115,7 +115,7 @@ Minimal TOML:
```toml
version = 1
graph = "workflow.dot"
graph = "workflow.fabro"
```
Common additions:
@ -133,7 +133,7 @@ NODE_ENV = "test"
### Step 7: Validate
Run `fabro run --preflight workflow.toml` (or `fabro run --preflight workflow.dot`) to validate without executing.
Run `fabro run --preflight workflow.toml` (or `fabro run --preflight workflow.fabro`) to validate without executing.
If validation fails, fix the reported errors and re-validate.
@ -156,7 +156,7 @@ Place workflow files together in a directory:
```
my-workflow/
workflow.dot # the graph
workflow.fabro # the graph
workflow.toml # run configuration (optional)
prompts/ # external prompt files (optional)
implement.md
@ -166,14 +166,14 @@ my-workflow/
## Running Workflows
```bash
fabro run workflow.dot # run DOT directly
fabro run workflow.fabro # run graph directly
fabro run workflow.toml # run with TOML config
fabro run workflow.toml --dry-run # simulated LLM backend
fabro run workflow.toml --no-retro # skip retro (faster for testing)
fabro run workflow.toml --auto-approve # auto-approve human gates
fabro run workflow.toml --model claude-opus-4-6 # override model
fabro run workflow.toml --sandbox local # override sandbox
fabro validate workflow.dot # validate only
fabro validate workflow.fabro # validate only
```
## References

View file

@ -255,7 +255,7 @@ Paired TOML:
```toml
version = 1
graph = "workflow.dot"
graph = "workflow.fabro"
[sandbox]
provider = "local"

View file

@ -6,7 +6,7 @@ The TOML file configures how a workflow is executed. It is separate from the DOT
```toml
version = 1
graph = "workflow.dot"
graph = "workflow.fabro"
goal = "Implement the login feature"
```