- rust.yml: move clippy to nightly-2026-04-14 (was stable); also pin
fmt to the same nightly date for consistency. Both jobs now use the
dated nightly and the run-step uses `cargo +nightly-2026-04-14 ...`.
- AGENTS.md: update developer commands to match CI.
- Duration constructors: replace `Duration::from_secs(N * 60)` /
`Duration::from_millis(N * 1000)` with `from_mins` / `from_secs` /
`from_hours` across the workspace to satisfy clippy's new
`duration_suboptimal_units` lint. std::time::Duration only — custom
`settings::duration::Duration` sites kept on `from_secs`.
- map/unwrap_or cleanup: `.map(f).unwrap_or(v)` → `.map_or(v, f)`,
`.map(f).unwrap_or(false)` on Result → `.is_ok_and(f)`, per
`clippy::map_unwrap_or`.
- Misc lints: collapse nested `if` into match guard in
handler/llm/api.rs and run_state.rs; replace `columns.len() > 0`
with `!columns.is_empty()`; switch a pair of `sort_by` calls to
`sort_by_key`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Derive configured providers from env and vault when choosing default
models during run creation and materialization, and thread the resolved
run provider through execution handlers instead of recomputing it.
Also return a user-facing error when fabro-agent cannot infer a default
model for the selected provider.
When `disk_cache = true` in `[server.slatedb]`, Fabro enables SlateDB's
object-store cache at `<storage_root>/cache/slatedb`, caching raw S3
bytes on local disk to reduce read latency. All cache parameters use
SlateDB defaults (16 GB max, 4 MB parts). A warning is emitted if
enabled with `provider = "local"` since the cache adds overhead when
the object store is already on the local filesystem.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a Clippy disallowed-methods guardrail for std::thread sleep/spawn
and convert the CLI polling paths to tokio::time::sleep so they no
longer block Tokio workers. Keep the intentional OS-thread sites with
narrow #[expect(...)] annotations that explain why std::thread is
required there.
No production deployments exist, so there's no need for migration shims.
Remove all six backwards-compat type aliases (AgentError, SdkError,
CoreError, GraphvizError, StoreError, FabroError) and migrate ~880
callsites to use the canonical Error name directly within each crate,
or qualified imports (e.g., `use fabro_llm::Error as LlmError`) for
cross-crate references. Also fix a pre-existing absolute-path clippy
lint in fabro-server error.rs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Skip MiniJinja parse+render for plain-text strings (no {{ / {% / {#)
- Remove dead VariableExpansionTransform type alias
- Add From<TemplateError> for FabroError, replace manual map_err with ?
- Extract resolve_prompt_and_model helper in hooks executor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a shared MiniJinja-based template crate and migrate workflow prompts,
imports, hooks, and InterpString env references to the new {{ ... }}
syntax. This also threads typed run inputs through workflow rendering and
updates docs and tests to match the new templating model.
Cleanup pass on the events schema v2 work merged from origin/main.
Quality fixes:
- prompt.rs: drop dead `_visit` local; use stage_scope.visit at the
emit site (the value was being recomputed inline next to a scope
that already had it).
- llm/cli.rs: rename `_context` to `context` in CodergenBackend::run
(it's actually used now); delete the lingering `current_visit`
helper that was deleted from llm/api.rs in 49767a43f but missed
here; use stage_scope.visit at the emit site.
- llm/api.rs: rename `event_scope` to `stage_scope` for consistency
with every other handler.
- agent.rs, fan_in.rs, parallel.rs: same `visit_from_context` →
`stage_scope.visit` substitution at every event-emit site.
- parallel.rs: switch ParallelStarted/ParallelCompleted from `emit`
to `emit_scoped` so they carry stage_id in the envelope.
- event.rs: fix the StageScope::for_handler docstring — the lifecycle
hook is `before_node`, not `before_attempt`.
Reuse fixes:
- run_event/mod.rs: add `ActorRef::agent(session_id, display)` symmetric
with the existing `ActorRef::user`; use it from agent_actor_for_event
in workflow event.rs.
Correctness fixes:
- event.rs: introduce `StageScope::for_parallel_branch` to name the
"branch starts at visit 1" invariant the parallel handler was
hardcoding via a struct literal at parallel.rs:307. This makes
the assumption auditable and gives a single place to fix when
parallel nodes ever loop.
Efficiency fixes:
- stage_id.rs: switch StageId/ParallelBranchId Serialize impls from
`serializer.serialize_str(&self.to_string())` to `collect_str(self)`,
removing one transient String allocation per ID per emitted event.
Hardening:
- event.rs: add `#[must_use]` on `to_run_event`, `to_run_event_at`,
and `event_name`.
- store/types.rs: add a second wire-envelope round-trip test that
populates stage_id, parallel_group_id, parallel_branch_id,
session_id, parent_session_id, tool_call_id, and actor — the
existing test only exercised stage_id, so a regression in any of
the other envelope fields' #[serde(flatten)] interaction would
have been silent.
All 3810 workspace tests pass; clippy and fmt clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brings in the events schema v2 work (RunEvent envelope fields, ActorRef,
parallel branch ids, flattened EventEnvelope wire JSON) on top of the
local Stage 6 settings TOML redesign.
Conflict resolutions:
- fabro-types/src/lib.rs: keep new ParallelBranchId re-export from
origin; drop the legacy Settings/ArtifactStorage* re-exports (the
flat Settings struct was deleted in Stage 6.3b).
- fabro-server/src/server.rs: keep new ActorRef import from origin;
drop the unused legacy Settings import that came along with it.
- fabro-api-client/src/models/web-settings.ts: keep our deletion. The
remote modification was an incidental TS-client regeneration that
Stage 6.6 already invalidated by collapsing settings DTOs to a
freeform v2 shape.
- fabro-workflow/src/event.rs: rewrite the run_created actor test to
use SettingsFile::default() instead of the deleted Settings type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Final mechanical pass: replaces every remaining
`fabro_types::settings::v2::*` import path with
`fabro_types::settings::*` (or the appropriate submodule) across 53
files in 10 crates, then deletes the transitional
`pub mod v2 { pub use super::*; }` alias from
`fabro-types/src/settings/mod.rs`.
No functional changes — all touches are `sed s|settings::v2::|settings::|g`
on import statements and fully-qualified type paths. The v2
namespace is now fully gone; the authoritative module path is
`fabro_types::settings::{accessors, cli, duration, features, interp,
model_ref, project, run, server, size, splice_array, tree, version,
workflow}`.
All 3,758 workspace tests pass. `cargo fmt --check --all` and
`cargo clippy --workspace -- -D warnings` are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mostly consolidation of code added in the recent schema v2 work:
- Share a single ActorRef::user() constructor between server control
actions and workflow provenance conversions.
- Share StageScope::from_context() between current_stage_scope and
StageScope::for_handler so the 4-field construction lives in one place.
- Collapse RunEvent::to_value's if-let chain into an insert_opt helper.
- Use Value::String(id.to_string()) instead of serde_json::to_value for
StageId/ParallelBranchId when seeding the parallel branch context.
- Share parse_event_envelopes via tests/it/support/mod.rs instead of
duplicating the parsing block in two CLI run_events helpers.
Also fix parallel-branch git.commit to emit via emit_scoped with a
branch-specific StageScope so it carries stage_id / parallel_group_id /
parallel_branch_id alongside the other stage-scoped events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Populate stage_id / parallel_group_id / parallel_branch_id on every
event tied to a concrete stage execution, per the spec at
docs-internal/fabro-event-schema-v2-concrete-shape.md:223-279.
Before this commit, stored_event_fields() only set stage_id for the
four Event::Stage* variants and Event::Agent -- the only variants
that carried visit/parallel_group_id/parallel_branch_id in their
payload. Every other stage-scoped event (Checkpoint*, PromptCompleted,
Command*, AgentCli*, Prompt, Interview*, Failover, StallWatchdog,
GitCommit, ArtifactCaptured) fell through to node_stored_fields()
and left stage_id as None.
New approach: scope is carried alongside the event, not on the
variant.
- fabro-workflow/src/event.rs: new StageScope type
{ node_id, visit, parallel_group_id, parallel_branch_id }. New
Emitter::emit_scoped(&event, &scope) for stage-level emission.
to_run_event_at and stored_event_fields take an
Option<&StageScope> that merges into the returned envelope
fields. StageScope::for_handler(context, node_id) is the
canonical handler-side constructor -- prefers
context.current_stage_scope() set by the fidelity lifecycle,
falls back to a scope synthesized from the node_id + context
visit count for tests that don't go through the full lifecycle.
- fabro-workflow/src/context.rs: new
WorkflowContext::current_stage_scope() method reads CURRENT_NODE,
internal.node_visit_count, internal.parallel_group_id,
internal.parallel_branch_id from the context.
- Remove the now-redundant visit/parallel_group_id/parallel_branch_id
fields from Event::Stage{Started,Completed,Failed,Retrying} and
the parallel_* fields from Event::Agent. These existed only to
feed stored_event_fields() and are obsolete once scope is
threaded through the emitter.
Emission site migration (all stage-scoped handlers now use
emit_scoped):
- lifecycle/event.rs: StageStarted, StageCompleted, StageFailed,
StageRetrying, CheckpointCompleted, GitCommit (from on_checkpoint)
- lifecycle/git.rs: CheckpointFailed
- lifecycle/artifact.rs: ArtifactCaptured
- handler/command.rs: CommandStarted, CommandCompleted
- handler/prompt.rs: Prompt, PromptCompleted
- handler/agent.rs: Prompt, PromptCompleted
- handler/fan_in.rs: Prompt, PromptCompleted
- handler/human.rs: InterviewStarted, InterviewTimeout,
InterviewInterrupted, InterviewCompleted
- handler/llm/api.rs: Failover, Agent (via spawn_event_forwarder
which now carries a StageScope across the tokio::spawn boundary)
- handler/llm/cli.rs: AgentCliStarted, AgentCliCompleted
- handler/parallel.rs: ParallelBranchStarted, ParallelBranchCompleted
StallWatchdogTimeout stays on plain emit() because the watchdog
fires from an error path without a live stage context.
Deleted the local StageEventScope struct + current_stage_event_scope
helper from handler/llm/api.rs; it's generalized into StageScope.
Tests: two new unit tests in event.rs --
stage_scope_populates_stage_id_on_non_stage_events verifies
CommandStarted / Prompt / GitCommit all pick up stage_id from scope,
run_level_events_without_scope_leave_stage_id_absent confirms
run.* events still get no stage scope. Updated all test fixtures
across fabro-workflow, fabro-cli to drop the removed Event variant
fields. Accepted two insta snapshot updates in
fabro-cli/tests/it/cmd/{attach,run}.rs that now include the
formerly-missing stage_id fields on checkpoint and interview events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Partial Stage 6.1 migration of consumers off the legacy flat Settings
shape to v2 SettingsFile. Commits the in-flight work so subsequent
sessions can resume from here. Workspace currently does NOT build --
fabro-server still has ~60 consumer sites that reference state.settings
as legacy Settings, and fabro-cli is entirely untouched.
Landed in this commit:
fabro-types
- RunRecord.settings: Settings -> SettingsFile
- RunCreatedProps.settings: Settings -> SettingsFile
fabro-config
- effective_settings: full rewrite. resolve_settings now returns
SettingsFile; apply_server_defaults / apply_local_daemon_overrides
are v2-native and use the v2 merge matrix for server-owned domains.
- project::resolve_working_directory takes &SettingsFile and reads
run.working_dir as an InterpString.
fabro-workflow
- start.rs, create.rs, source.rs, validate.rs, run_options.rs, git.rs,
initialize.rs, manager_loop.rs all migrated to &SettingsFile reads.
- resolve_sandbox_provider / resolve_worktree_mode / resolve_daytona_config
/ resolve_fallback_chain walk v2 trees using the bridge helper fns.
- LifecycleOptions built from run_prepare_commands() / run_prepare_timeout_ms().
- Hooks built via bridge_hook on v2 HookEntry.
- MCPs built via bridge_mcp_entry on v2 McpEntryLayer.
- resolve_run_settings writes resolved model/provider back into
run.model (InterpString), not the flat llm struct.
- preprocess_and_validate pulls var expansion from run_inputs_as_strings.
fabro-server/run_manifest.rs
- PreparedManifest.settings -> SettingsFile.
- prepare_manifest_with_mode takes &SettingsFile.
- build_preflight_report / run_llm_check / resolve_model_provider /
run_github_token_check / resolve_sandbox_provider / resolve_daytona_config
all migrated.
- Tests rewritten to use v2 fixtures via ConfigLayer::parse.
fabro-server/server.rs
- AppState.settings type changed to Arc<RwLock<SettingsFile>>.
- github_app_credentials call site uses settings.github_app_id_str()
accessor instead of the flat app_id().
Known remaining errors:
- fabro-server/server.rs: ~60 state.settings.read() sites still
reference legacy Settings fields (llm, sandbox, setup, git, etc.).
- fabro-server/web_auth.rs: heavy git settings usage, tests.
- fabro-server/serve.rs: state mutation of flat llm/sandbox fields.
- fabro-server/diagnostics.rs: app_id / api auth strategies.
- fabro-cli: manifest_builder, commands, tests all untouched.
- Test fixtures across the workspace still construct Settings literals.
- insta snapshots will need bulk-accept after the runtime shape stabilizes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Promote RunEvent.stage_id / parallel_group_id / parallel_branch_id
and the internal Event enum's matching fields from stringly-typed
Option<String> to Option<StageId> / Option<ParallelBranchId>. The
wire contract is now self-enforcing: malformed strings are rejected
at the serde seam, not quietly round-tripped, and the three
StageId::new(...).to_string() calls in stored_event_fields() just
drop the .to_string() since the newtypes flow straight through.
- fabro-types/src/stage_id.rs: new ParallelBranchId { group: StageId,
index: u32 } mirroring StageId's Display / FromStr / serde string
form. "{group}:{index}" (e.g. "fanout@2:0"). Tests for round-trip
and parse rejections.
- fabro-types/src/lib.rs: re-export ParallelBranchId.
- fabro-types/src/run_event/mod.rs: RunEvent, RunEventRaw, and
RunEventParts take Option<StageId> / Option<ParallelBranchId>.
from_ref gains a small generic opt_field<T: Deserialize> helper
that also replaces the bespoke actor null-handling branch. to_value
uses serde_json::to_value(value) for the three typed fields.
- fabro-workflow/src/event.rs: Event::Stage{Started,Completed,
Failed,Retrying} and Event::Agent take Option<StageId> /
Option<ParallelBranchId>. Event::ParallelBranch{Started,Completed}
take the required (non-Option) typed forms. StoredEventFields
and stored_event_fields() plumb the newtypes end-to-end.
- fabro-workflow/src/context.rs: WorkflowContext::parallel_group_id()
returns Option<StageId>, parallel_branch_id() returns
Option<ParallelBranchId>. Read via serde_json::from_value which
validates the shape on the way out.
- fabro-workflow/src/handler/parallel.rs: builds typed values
directly, stores in context via serde_json::to_value (still
produces a JSON string through the custom Serialize). BranchSetup
holds a ParallelBranchId.
- fabro-workflow/src/handler/llm/api.rs: StageEventScope holds
typed ids.
- fabro-workflow/src/lifecycle/event.rs: stage_parallel_ids returns
typed tuple.
Wire JSON is byte-identical before and after (StageId serializes as
"{node_id}@{visit}", ParallelBranchId as "{node_id}@{visit}:{index}",
matching the existing spec). Progenitor-generated types and OpenAPI
schema untouched. Existing None-only fixtures in runtime_store,
git, pipeline, error, run_state, rewind, pr_view, and store/dump
didn't need any edit because None fits any Option<T>.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quality cleanup on top of the v2 envelope commits:
- fabro-workflow/src/event.rs: add ActorKind/ActorRef/RunProvenance
to the existing ::fabro_types import block so call sites can use
unqualified names (restores CLAUDE.md import style). Extract a
node_stored_fields helper to collapse 4 near-identical match arms
in stored_event_fields. Drop the no-op ..default() from the Agent
arm where all 9 fields are set explicitly.
- fabro-types/src/run_event/mod.rs: collapse 9 copies of the
obj.get/as_str/to_string chain in from_ref behind an opt_str
closure.
- fabro-server/src/server.rs: dedupe the two identical error
closures in api_event_envelope_from_store. Skip the typed
ApiEventEnvelope roundtrip in sse_event_from_store so streamed
events go straight from the wire Value to a JSON string.
- fabro-workflow/src/handler/llm/api.rs: inline current_visit into
its sole caller current_stage_event_scope.
Also fixes pre-existing test compile breakage carried in by the
v2 commits: restore the fabro_types::RunId import in support.rs
(removed by 44def786 but still referenced by find_run_dir), and
thread parallel_group_id/parallel_branch_id: None through 9
Event::Stage*/Event::Agent constructors in run_progress and
store/dump tests that 28d28c59 missed.
No behavior change aside from the SSE hot path avoiding one full
strong-type deserialize + reserialize per event.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Centralize flattened EventEnvelope conversion in fabro-store so the CLI,
server, and test helpers reuse one wire-shape path. Also thread parallel
group and branch ids through nested stage and agent events so the new
envelope fields stay populated inside parallel branches.
Adds visit: u32 to Event::StageStarted/Completed/Failed/Retrying so
stored_event_fields() can derive stage_id = "{node_id}@{visit}".
Adds parallel_group_id/parallel_branch_id to ParallelBranchStarted/
Completed Events, computed once in handler/parallel.rs from the
parent parallel node id + visit_from_context + branch index.
Emission sites in lifecycle/event.rs populate visit from
state.node_visits via a new stage_visit helper.
Stored_event_fields() still leaves stage_id and parallel ids None
pending the extraction pass in the next commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stage captured artifacts in per-attempt tempdirs and persist them through an
explicit artifact sink instead of writing into run scratch cache.
Server-managed and test-owned runs now write directly to ArtifactStore, while
CLI worker runs keep the staged upload path. The local run summary now prints
durable artifact identifiers and copy hints rather than scratch-cache paths,
and the run-directory docs and integration coverage were updated to match.
Remove test assertions that verified legacy files (final.patch,
workflow_bundle.json, manifest.json, cache/artifacts/values/) do not
exist in scratch directories — these are a test smell since the code
that wrote them is long gone.
Also rename child workflow scratch path from nodes/{id}_{visit}/child
to stages/{id}@{visit}/child to align with stage_id convention.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reuse the existing sandbox cancellation bridge for workflow setup
commands so server-side startup cancellation interrupts setup work
promptly and preserves the cancelled terminal state under nextest.
Move the built web bundle into an embedded fabro-spa crate so Cargo and
release builds no longer depend on Bun at build time, and preserve the
local dev override path for fast UI iteration.
At the same time, rename interview and agent-level aborted flows to
interrupted, keep cancelled for run-level shutdown, and stop reporting
skipped answers as interruptions in the run event stream.
Persist pending interviews in run state, deliver accepted answers to workers
through the server-owned control path, and remove the old scratch-file and
WebInterviewer transports.
This also moves Slack onto the canonical server answer flow, adds richer
question metadata to the API and run events, and covers the subprocess
question lifecycle with end-to-end tests.
Tighten the worker upload path so object-backed runs only fail when an
artifact upload is actually attempted without a token, and update CLI
snapshots for the new artifact storage metadata.
Fold in the workspace test and clippy fixes needed to verify the final
artifact upload implementation cleanly across Rust and web targets.
Add scoped worker upload tokens and HTTP artifact upload clients.
Support manifest-first multipart stage artifact uploads with validation and checksums.
Gate artifact reads by run capability while preserving legacy scratch fallback.
Replace the overlapping usage and cost model with canonical billing
primitives centered on ModelRef, ModelHandle, TokenCounts, and
BilledModelUsage. This also renames the public API and web surface from
usage to billing, removes compatibility aliases, and normalizes provider
usage adapters onto the shared billing vocabulary.
Move detached workers onto an HTTP-backed runtime store so the server
remains the only SlateDB owner. This replaces the worker's seeded local
RunDatabase with a canonical server-backed handle for state, events, and
blobs, and updates workflow runtime plumbing to use that abstraction.
Pass a real cancel signal from __run-worker through the workflow engine
into sandbox command execution so cancelled runs reap gated shell loops
instead of leaking slow.gate waiters.
Move subprocess workers fully behind the server-owned run store by
switching worker/server coordination to HTTP-backed run events and
control state. Reconcile stale in-flight runs on boot, terminate live
workers during shutdown, and update process titles to reflect server and
worker lifecycle phases.
- centralize FABRO_HOME and storage path resolution in fabro-config
- rename store types, extract ArtifactStore, and simplify run key layout
- switch run scratch to scratch/, remove RuntimeState, and refresh docs/clients
Move durable run metadata and path derivation onto RunId, simplify the
Slate catalog/index format, and carry the storage-specific run directory
through workflow creation so detached and lookup flows stay aligned.
Also update affected CLI snapshots and test helpers to match the new
run discovery behavior.
- run_dump: take &Path instead of PathBuf by value in path helpers
- test_support: remove empty no-op persist_run_artifacts_for_tests
- agent.rs: use u32::try_from instead of as u32 cast
- retro.rs: remove unnecessary let binding
- pull_request.rs: use NodeState::default() instead of Default::default()
- execute/tests.rs: replace bool::then in filter_map with filter+map
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove API/CLI request and log artifact writes plus panic.txt now that
run state and events are store-backed. Update the direct tests to assert
returned behavior instead of on-disk debug files.
All data in these files is already stored in SlateDB via events and
projected into RunState. No production code reads them from disk.
Removed writes: prompt.md, response.md, stdout.log, stderr.log,
script_invocation.json, script_timing.json, parallel_results.json,
provider_used.json, retro/{prompt,response,status,session}, live.json,
detached_failure.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>