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>
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>
Completes the fabro-cli test migration for Stage 6.1. Every test in
`cargo nextest run --workspace` now passes (3,764 passed / 0 failed).
Changes:
- cmd/support.rs: compact_inspect / compact_git_inspect now walk the
v2 tree (/settings/run/goal, /settings/run/sandbox/provider,
/settings/run/model/provider) and derive `dry_run` from the v2
execution mode.
- cmd/attach.rs: the event-log filter strips _version and redacts
settings.cli.target.path to [CLI_SOCKET] so randomized tempdir
sockets don't pollute the snapshot. Insta snapshot accepted.
- cmd/run.rs: same cli.target redaction in the run event filter.
dry_run_persists_event_history_in_store and
json_run_implies_auto_approve_for_human_gates check for
`settings.run.execution.approval == "auto"` instead of
`settings.auto_approve == true`. Insta snapshot accepted.
- cmd/config.rs: parse_settings bridges the v2 YAML output back down
to the legacy flat Settings shape so the existing helper assertions
keep working. settings_fetches_server_settings_and_merges_with_local_config
now asserts the v2 R22 behavior (run.inputs replaces wholesale, so
server-side `server_only` is dropped in favor of project's vars).
settings_uses_fabro_home_for_home_config_resolution walks the v2
JSON paths (cli.output.verbosity, run.model.name).
create_explicit_workflow_path_uses_project_config_relative_to_workflow
asserts against the v2 run-record shape.
- fabro-cli/commands/config/mod.rs: legacy_settings_to_v2 is now a
real (if narrow) reverse bridge covering storage, scheduler, github
integration, slack integration, run.model, run.inputs, and cli
verbosity. Stage 6.6 still replaces this when the API client returns
v2 types natively, but for now the server-side defaults round-trip
through the resolver with enough fidelity to keep the settings
command integration tests honest.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hand-written to_wire_value / from_wire_value helpers
and the wire_event_envelope_from_generated bridge with
#[serde(flatten)] on EventEnvelope.payload. Derived serde now
produces and accepts the wire shape natively:
{ "seq": 42, "id": "...", "event": "...", ... }
instead of the nested { "seq": 42, "payload": { ... } } the
derive would otherwise emit. #[serde(flatten)] composes fine with
the #[serde(transparent)] EventPayload(Value) wrapper, so the
inner payload object is merged into the outer map on both sides.
- fabro-store/src/types.rs: add #[serde(flatten)]; delete the two
wire helpers (33 lines of Value-map poking); update the
round-trip test to assert the shape is actually flat.
- fabro-server/src/server.rs: sse_event_from_store serializes
the envelope directly; api_event_envelope_from_store pipelines
to_value into from_value.
- fabro-cli/src/server_client.rs: buffer_sse_events parses
straight into EventEnvelope via serde_json::from_str;
list_run_events uses the existing convert_type helper in place
of the deleted wire_event_envelope_from_generated bridge.
- fabro-cli tests: helpers that called from_wire_value now call
serde_json::from_value.
Drops the shape check that from_wire_value used to perform on
parse (id/ts/run_id/event must exist as strings): that check
extracted run_id from the payload and then validated it against
itself, so it only guaranteed presence, not correctness.
EventPayload::new(value, expected_run_id) still runs the same
check where a caller has a real external run_id to cross-match.
Generated code and the OpenAPI allOf(seq, RunEvent) schema are
untouched; the wire JSON is byte-identical before and after.
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.
Wire EventEnvelope now inlines the RunEvent payload fields alongside
seq at the top level of the JSON object. The internal Rust
EventEnvelope { seq, payload } stays structurally unchanged; only the
API/SSE serialization layer flattens for clients.
- OpenAPI spec: add stage_id, parallel_group_id, parallel_branch_id,
tool_call_id, actor to RunEvent; model EventEnvelope as allOf(seq,
RunEvent); introduce ActorRef/ActorKind schemas.
- fabro-server: rewrite api_event_envelope_from_store to merge seq
into the payload JSON value before returning the generated flat
type; remove the now-unused nested ApiRunEvent conversion helper.
- fabro-cli server_client: add wire_event_envelope_into_store helper
that turns flat wire JSON back into fabro_store::EventEnvelope
{ seq, payload } for internal consumers.
- Regenerate progenitor Rust types and typescript-axios client.
- Update demo stubs, SSE tests, CLI test helpers, and insta
snapshots to expect the flattened shape and the new stage_id field.
Incidental: the typescript regeneration also picked up prior-merged
spec fields (ApiQuestion stage/timeout/context, upload manifest
batches, web-settings) that were stale in the TS client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stage 3 of the settings TOML redesign. Switches the core parse/merge/
resolve path to the v2 namespaced schema while keeping the legacy flat
Settings shape accessible via the bridge for not-yet-migrated consumers.
Parser and layering:
- ConfigLayer is now a newtype around v2 SettingsFile. Loading via
ConfigLayer::parse/load/settings/for_workflow/project now hard-fails
on legacy top-level keys (version, llm, vars, sandbox, etc.) with
targeted rename hints emitted by fabro_types::settings::v2::tree
- new fabro_config::merge module encodes the merge matrix directly:
replace-by-default maps, sticky merge for run.sandbox.env and
provider-native labels, splice-aware string arrays for
run.model.fallbacks and notification route events, whole-list
replacement for run.prepare.steps, field-merge keyed objects for
notifications/MCPs/web-auth providers, and ordered hook id-aware
replacement
- ConfigLayer::resolve delegates to fabro_types::settings::v2::bridge
so consumers keep reading through the legacy Settings shape until
Stage 4 migrates them off it
- effective_settings::resolve_settings now treats project/workflow/
run/features as shared layered domains and strips cli/server from
non-local layers before merging, fulfilling the owner-first trust
boundary rule
Consumer migration (Stage 4 preview, kept to the files that block
the workspace build):
- fabro-server run_manifest builds v2 RunLayer from ManifestArgs and
resolves manifest dockerfile references through the v2 sandbox
daytona snapshot tree
- fabro-cli manifest_builder consults run.goal via v2; user_config
writes the v2 server.storage.root field under the CLI storage-dir
override; run/overrides constructs a v2 RunLayer from RunArgs
- fabro-cli scaffolds (repo init, workflow create) emit _version = 1
with project.directory/workflow.graph/run.sandbox etc.
fabro-config / fabro-types legacy parse-time types (ProjectConfig,
LlmConfig, SandboxConfig, PullRequestConfig, ExecConfig, SettingsFile
try_into, etc.) are deleted from the parse path; the resolved type
re-exports (LlmSettings, SandboxSettings, etc.) remain as shims so
unmigrated consumers keep compiling.
fabro-test helper: settings.toml fixtures now use _version = 1 plus
[server.storage] root and [cli.target] type = "unix" path. Legacy
flat storage_dir/server.target handling removed from the sync path.
Known Stage 4/5 follow-ups:
- fabro-cli integration test fixtures still use legacy-shape TOML
(version = 1, [llm], [sandbox], [vars], [exec], [fabro], etc.);
tests currently fail to parse against the v2 schema as intended.
Migrating them is the bulk of Stage 4 and lands in subsequent
commits.
- OpenAPI ServerSettings schema, generated clients, apps/fabro-web
workflowData fallback, and docs/reference examples are unchanged
and land in Stage 5.
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.
Replace the slow CLI integration tests that waited on worker shutdown
grace periods with focused coverage that still checks the important
behavior. The attach JSON test now finishes the gated run cleanly,
the rm force test uses a mocked server contract, and the Ctrl-C cancel
path is covered at the attach layer instead of through a full live run.
Add a cooperative subprocess cancel control message so cancel and delete
can abort pending interviews without relying only on the 5 second hard
kill fallback.
Collapse expensive CLI smoke coverage into scenario tests, replace the
slow doctor no-color integration check with a unit-level render test,
and remove duplicate attach coverage. Also fix local Unix-socket
autostart so missing daemons don't spend the full 5s readiness wait
before startup.
The commit includes the measured slow-test report updates for the work
landed here.
Make ArtifactStore the only artifact read path, stop writing
manifest.json into run scratch, and update the CLI summary to
resolve artifact paths from the durable server API.
Drop scratch-only compatibility paths and legacy test scaffolding now that
SlateDB-backed state is authoritative. This removes scratch file fallbacks,
updates docs and UI labels, and moves tests onto durable store-backed helpers.
Replay persisted run events for attach requests, keep the SSE stream live
only while the run is active, and close on terminal run events instead of
returning 410 for completed runs.
The CLI now treats premature attach EOF as an error, and the affected
integration tests were stabilized around store-backed event ordering and
recovered rewind timelines.
Persist server, client, and subject provenance on run creation so
run state and inspect output can show which Fabro version created a
run, which first-party client submitted it, and how the request was
authenticated.
Default test daemons now opt into an in-memory object store and test
helpers carry explicit run ids instead of rediscovering runs from
shared state.
This also disables the disk-backed store dump integration tests until
store dump is routed through the server's live store handles.
Make fabro-store::RunProjection the single projection type used by the
server, CLI, and CLI test helpers. This removes the duplicated CLI-side
mirrors and adds serde coverage for the store-owned projection.
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.
Increase the artifact scenario timeout so the retry fixture still forces one timeout without spuriously creating a third retry under full-workspace nextest load. Also import the generated ServerSettings type directly so workspace clippy stays clean.
Keep local server targeting based on explicit server targets instead of
implicitly deriving a socket from storage_dir. This makes ~/.fabro/fabro.sock
the default local socket again, keeps storage under ~/.fabro/storage, threads
FABRO_CONFIG through server autostart paths, and updates the CLI test harness
for the new split.
- 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 access and execution control onto the server-backed client,
canonicalize run APIs under /api/v1/runs, and switch CLI integration tests
to a shared test daemon/storage model with shared-state-safe assertions.
Clarifies that this type is an event-sourced projection of run history,
distinct from fabro_core::ExecutionState which tracks live execution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
InMemoryStore duplicated SlateStore's interface and was unused in
production. RunSnapshot/NodeSnapshot were intermediate projections that
tests consumed — replaced with RunState to eliminate the indirection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make run lookup fail with RunNotFound instead of returning Option, thread a required RunStore through workflow and retro paths, and update CLI, server, and test callers to match. Also treat null optional event properties as absent during store-backed replay so event-sourced state stays robust.
Tests were flaky because command() inherited the real repo as the
working directory. When the repo was clean, detached runs attempted
git worktree creation against it, sometimes failing and injecting
extra warning lines into snapshots.
Now command() defaults to the non-git temp_dir, eliminating this
class of flakiness. Tests needing a specific directory override with
.current_dir(). Also canonicalizes fixture paths and adds a
[FIXTURES] snapshot filter via test_context!() macro.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>