Move each transformer into its own file under transforms/, move
stylesheet.rs into the directory, and fold vars.rs into
variable_expansion.rs. Backward-compat re-exports in lib.rs keep all
external paths working.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relocate conclusion.rs to records/conclusion.rs behind a new records
module, and move preamble.rs into handler/llm/preamble.rs where it is
actually used. Update all imports across fabro-cli and fabro-workflows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-locate LLM backend implementations (AgentApiBackend, AgentCliBackend,
BackendRouter) under handler/ since they implement the CodergenBackend
trait defined in handler/agent.rs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Gate pr_config on dry_run_mode to prevent PR creation during dry runs
- Restore em dash (—) separator in retro output
- Print "Retro unavailable" when retro is enabled but returns None
- Fix pre-existing clippy warnings (derivable_impls, needless_borrow)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename `settings: mut config` binding to `mut settings` in resume.rs
and update all 8 downstream references
- Deduplicate normalize_config call in run.rs by reusing the result
computed for RunRecord
- Replace expect() with graceful error handling when loading RunRecord
in the API server's execute_run
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 5: Rename debug artifacts from run.toml/graph.fabro to
workflow.toml/workflow.fabro. Change write_run_config_snapshot to
byte-for-byte copy of the original TOML instead of re-serialization.
Phase 6: Add run_from_record() that builds execution state directly
from a RunRecord, bypassing prepare_workflow(). Refactor run_command
into run_command + run_command_impl to share execution logic. Simplify
run_engine_entrypoint to call run_from_record() instead of
reconstructing RunArgs and re-parsing the workflow.
Step 7k: Write RunRecord in the API server's execute_run() for
observability, enabling fabro ps/inspect for API-initiated runs.
Fix stale manifest.json reference in docs/agents/outputs.mdx.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Collapse init_run/init_run_with_records/init_run_inner into a single
init_run(run_id, files) that takes all files as a flat slice. Resume
from metadata branch now uses RunRecord's embedded graph directly
when available, falling back to graph.fabro DOT parsing for old runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Delete run_spec.rs and manifest.rs modules. Remove write_manifest()
from the engine, update DiskLifecycle and GitLifecycle to only write
StartRecord. Remove read_manifest() from MetadataStore. Update
run_fork to only handle run.json/start.json. Convert resume.rs to
use RunRecord/StartRecord from the metadata branch. Update all tests
and integration tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce two new persistence types aligned to the CREATE/START lifecycle:
- RunRecord (run.json): written at CREATE with merged FabroConfig, fully
transformed Graph, and run metadata
- StartRecord (start.json): written at START with start_time, run_branch,
and base_sha
All readers (run_lookup, inspect, diff, pr, attach, detached_support,
start, run_fork, pull_request, run_rewind, resume) now read from the
new types first. Legacy manifest.json + spec.json are still written
for backward compatibility (removal in follow-up).
Also adds dry_run, auto_approve, no_retro fields to FabroConfig, derives
Default on LlmConfig and Graph, and updates docs + tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dry_run_writes_jsonl_and_live_json test was timing out at 4s because
the arc() helper didn't pass --no-upgrade-check, causing every test run
to await a background GitHub API call before process exit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce `fabro_workflows::pipeline` module defining typed phases:
PARSE → TRANSFORM → VALIDATE → INITIALIZE → EXECUTE → RETRO → FINALIZE.
Each phase is a standalone function with `#[non_exhaustive]` input/output
types so the compiler enforces ordering. `Validated` uses private fields
with read-only accessors to guarantee immutability post-validation.
Split `engine.run_with_lifecycle()` into `prepare_sandbox()` +
`execute_graph()` (backward-compatible wrapper preserved). Rewrite
`WorkflowBuilder::prepare_inner()` and CLI `prepare_workflow()` to use
pipeline functions. `PreparedWorkflow` now carries a `Validated` with
accessor methods instead of raw `graph`/`source` fields.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make fabro-core's Outcome generic over a usage/metadata type parameter
(OutcomeMeta trait), allowing fabro-workflows to use core's types
directly via a type alias instead of maintaining duplicate Outcome,
StageStatus, and FailureDetail types with bidirectional conversions.
Key changes:
- Add FailureCategory enum to fabro-core (moved from fabro-workflows'
FailureClass), with Display/FromStr/is_signature_tracked
- Add OutcomeMeta supertrait + blanket impl for the generic parameter
- Make Outcome<M>, NodeResult<M>, RunState<M>, NodeDecision<M> generic
with default type parameter M=()
- Add Graph::Meta associated type
- Update FailureDetail with serde renames (category→"failure_class",
signature→"failure_signature") for checkpoint backward compat
- Replace fabro-workflows' Outcome with type alias to
fabro_core::Outcome<Option<StageUsage>>
- Add OutcomeExt extension trait for wf-specific factory methods
(fail_classify, fail_deterministic, retry_classify, simulated, etc.)
- Delete core_adapter/outcome.rs (~170 lines of conversion functions)
- Replace FailureClass with FailureCategory throughout fabro-workflows
- Fix timeout handler to use TransientInfra category, panic handler to
use Deterministic category
Net: -144 lines, zero-cost type unification with no runtime conversions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add verbose_enabled(), prevent_idle_sleep_enabled(), and
upgrade_check_enabled() helpers to FabroConfig to encapsulate default
values. Update all call sites in fabro-cli to use the new helpers.
Also eliminate an unnecessary clone in SubAgentManager::run_to_completion
and use extend() instead of append()+clone() in config merging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update references to WorkflowRunConfig, ServerConfig, apply_defaults,
and deny_unknown_fields in comments and docs to reflect the FabroConfig
unification.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace five config types with a single FabroConfig superset type. All
loading functions (load_cli_config, load_server_config, load_run_config,
parse_project_config) now return FabroConfig. This eliminates the
run_defaults indirection, into_run_defaults() conversion, and
apply_defaults() bridging method in favor of a single merge_overlay()
that works across all config layers (CLI → project → workflow).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The field was populated in constructors but never read by any code.
Selected keys are already carried by AnswerValue::MultiSelected(Vec<String>),
making this field redundant. Also removes the unused options parameter from
Answer::multi_selected().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove ErrorPolicy enum (continue/fail_fast/ignore) and the k_of_n/quorum
join policies from the parallel handler, leaving only wait_all and
first_success. This deletes ~180 lines of conditional logic including
FailFast early termination, the ParallelEarlyTermination event, and all
related tests and documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce a reusable Warning { kind, message, details } variant in
AgentEvent so non-fatal warnings (context window usage, deprecation,
etc.) share a single event shape. The context_window warning preserves
all original fields inside the JSON details object.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Delete the single-implementor LanguageModel trait and merge its methods
into inherent impl on a renamed Model struct. Change provider field from
String to Provider enum, eliminating constant string↔enum conversions
across the codebase. Fix Provider serde attributes so OpenAi serializes
as "openai" (not "open_ai") to match catalog.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce OOP API for the model catalog: LanguageModel trait with blanket
impl on ModelInfo, Catalog struct with typed methods (get, list,
default_for_provider, closest, build_fallback_chain, etc.), ModelRef enum
replacing ModelId, and Provider::OpenAiCompatible variant. Migrate all
callers across the workspace to use Catalog::builtin() and remove the old
free-function API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
reqwest::Client::new() queries macOS SCDynamicStore for system proxy
settings, which calls CFBundleGetMainBundle() → readdir() on
target/debug/deps/. With 576K stale build artifacts accumulated in
that directory, each readdir() took 1.3s, causing 15s+ delays when
multiple test processes ran concurrently.
- Disable jsonschema default features to remove unnecessary reqwest@0.13
and rustls-platform-verifier dependencies
- Make reqwest::Client lazy in web_search tool (OnceLock) to avoid
constructing it during profile tests
- Mark validate_api_key_rejects_invalid_key as #[ignore] since it hits
the live Anthropic API (3.2s per invocation)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- **Unify foreground and detach code paths**: Both `fabro run` modes now
go through the same `create_run() + start_run()` pipeline, with
foreground adding `attach_run()`. Only `--preflight` remains as a
special case.
- **Fix three bugs in create→start→attach path**: (1) `_run_engine`
crashed for `.fabro` workflows by hardcoding `run.toml` — now falls back
to `graph.fabro`; (2) `attach_run` couldn't detect crashed engines due
to zombie processes — `start_run` now returns the `Child` handle; (3)
`create_run` ignored `--run-id`.
- **Configure nextest slow-timeout profiles**: Tighten unit test timeout
to 2s slow / 4s kill, add `e2e` profile with 10s/30s. Switch CI and docs
to `cargo nextest run`.
## Test plan
- [ ] `cargo nextest run --workspace` passes with new timeout profiles
- [ ] `fabro run <workflow>` works in foreground mode (create + start +
attach)
- [ ] `fabro run --detach <workflow>` prints run ID and exits
- [ ] `fabro attach <run>` works standalone (without child handle)
- [ ] `fabro resume <run>` works for both `.toml` and `.fabro` workflows
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix false positive in backward-compat fallback: use path.exists() instead
of error chain inspection to distinguish missing run.toml from one with
a broken internal reference (e.g. missing Dockerfile)
- Skip write_run_config_snapshot in _run_engine path to prevent double
apply_defaults corrupting the snapshot on each restart
- Resolve ${env.VARNAME} refs in run_defaults.sandbox.env when falling
back for bare .fabro workflows
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This PR fixes a bug where workflow TOML configuration (including
`[pull_request]` settings) was silently dropped when running in detached
mode (`fabro run -d`). The root cause was a three-part failure:
`create.rs` checked the raw CLI argument string for a `.toml` extension
instead of the resolved path, so `run.toml` was never written to the run
directory; `RunEngine` always fell back to `graph.fabro` (a DOT file),
causing `prepare_workflow` to return `run_cfg = None` and lose all
TOML-level configuration; and `pull_request`/`asset_globs` fields in
`RunConfig` had no fallback to `run_defaults` when `run_cfg` was absent.
The fix replaces the naive file-copy approach with a proper
serialization pipeline. Rather than copying the raw TOML (which would
contain a `graph` field pointing to a nonexistent file in the run
directory), `create.rs` now calls `write_run_config_snapshot`, which
serializes the already-merged `WorkflowRunConfig` and rewrites the
`graph` field to `"graph.fabro"` — the canonical cached name. This makes
the run directory fully self-contained with all defaults merged,
environment variables resolved, and the graph path correct. `RunEngine`
in `main.rs` now unconditionally points at `run.toml`; a new
`resolve_workflow_source` helper handles the `.toml` path by loading the
config and resolving the graph path, with a backward-compatible fallback
to `graph.fabro` for older detached runs created before this change.
As defense-in-depth, fallbacks to `run_defaults` are added throughout
`run.rs` for `pull_request`, `asset_globs`, `devcontainer`, and
`sandbox.env` — ensuring bare `.fabro` files passed directly still pick
up project-level defaults. Two new unit tests verify the serialization
round-trip (confirming `graph` is rewritten and `pull_request` config is
preserved) and the missing-`run.toml` fallback behavior.
### Fabro Details
<details>
<summary>Ran 9 stages in 26m 29s for $9.17</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 14s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 4m 54s | $0.71 | 0 |
| simplify_opus | 8m 41s | $1.77 | 0 |
| simplify_gpt | 10m 41s | $6.69 | 0 |
| verify | 18s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **26m 29s** | **$9.17** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { 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_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", 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_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>
`prepare_workflow` unconditionally printed Workflow/Graph/Goal info to
stderr, which leaked into `--detach` and `create` output that should
only emit the run ID. Add a `quiet` flag to suppress this output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This PR updates subagent ID generation to use short 8-character hex
strings instead of full UUID v4 strings. Previously, subagent IDs were
36-character UUIDs (e.g. `550e8400-e29b-41d4-a716-446655440000`), which
were verbose in CLI output and unwieldy when the LLM needed to reference
them in tools like `send_input`, `wait`, and `close_agent`. The new
format generates IDs like `a3f1b20c` — compact, human-readable, and with
~4 billion possible values, effectively collision-free within a session.
The change is made at the source in `subagent.rs`, where UUID generation
is replaced with `format!("{:08x}",
uuid::Uuid::new_v4().as_fields().0)`. Because IDs are now inherently 8
characters, the display-layer truncations in `cli.rs` (5 occurrences)
and `run_progress.rs` (2 occurrences) are redundant and have been
removed — `agent_id` is used directly in format strings instead of a
`short_id` slice.
### Plan Summary
- **Replace UUID generation** in `subagent.rs`: use the first field of a
UUID v4 formatted as 8-char lowercase hex, yielding IDs like `a3f1b20c`
instead of full 36-char UUIDs
- **Remove `short_id` truncation** in `cli.rs` (5 places) and
`run_progress.rs` (2 places): since IDs are now already 8 chars, the
`let short_id = &agent_id[..8.min(agent_id.len())]` pattern is
eliminated and `{agent_id}` is used directly in all format strings
- No test changes required — existing tests use hardcoded IDs like
`"sa-1"` and don't assert on ID length or format
<details>
<summary>Full plan</summary>
````md
The plan has been written to `/home/daytona/workspace/plan.md`.
It covers:
- **4 files to modify**: `fabro-agent/Cargo.toml` (add `rand` dep), `subagent.rs` (replace UUID with 8-char hex), `cli.rs` (remove 5 `short_id` truncations), `run_progress.rs` (remove 2 `short_id` truncations)
- **Step-by-step implementation** with exact line references and before/after code
- **Verification commands** to confirm correctness
- **Test case analysis** explaining why no test changes are needed
````
</details>
### Fabro Details
<details>
<summary>Ran 3 stages in 18m 46s for $0.57</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| plan | 1m 28s | $0.57 | 0 |
| implement | 17m 6s | – | 0 |
| **Total** | **18m 46s** | **$0.57** | **0** |
</details>
<details>
<summary>Ran <code>GhImplement.fabro</code> (4 nodes and 3
edges)</summary>
```dot
digraph GhImplement {
graph [
goal="Implement a GitHub issue",
model_stylesheet="
* { model: claude-opus-4-6; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
plan [label="Plan", prompt="Fetch the GitHub issue from the goal using: gh issue view $goal --json title,body,labels,comments\n\nRead the issue title, description, and any comments carefully. Analyze what code changes are needed to resolve the issue.\n\nWrite a detailed implementation plan to plan.md that includes:\n- Summary of the issue\n- Files to create or modify\n- Step-by-step implementation approach\n- Test cases to add or update\n\nThe plan should be specific enough for another agent to implement without seeing the original issue.\n\nRespond with the location of the plan file (plan.md)."]
implement [label="Implement", shape=house, stack.child_workflow="fabro/workflows/implement/workflow.fabro", manager.max_cycles=100]
start -> plan
plan -> implement [fidelity="summary:high"]
implement -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Three places stripped markdown headings and `Plan:` prefixes from goals
with slightly different logic. Extract a shared function so all call sites
behave consistently, and fix `fabro run` which wasn't stripping at all.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add detail text for wait, send_input, close_agent (agent_id),
apply_patch (ellipsis), and read_many_files (file count) — these
were falling through to the `_ => None` catch-all in both
`tool_detail()` and `tool_display_name()`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sync `tool_detail()` in logs.rs with `tool_display_name()` in
run_progress.rs — the two had drifted, so `fabro logs -pf` was
missing detail text for these tool types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This PR introduces a unified `WorktreeSandbox` type in `fabro-sandbox`
that consolidates previously duplicated git worktree management logic
spread across `parallel.rs` and `run.rs`. The new type wraps any
`Arc<dyn Sandbox>`, handles the full worktree lifecycle (branch
creation, `worktree add`, and cleanup) in its `initialize()`/`cleanup()`
methods, overrides `working_directory()` and `exec_command()` to default
to the worktree path, and delegates all other `Sandbox` methods to the
inner sandbox. A `WorktreeConfig` struct controls behavior (branch name,
base SHA, worktree path, and a `skip_branch_creation` flag for resume
flows), and a `WorktreeEventCallback` mechanism bridges lifecycle events
to the workflow event system via a new
`EventEmitter::worktree_callback()` helper.
The old private `WorktreeSandbox` struct in `parallel.rs` (which only
redirected `exec_command` working dirs with no lifecycle awareness) is
removed and replaced with the shared implementation. The
`setup_worktree()` function in `run.rs` is also removed; its logic is
absorbed directly into the `SandboxProvider::Local` branch of sandbox
construction, where `WorktreeSandbox::initialize()` is called and
`std::env::set_current_dir()` follows on success. The resume path
(`run_from_branch`) similarly replaces direct `git::replace_worktree`
calls with `WorktreeSandbox` using `skip_branch_creation: true`. The
`MockSandbox` in `test_support.rs` gains `captured_commands` and
`captured_working_dirs` vectors to support sequenced-command assertions
in the new unit tests.
The `MockSandbox` enhancement is a notable improvement for testability
beyond this specific change—having the full ordered sequence of commands
rather than just the last one makes it straightforward to assert on
multi-step git workflows. One subtle behavior worth noting is that in
`parallel.rs` the `git reset --hard` step previously present after
worktree creation is now absent from `WorktreeSandbox::initialize()`;
the plan mentioned it but the implementation deliberately omits it (the
branch is already force-set to the target SHA, so the reset was
redundant for the parallel case). Cleanup for parallel branches
continues to go through `engine::git_remove_worktree` on the parent
sandbox rather than calling `wt_sandbox.cleanup()`, since the sandbox
`Arc` is consumed by the spawned task—this is a reasonable tradeoff
noted in the plan.
### Fabro Details
<details>
<summary>Ran 11 stages in 62m 32s for $3.67</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 11s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 30m 15s | $2.15 | 0 |
| simplify_opus | 15m 5s | $0.71 | 0 |
| simplify_gpt | 11m 8s | $0.54 | 0 |
| verify | 46s | – | 0 |
| fixup | 3m 17s | $0.27 | 0 |
| verify | 46s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **62m 32s** | **$3.67** | **0** |
</details>
<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (12 nodes and 15
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_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", 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_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>
This PR decomposes `fabro run` into three composable primitives —
`create`, `start`, and `attach` — following the Docker-style lifecycle
model. Previously, `fabro run` performed everything in a single
monolithic function, and `--detach` was implemented by reconstructing
CLI argv to spawn a child process, which was brittle and hard to extend.
The new architecture cleanly separates concerns: `fabro create`
allocates the run directory and persists a `RunSpec` struct to
`spec.json`; `fabro start` spawns a detached `_run_engine` process (a
hidden internal command that reads `spec.json`) via `setsid`; and `fabro
attach` tails `progress.jsonl` with live rendering and handles
file-based interview IPC. `fabro run` is now a composition of these
three primitives, and `fabro run --detach` simply skips the attach step.
The main rendering work lives in a new `handle_json_line()` method on
`ProgressUI` that parses JSONL envelopes and dispatches to the same
internal rendering methods already used by the in-process event handler.
This preserves 100% rendering fidelity without duplicating
spinner/stage/tool-call logic — the attach loop just feeds file lines
into the same code paths. File-based interview IPC is handled in the
attach loop itself: it watches for `interview_request.json`, prompts the
user via `ConsoleInterviewer`, and writes `interview_response.json` back
for the engine to consume. The `hide_bars`/`show_bars` methods
previously private to `ProgressAwareInterviewer` are promoted to public
methods on `ProgressUI` and reused in both the attach loop and the
existing in-process interviewer.
The old `detach_run()` function in `main.rs`, which reconstructed argv
by string-scanning `std::env::args()`, is deleted entirely and replaced
by the `create` + `start` composition. New tests cover the
`handle_json_line` dispatch paths (stage started/completed, tool calls,
retro events, invalid input) and the CLI argument parsing for the new
command variants.
### Fabro Details
<details>
<summary>Ran 9 stages in 30m 55s for $8.55</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 11s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 18m 15s | $5.28 | 0 |
| simplify_opus | 10m 31s | $3.27 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 17s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **30m 55s** | **$8.55** | **0** |
</details>
<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (12 nodes and 15
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_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", 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_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>
Add aarch64-unknown-linux-gnu as a third release platform using GitHub's
native ARM64 runner. Updates the release workflow matrix, install script
architecture detection, and CLI upgrade platform detection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This PR adds a `fabro wait` subcommand that blocks until a workflow run
reaches a terminal state and exits with a code reflecting the outcome —
analogous to `docker wait`. The command accepts a run ID prefix or
workflow name, polls `status.json` at a configurable interval
(defaulting to 1 second), and exits 0 on success or 1 on failure/dead.
An optional `--timeout` flag causes the command to bail with an error
message if the deadline is exceeded before the run completes.
The implementation reuses existing infrastructure throughout:
`resolve_run()` for run ID/name resolution, `RunStatusRecord::load()`
and `RunStatus::is_terminal()` for polling, `Conclusion::load()` for
retrieving duration and cost after completion, and `Styles` for colored
terminal output. Human-readable status is written to stderr (preserving
stdout for data), while `--json` mode writes structured conclusion data
to stdout. Missing status files are treated as `Dead` to handle orphaned
runs gracefully. No new dependencies were required.
The change is registered in all three necessary locations:
`commands/mod.rs`, the `Command` enum in `main.rs`, the command name
mapping, and the dispatch match arm. Unit tests cover JSON output across
all terminal states (with and without conclusion data), the
human-readable output path, immediate-terminal poll behavior, and the
missing-file fallback to `Dead`.
### Fabro Details
<details>
<summary>Ran 9 stages in 12m 13s for $2.81</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 12s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 4m 23s | $1.47 | 0 |
| simplify_opus | 4m 22s | $1.34 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 1m 21s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **12m 13s** | **$2.81** | **0** |
</details>
<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (12 nodes and 15
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_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", 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_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>
Extract the Sandbox trait, types, and all sandbox implementations from
fabro-agent and four separate crates (fabro-exe, fabro-ssh, fabro-sprites,
fabro-daytona) into a single fabro-sandbox crate. This cleans up the
dependency graph — implementation crates no longer pull in the full
fabro-agent just for the trait.
The new crate uses feature flags (local, docker, ssh, exe, sprites,
daytona, test-support) to gate each implementation. The shell_quote()
helper is unified into a single shared implementation, eliminating four
duplicate copies.
fabro-agent now re-exports all sandbox types from fabro-sandbox for
backward compatibility. The four absorbed crates are removed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The release tarball nests the binary in a subdirectory
(fabro-{triple}/fabro), but the upgrade code expected it at the
tarball root. Use the correct nested path matching the tarball structure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Compute sanitize_command, repository_identifier, and CI check once
before the if/else branches to avoid duplicate git I/O
- Make should_track_for_level private (only used by _track_inner)
- Check tracks.is_empty() before credentials in upload_blocking for
consistency with emit()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>