Track input, output, cache, and reasoning tokens in the Conclusion
struct so the run summary can display token usage even when cost
pricing is unavailable. The summary now shows cache read/write stats
and reasoning tokens when present.
Also fix attach_run to kill the engine child process on timeout or
cancellation instead of orphaning it, and return exit code 1 on cancel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tighten default profile to 2s slow / 4s kill for fast feedback on
hanging unit tests, and add an `e2e` profile with 10s slow / 30s kill
for ignored tests that hit real APIs. Switch CLAUDE.md/AGENTS.md and
CI to use `cargo nextest run`. Fix retry_emits_stage_started_per_attempt
test that exceeded the new timeout by using aggressive backoff preset
instead of the 5s default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. _run_engine crashed for .fabro workflows because it hardcoded
run.toml as the workflow path, but create_run only writes run.toml
for .toml configs. Now falls back to graph.fabro when run.toml is
absent.
2. attach_run couldn't detect a crashed engine because start_run
dropped the Child handle, creating a zombie that kill(pid, 0)
reported as alive. Now start_run returns the Child and attach_run
uses try_wait() to safely detect exit.
3. create_run ignored --run-id, always generating a new ULID. Now
uses args.run_id when provided.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foreground mode previously called `run_command()` directly while detach
mode used `create_run() + start_run()` — two entirely different code
paths. Now both modes go through the same create + start pipeline, with
foreground adding an `attach_run()` call. Only `--preflight` remains as
a special case since it validates config without creating a run dir.
Extract `print_run_summary()` from `run_command` to display the run
result block (status, duration, cost, PR link, output, assets) from
`conclusion.json` after attach completes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add conflicts_with/requires to ResumeArgs checkpoint field so invalid
flag combos produce clap usage errors instead of runtime failures. Add
sleep_inhibitor guard to Command::Resume matching Run and Exec. Split
Docker out of Local match arm in prepare_from_checkpoint to create a
proper DockerSandbox instead of silently falling back to LocalSandbox.
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>
The resume command had two gaps: Daytona sandbox selection bailed with
"not yet supported" despite being documented, and run_resumed omitted
the JSONL/live.json event writers that run_command registers, so
`fabro logs`, `fabro attach`, and the web UI saw no events for resumed
runs. Wire up DaytonaSandbox in both prepare paths and mirror the event
writer block from run_command. Widen resolve_daytona_config and
build_event_envelope to pub(crate).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix three bugs from PR review in the new `fabro resume` command:
1. RunArgs.workflow lost its required constraint when --run-branch was
removed — add #[arg(required = true)] so clap enforces it.
2. run_resumed ignored --preserve-sandbox (hardcoded false),
--verbose (no ProgressUI), and --ssh (no listener). Wire
preserve_sandbox through resolve_preserve_sandbox, create a
ProgressUI registered on the emitter, and handle SSH access.
3. prepare_from_checkpoint unconditionally created a LocalSandbox,
ignoring --sandbox. Add the same sandbox resolution logic used by
prepare_from_branch (Local, Docker, Ssh, Exe, Daytona).
Remove --ssh from `fabro run` and `fabro resume` since `fabro ssh`
is the dedicated command now. Remove the flag from RunArgs, ResumeArgs,
RunSpec, and all docs/changelogs.
Simplify EventEmitter: change on_event to take &self (via
Mutex<Vec<Arc<...>>>) instead of &mut self, removing the need for
the late_listeners workaround. emit() snapshots the listener list
before dispatching to prevent deadlocks from reentrant emit calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The OpenAI Responses API requires store: false for non-Azure endpoints.
Reasoning items round-trip correctly by requesting encrypted_content
via the `include` field, which embeds them in the response payload
rather than relying on server-side storage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude-sonnet-4-5 doesn't support output_config.effort — it needs the
older thinking API with budget_tokens. Add an `effort` feature flag to
ModelFeatures and have the Anthropic adapter auto-convert reasoning_effort
to a thinking config for models that lack effort support.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
store: !codex_mode was sending store: false for non-Codex models,
which prevented reasoning items from being persisted. This broke
multi-turn conversations where reasoning items from turn 1 need to
be sent back in turn 2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests 6 models at a time in shuffled order to spread load across
providers. Uses indicatif progress bar instead of per-model eprint
lines. Results table is sorted back to original catalog order.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exercises a 2+ turn tool-call round-trip with reasoning_effort("high")
to catch bugs like store: false that only manifest when reasoning items
from turn 1 are sent back in turn 2.
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>
Document loop_restart_signature_limit (graph), fallback_retry_target
(node), freeform (edge), and the full manager loop node attribute table
that was previously absent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New page (execution/outcomes.mdx) defines the 5 stage statuses, documents
how each handler produces them, and explains allow_partial, auto_status,
the retry loop, goal gate interaction, and outcome in edge conditions.
Existing pages updated: added missing `skipped` status to outcome key
descriptions, improved `goal_gate`/`auto_status` descriptions in the
dot-language reference, added `allow_partial` to the attributes table,
and added cross-links from failures.mdx and transitions.mdx.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Edge thread_id should override node thread_id, consistent with how
resolve_fidelity already works. The previous order was reversed.
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>
Gives both workflows clearer, consistent names. Updates the graph
identifiers and the child_workflow reference accordingly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a two-stage workflow (plan → implement) that fetches a GitHub
issue via `gh issue view`, writes an implementation plan, then delegates
to the existing implement workflow. Also removes the unnecessary
`backend: api` directive from the implement workflow's model stylesheet.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ProviderInferenceTransform only inferred the provider but passed the
raw alias (e.g. "gpt-54") to the LLM API, causing request failures.
Rename to ModelResolutionTransform and resolve aliases via the model
catalog so the canonical ID (e.g. "gpt-5.4") is used in API calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>