Extract set_hook_node() helper to deduplicate the 5 call sites that
populate node fields on HookContext. The helper lives in fabro-workflows
(which has the fabro-graphviz dependency) rather than fabro-hooks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the self-contained hooks module (~2900 LOC) into its own crate to
clarify the dependency graph and make the hook system independently
reusable. The set_node convenience method is inlined at its two call
sites in parallel.rs since it depends on fabro-graphviz types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The interviewer module (trait + 7 implementations for human-in-the-loop
interactions) had zero dependencies on fabro-workflows internals, making
it a clean extraction. Consumers (fabro-api, fabro-slack) now depend on
fabro-interview directly instead of reaching through fabro-workflows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add From<ValidationError> for FabroError to eliminate duplicated
.map_err(|e| FabroError::Validation(e.0)) at call sites
- Use top-level `use` imports for stylesheet types in rules.rs
instead of verbose fully-qualified paths
- Remove duplicate parse_condition tests from fabro-workflows
(already covered by fabro-graphviz)
- Use //! inner doc comments in context/keys.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move validation/lint framework and all 24 rules into a dedicated
fabro-validate crate. As prerequisites, move Fidelity, stylesheet
parser/types, and condition parser into fabro-graphviz (where the
Graph types they operate on already live) so fabro-validate can
depend on fabro-graphviz directly without a circular dependency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace fabro_graphviz::graph::types:: with fabro_graphviz::graph::
everywhere, since graph/mod.rs re-exports types::*. Also simplify
the From<GraphvizError> impl.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the self-contained graph/ and parser/ modules into a new
fabro-graphviz crate so the Graphviz DOT parser can be used without
pulling in the full workflow engine.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
When a human gate has only a freeform edge and no fixed choices, use
QuestionType::Freeform instead of MultipleChoice. This sends the user
directly to a text input prompt, skipping the unnecessary select menu.
Also removes the Quit choice from the REPL workflow so it uses the new
freeform-only path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thread the previous stage's response through `Question.context_display`
so `ConsoleInterviewer` renders it before the interactive prompt. This
lets users see the agent's response before deciding what to do next in
looping workflows like REPL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename `llm_model` → `model` and `llm_provider` → `provider` in stylesheet
properties, accessor methods, and all DOT/doc references. Add
ProviderInferenceTransform that automatically infers provider from the model
catalog, eliminating redundant provider declarations in stylesheets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>