Commit graph

855 commits

Author SHA1 Message Date
Bryan Helmkamp
a93bf1df9c refactor(settings): resolve cli settings 2026-04-10 07:04:34 -04:00
Bryan Helmkamp
5dd6b88415 refactor(settings): resolve run settings and materialize defaults
Add the resolved run namespace, materialize persisted run defaults at create
time, and migrate the main workflow/server/CLI runtime paths off the old
run bridges.
2026-04-10 06:54:31 -04:00
Bryan Helmkamp
2d8dea3454 Merge origin/main into main
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>
2026-04-09 21:11:55 -04:00
Bryan Helmkamp
872783b4ed chore(simplify): cleanup from review of recent commits
- Use FABRO_LOCAL_NO_AUTH_ENV const in start.rs and tests instead of
  the literal it was hoisted from.
- Preserve error chain in resolve_goal_override via anyhow::Error::from
  rather than stringifying through anyhow!.
- Drop {source} from ResolveGoalError::Io Display to avoid duplicate
  text under anyhow's chain formatter.
- Fail loud in setup_register when ConfigLayer reload or parent dir
  creation errors instead of silently leaving stale state.
- Promote resolve_goal_file_path to pub and call it from fabro-config
  to dedupe the absolute-or-base.join logic.
- Trim narrator-voice paragraphs from tls_config and web_auth comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:03:46 -04:00
Bryan Helmkamp
fcf3152daf fix(config): route project/workflow loaders through ConfigLayer::load
The Stage 6 audit caught that `load_project_config` and `load_run_config`
bypassed `ConfigLayer::load` and called `parse_project_config` /
`ConfigLayer::parse` directly. As a result, `resolve_goal_file_paths` —
which rewrites relative `[run.goal] file = "..."` paths to absolute
against the declaring file's directory — only fired for
`~/.fabro/settings.toml`, never for `fabro.toml` or `workflow.toml`.

That meant a project author writing

    [run.goal]
    file = "prompts/goal.md"

would have the relative path survive all the way to consume time and
get resolved against the run's `working_directory` instead of the
config-file directory, contradicting the agreed "config-file rooted"
rule and breaking the most common case.

Both loaders now delegate to `ConfigLayer::load(path)`, which performs
the load-time rewrite. The user-settings path was already correct.

## Tests

- `load_project_config_rewrites_relative_goal_file_path`
- `load_run_config_rewrites_relative_goal_file_path`
- `load_run_config_leaves_absolute_goal_file_untouched`
- `build_manifest_resolves_relative_goal_file_in_project_config` —
  end-to-end via `build_run_manifest`, asserting the absolute path lands
  in `manifest.goal.path` and the file contents land in
  `manifest.goal.text`.
- `build_manifest_resolves_relative_goal_file_in_workflow_config` — same
  shape but exercising `workflow.toml`-declared goal files, which
  resolve relative to the much deeper workflow directory rather than
  the project root.

3,787 workspace tests pass (was 3,782, +5 new). `cargo fmt --check
--all` and `cargo clippy --workspace -- -D warnings` are clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:41:00 -04:00
Bryan Helmkamp
2b2f158c16 feat(settings): run.goal tagged union (inline | file)
`--goal-file` was broken in the v2 path: `TryFrom<&RunArgs> for ConfigLayer`
did `let _ = &args.goal_file;`, so clap accepted the flag listed in
`--help` and then silently dropped it. Users running
`fabro run demo --goal-file prompts/goal.md` ended up with no goal at
all (or the DOT graph-level fallback), a regression from the legacy
flat `Settings` shape.

This commit adds first-class support for both inline and file-sourced
goals via a tagged union on `run.goal`. Greenfield decisions:

- **Single field, two variants.** `RunGoalLayer` is an untagged enum
  of `Inline(InterpString)` and `File { file: InterpString }`. Makes
  `goal XOR goal_file` un-representable in the type system and lets
  the v2 merge matrix treat `run.goal` as a single scalar
  (last-writer-wins) instead of needing a custom mutual-exclusion
  merge rule. Matches the existing `DaytonaDockerfileLayer` pattern.

- **Relative paths are anchored at the file that declared them.**
  `ConfigLayer::load(path)` walks the just-parsed `SettingsFile` and
  rewrites any literal relative `run.goal.file` path to absolute
  using `path.parent()` as the base, via new
  `fabro_config::config::resolve_goal_file_paths`. CLI-sourced paths
  via `--goal-file` are anchored at CWD in
  `overrides::goal_layer_from_args`. Env-interpolated paths
  (`${env.GOALS_DIR}/goal.md`) are left unresolved until consume time
  and then resolved against the run's working_directory.

- **New accessors, no shims.**
  - `run_goal_layer() -> Option<&RunGoalLayer>` — raw variant access.
  - `run_goal_inline_str() -> Option<String>` — inline-only, returns
    `None` for file-sourced goals.
  - `resolve_run_goal(base_dir) -> Result<Option<ResolvedRunGoal>>` —
    reads the file from disk if needed, returns text + provenance
    (`ResolvedGoalSource::Inline | File { path }`).
  - New `ResolveGoalError` enum covers env-lookup and I/O failures.
  - Old `run_goal() / run_goal_str()` are **deleted** outright; every
    call site has been updated to pick the right variant.

- **CLI wiring (the actual bug fix).** `overrides::goal_layer_from_args`
  replaces the two `let _ = &args.goal_file;` lines with real
  resolution: `(Some(text), None)` → `Inline`, `(None, Some(path))` →
  `File { file: absolute }`. Both-set is rejected by a helper error
  and clap already had `conflicts_with = "goal"` as a belt-and-
  braces check. Applied to both `RunArgs` and `PreflightArgs`.

- **Manifest builder.** `resolve_manifest_goal` now calls
  `args_layer.as_v2().resolve_run_goal()` and
  `settings.resolve_run_goal()` in precedence order, then falls
  through to the graph-level `@file` sugar if both are absent. The
  resolved goal is translated to a `ManifestGoal { text, type_, path }`
  by a new `resolved_goal_to_manifest` helper — inline goals get
  `type = Value`, file-sourced goals get `type = File` with the
  absolute path echoed for provenance.

- **Workflow pipeline.** `fabro-workflow::operations::source::
  resolve_goal_override` is rewritten to use `resolve_run_goal`
  against the working_directory. The orphaned helper `resolve_goal_file`
  (a stub from Stage 4 that was always called with `None`) is
  deleted.

- **Server-side manifest.** `fabro-server::run_manifest::
  prepare_manifest` stores the CLI-resolved goal as
  `RunGoalLayer::Inline`, matching the Stage 4 plan's "CLI owns goal
  file reads; server never touches the filesystem for goals"
  contract.

## Tests

**Schema** (`fabro-types::settings::accessors`):
- `run_goal_inline_str_returns_source_value` — literal inline variant
- `run_goal_inline_str_is_none_for_file_variant` — file variant
  explicitly yields `None` from the inline accessor
- `resolve_run_goal_reads_file_variant_from_disk` — end-to-end file
  read with provenance assertion
- `resolve_run_goal_inline_passes_text_through` — inline passthrough

**Config load** (`fabro-config::config`):
- `parse_accepts_inline_goal` + `parse_accepts_file_variant`
- `parse_rejects_goal_with_unknown_sibling_fields` — untagged enum
  correctly rejects mixed-shape TOML
- `combine_replaces_file_goal_with_inline_from_higher_layer` and the
  reverse — confirms the tagged union merges as a single scalar with
  no custom rule needed
- `load_rewrites_relative_goal_file_to_absolute`
- `load_leaves_absolute_goal_file_untouched`
- `load_leaves_env_interpolated_goal_file_untouched`

**CLI overrides** (`fabro-cli::commands::run::overrides`):
- `goal_and_goal_file_together_is_rejected`
- `goal_file_is_anchored_at_cwd_when_relative`
- `absolute_goal_file_is_preserved`
- `inline_goal_builds_inline_variant`
- `empty_args_produce_no_goal_layer`

**CLI integration** (`fabro-cli::tests:🇮🇹:cmd::run`):
- `dry_run_with_goal_file_reads_contents_into_goal` — end-to-end
  `fabro run --dry-run --auto-approve --goal-file <path>` and asserts
  the file contents appear in the preflight summary. Explicit
  regression test for the silently-ignored flag.
- `dry_run_rejects_goal_and_goal_file_together` — clap conflicts_with

## Callsite churn

Every `run_goal() / run_goal_str()` call site updated:
- `fabro-config/src/effective_settings.rs` — 2 test assertions →
  `run_goal_inline_str()`
- `fabro-cli/tests/it/cmd/{config,create}.rs` — 3 sites → inline
- `fabro-cli/src/manifest_builder.rs` — rewritten to use
  `resolve_run_goal`
- `fabro-workflow/src/operations/create.rs` — 2 sites, test + set
- `fabro-workflow/src/operations/source.rs` — rewritten
- `fabro-server/src/{run_manifest,server}.rs` — set + test assertion

3,782 workspace tests pass (was 3,765, +17 new). `cargo fmt
--check --all` and `cargo clippy --workspace -- -D warnings` are
clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:47:12 -04:00
Bryan Helmkamp
1c0caa2395 feat(server): fail-closed auth posture per R52/R53
`resolve_auth_mode_with_lookup` now returns `anyhow::Result<AuthMode>`
and refuses to return success when `server.auth` resolves to zero
enabled strategies. Startup propagates the error via `?` and aborts
with a descriptive message pointing at the three configuration
escape hatches.

Previously the resolver logged a warning and returned
`AuthMode::Strategies(empty)`, which meant an unconfigured server
would start and then reject every request — accidental
misconfigurations produced a silently-broken process rather than a
clean startup failure. The new behavior matches the implementation
plan's explicit guidance: "if `server.auth` is absent or resolves to
no enabled API or web auth configuration, normal server startup
must refuse to start. Demo and test helpers may continue to inject
explicit insecure settings, but insecure startup must be opt-in
rather than accidental."

The single opt-in path is the `FABRO_LOCAL_NO_AUTH` env var set to
the literal string `"1"`, now hoisted into a module-level
`FABRO_LOCAL_NO_AUTH_ENV` constant. `fabro server start --bind
<unix-socket>` already sets this implicitly in `start.rs:232-234`,
so local daemon usage is unchanged. TCP binds now require either
real auth config or an explicit `FABRO_LOCAL_NO_AUTH=1` — arguably
a security improvement for TCP.

Detailed error message lists the three configuration options:

    Configure at least one of the following in `[server.auth]`:
      - `[server.auth.api.jwt]` (requires `FABRO_JWT_PUBLIC_KEY` env)
      - `[server.auth.api.mtls]` (requires `[server.listen.tls]` ...)
      - `SESSION_SECRET` env (enables cookie-based web auth)

Adds six new unit tests covering the full decision matrix:

- `fail_closed_when_server_auth_absent`
- `fail_closed_when_all_strategies_disabled`
- `opt_in_insecure_startup_via_env`
- `insecure_startup_flag_any_other_value_still_fails_closed`
- `cookie_strategy_alone_unlocks_startup`
- `mtls_strategy_resolves_when_enabled_with_listen_tls`

Also adds `#[derive(Debug)]` to `AuthMode` and `AuthStrategy` so the
tests can `expect_err()` on the resolver result.

Two existing `fabro-cli` integration tests for TCP bind resolution
(`start_with_tcp_host_only_bind_resolves_to_host_and_port` and
`start_with_tcp_host_only_bind_warns_and_falls_back_when_default_port_is_unavailable`)
now set `FABRO_LOCAL_NO_AUTH=1` in the test environment. They were
exercising bind-address resolution, not auth, so opting into
insecure startup explicitly keeps their focus narrow.

3,764 workspace tests pass (was 3,758, +6 new). `cargo fmt
--check --all` and `cargo clippy --workspace -- -D warnings` are
clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:03:51 -04:00
Bryan Helmkamp
4e7839c202 refactor(settings): stage 6.5b sweep ::v2:: prefix out of consumers
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>
2026-04-09 18:42:05 -04:00
Bryan Helmkamp
5905befa8d refactor(settings): stage 6.3b promote mcp runtime types into fabro-mcp
Moves `McpServerEntry`, `McpServerSettings`, `McpTransport`, plus the
`default_startup_timeout_secs` / `default_tool_timeout_secs` helpers
from `fabro-types/src/settings/mcp.rs` into
`fabro-mcp/src/config.rs`. fabro-mcp was already the only crate that
re-exported them, so this deletes the `fabro-types` module entirely
and drops the `pub use mcp::*` re-export from `settings/mod.rs`.

`bridge_mcps` / `bridge_mcp_entry` (v2 `McpEntryLayer` → runtime
`McpServerEntry` converters) also move to `fabro-mcp/src/config.rs`.
`fabro-workflow::operations::start` and `fabro-cli::commands::exec`
now import `bridge_mcp_entry` from `fabro_mcp::config::bridge_mcp_entry`
instead of the v2 `to_runtime` module.

Four of the seven legacy runtime type modules are now gone; three
remain (run, sandbox, server). The `to_runtime.rs` module is down to
just sandbox, pull-request, merge-strategy, artifacts, and
worktree-mode helpers.

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>
2026-04-09 18:12:00 -04:00
Bryan Helmkamp
a3808a98b3 refactor(settings): stage 6.3b promote user runtime types into consumers
First consumer migration pass. Deletes
`lib/crates/fabro-types/src/settings/user.rs` outright:

- `OutputFormat`, `PermissionLevel`: moved into `fabro-agent/src/cli.rs`
  where they are actually consumed as `AgentArgs` fields. They carry
  clap `ValueEnum` derives so `fabro-cli` keeps importing them via the
  `fabro_agent::cli::{OutputFormat, PermissionLevel}` public path.
- `ClientTlsSettings`: moved into `fabro-cli/src/user_config.rs` as a
  crate-private struct. Only `fabro-cli` references it (via
  `cli_target_from_v2` when building the HTTP client).
- `ExecSettings`, legacy `ServerSettings` (from `settings::user`):
  deleted outright — no callers remained.

Also removes the now-dead `From<&GitAuthorSettings> for GitAuthor`
impl in `fabro-checkpoint/src/author.rs`. The v2 `GitAuthorLayer`
conversion is the only path `fabro-workflow::git::git_author_from_settings`
uses. Drops the `fabro_types::settings::server::GitAuthorSettings`
import along with it.

`settings/mod.rs` drops the `pub mod user` declaration and the
`pub use user::*` re-export line. One of the seven legacy runtime
type modules is now gone; six remain.

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>
2026-04-09 17:59:49 -04:00
Bryan Helmkamp
587bd6f5c5 refactor(events): dedupe schema v2 plumbing
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>
2026-04-09 17:23:54 -04:00
Bryan Helmkamp
40c9aae29c feat(settings): stage 6.6 wire server + CLI to v2 SettingsFile DTO
Replaces the Stage 6.2 stopgap `strip_nulls(serde_json::to_value(full
SettingsFile))` path in `get_server_settings` with an explicit
redaction pass in the new `fabro_server::settings_view` module.

The redaction drops the narrow set of fields that leak operational
secrets or host filesystem layout:

- `server.listen.*` (bind + TLS material)
- `server.auth.api.jwt.{issuer, audience}` (auth topology)
- `server.auth.api.mtls.ca` (filesystem path)
- `server.auth.web.providers.github.client_secret`

Every other field is preserved. `InterpString` values that reference
`${env.NAME}` already serialize to their unresolved template form, so
no additional env-provenance walk is needed in this pass.

Implements the real `/api/v1/runs/:id/settings` handler — previously
wired to `not_implemented` — by opening the run reader, reading the
persisted `RunRecord.settings`, running it through the same
redaction, and serializing. The demo route still points at
`demo::get_run_settings`, unchanged.

Updates `fabro-cli` to deserialize the new wire shape as
`SettingsFile` directly:

- `server_client::retrieve_server_settings` now returns
  `SettingsFile` (no longer the legacy flat `Settings`) by decoding
  the progenitor `types::ServerSettings` newtype map into a
  `serde_json::Value` and then into `SettingsFile`.
- `commands/config/mod.rs::legacy_settings_to_v2` shim (TODO-1)
  **deleted**; `merged_config` passes the v2 file straight into
  `effective_settings::resolve_settings`.
- The `fabro-cli` integration tests rewrite their mock `/api/v1/settings`
  payloads as v2 TOML via `ConfigLayer::parse` instead of hand-rolling
  the legacy TOML shape.

All 3,761 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>
2026-04-09 17:16:26 -04:00
Bryan Helmkamp
49767a43fe refactor(events): thread stage scope through emitter
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>
2026-04-09 16:34:59 -04:00
Bryan Helmkamp
eb7f99310c refactor(config): stage 6.4 delete fabro-config re-export shims
fabro-config no longer carries the legacy pass-through shims that
forwarded type re-exports from `fabro_types::settings::{hook,mcp,sandbox,
server,user,run}`. Consumers now import the runtime types directly
from `fabro_types::settings::*`, which is the only definitional
location.

Deleted files:
- `fabro-config/src/hook.rs` (1 LOC glob re-export)
- `fabro-config/src/mcp.rs`   (1 LOC glob re-export)
- `fabro-config/src/sandbox.rs` (~8 LOC re-export list)
- `fabro-config/src/server.rs`  (re-exports + `resolve_storage_dir`;
  the `resolve_storage_dir` helper moved to `fabro_config`'s crate root
  and takes `&SettingsFile` directly)

Shrunk files:
- `fabro-config/src/run.rs` lost the `ArtifactsSettings` /
  `CheckpointSettings` / `GitHubSettings` / `LlmSettings` /
  `MergeStrategy` / `PullRequestSettings` / `SetupSettings` re-export
  block and the unused `resolve_env_refs` helper. What remains is just
  the workflow TOML loader helpers (`parse_run_config`, `load_run_config`,
  `resolve_graph_path`).
- `fabro-config/src/user.rs` lost the `ClientTlsSettings` /
  `ExecSettings` / `OutputFormat` / `PermissionLevel` /
  `ServerSettings` re-export block. The settings-path helpers and
  legacy-config warning logic stay. `fabro-cli/src/user_config.rs`
  now imports `ClientTlsSettings` directly from fabro_types.

Callers updated to use the canonical paths:
- `fabro-agent/src/cli.rs` imports `{OutputFormat, PermissionLevel}`
  from `fabro_types::settings::user`; added `fabro-types` dep.
- `fabro-hooks/src/{config,types}.rs` re-export from
  `fabro_types::settings::hook`.
- `fabro-mcp/src/config.rs` re-exports from `fabro_types::settings::mcp`.
- `fabro-sandbox/src/daytona/mod.rs` re-exports from
  `fabro_types::settings::sandbox`.
- `fabro-server/src/{lib,jwt_auth,tls,serve,demo}.rs` +
  `tests/it/openapi_conformance.rs` import server types from
  `fabro_types::settings::server` and call `fabro_config::resolve_storage_dir`
  from the crate root.
- `fabro-workflow/src/{operations/start,pipeline/types,pipeline/pull_request}.rs`
  import sandbox / pull_request types from `fabro_types::settings::*`.

Build, clippy, fmt, and 3756 / 3756 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:30:38 -04:00
Bryan Helmkamp
2c8b6c95aa refactor(settings): stage 6.3 delete dead Settings helpers + v2 install TOML
Stage 6.3 closes out the dead code that Stage 6.1 left behind:

fabro-types
- Delete the inherent helpers on the legacy flat `Settings` struct
  (`app_id`, `slug`, `client_id`, `git_author`, `sandbox_settings`,
  `setup_settings`, `setup_commands`, `setup_timeout_ms`,
  `preserve_sandbox_enabled`, `github_permissions`, `mcp_server_entries`,
  `verbose_enabled`, `prevent_idle_sleep_enabled`, `upgrade_check_enabled`,
  `dry_run_enabled`, `auto_approve_enabled`, `no_retro_enabled`,
  `storage_dir`, `slack_settings`). Nothing reads them anymore --
  consumers now use `SettingsFile` accessors (`github_app_id_str()`,
  `run_sandbox()`, `dry_run_enabled()`, `storage_dir()`, etc.). The
  `Settings` struct itself stays alive for the remaining legacy
  OpenAPI response path and a handful of demo-route payloads; Stage
  6.6 finishes the deletion alongside the OpenAPI spec rewrite.
- Delete the `#[cfg(test)] mod tests` block that only covered the
  deleted `storage_dir()` helper.

fabro-cli/commands/install.rs
- `merge_server_settings` now writes a v2 TOML file (with
  `[server.{api,listen.tls,web,auth.api.{jwt,mtls},auth.web}]` stanzas)
  instead of the legacy v1 top-level `[web]`/`[api]`/`[git]` shape.
  The generated file previously failed to parse as v2 on next startup;
  now it round-trips through `ConfigLayer::parse`.
- Tests rewritten to parse the generated TOML through
  `fabro_config::ConfigLayer::parse` and assert against the v2 tree
  (`server.auth.web.allowed_usernames`, `server.auth.api.{jwt,mtls}.enabled`,
  `server.listen.tls.{cert,key,ca}`). The `merge_server_settings_preserves_existing_*`
  tests collapsed into a single `preserves_existing_top_level_sections`
  test since the old tests were asserting v1 `[git]` / `[api]` keys
  that no longer make sense.

Build, clippy, fmt, and tests all green: 3756 / 3756 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:20:26 -04:00
Bryan Helmkamp
884fa329ba feat(settings): stage 6.2 delete bridge_to_old seam
bridge.rs (818 LOC) is gone. Production consumers no longer produce a
full legacy `Settings` from v2 state; every read path walks the v2 tree
directly or uses one of the narrow v2->runtime helpers in the new
`settings::v2::to_runtime` module.

Core moves:

fabro-types
- Delete `settings::v2::bridge::bridge_to_old` and the whole bridge.rs
  file.
- Relocate the narrow v2->runtime helpers (`bridge_sandbox`,
  `bridge_mcp_entry`, `bridge_mcps`, `bridge_hook`, `bridge_worktree_mode`,
  `bridge_merge_strategy`, `bridge_pull_request`, `bridge_run_artifacts`)
  into a new `settings::v2::to_runtime` module. Each helper takes a
  single v2 subtree and produces the corresponding runtime shape;
  nothing assembles a full legacy `Settings` anymore.
- `settings/mod.rs` doc comment rewritten to describe `Settings` as a
  runtime shape, not a resolved parse target. Stage 6.3 deletes it.

fabro-config
- `ConfigLayer::resolve` is gone along with the `TryFrom<ConfigLayer>
  for Settings` impls. Consumers call `.into()` for a `SettingsFile`,
  or `.as_v2()` to borrow one.
- `fabro_config::server::resolve_storage_dir` now takes `&SettingsFile`.

fabro-server
- `api_server_settings` emits the v2 `SettingsFile` JSON shape
  directly instead of bridging to the legacy flat DTO. Stage 6.6
  replaces the shape again with an explicit allow-list DTO.
- `serve.rs`: `load_settings` returns `SettingsFile`;
  `apply_serve_overrides` / `apply_runtime_settings` mutate v2
  subtrees directly; `build_artifact_object_store` walks
  `server.artifacts`; `build_legacy_api_settings` projects the v2
  auth/listen/api subtrees down to the legacy `ApiSettings` shape for
  the (still-legacy) auth resolver.
- `diagnostics::check_crypto` walks `server.auth.api.{jwt,mtls}` and
  `server.listen.tls` directly.
- `web_auth.rs` oauth / register / setup-status / auth-me flows all
  read `server.web`, `server.integrations.github`, and
  `server.auth.web` directly via the v2 accessors. `merge_settings_keys`
  now writes v2 TOML (with `[server.web]`, `[server.integrations.github]`,
  etc.) instead of the legacy v1 top-level keys, and the register
  handler re-parses the freshly-written file back into the in-memory
  `SettingsFile` state.

fabro-cli
- `CommandContext::machine_settings` returns `&SettingsFile`.
- `user_config::load_settings` and friends return `SettingsFile`.
- `user_config::resolve_server_target` / `exec_server_target` /
  `configured_server_target` walk `cli.target.{http,unix}` directly.
  Tests rewritten against v2 TOML fixtures.
- `main.rs` logging init reads `cli.logging.level` / `server.logging.level`
  via v2 accessors.
- `commands/exec.rs` reads `cli.exec.{model,agent}` and builds mcps
  from `cli.exec.agent.mcps` (falling back to `run.agent.mcps`) via
  `to_runtime::bridge_mcp_entry`.
- `commands/pr/mod.rs` calls `github_app_id_str()`.
- `commands/run/create.rs` drops the legacy `.resolve()` call and uses
  `Into::<SettingsFile>::into(...)`.
- `commands/config/mod.rs::legacy_settings_to_v2` is now a real
  reverse-mapping helper that covers `storage`, `scheduler`,
  `integrations.{github,slack}`, `run.model`, `run.inputs`, and
  `cli.output.verbosity`. Stage 6.6 deletes it when the API client
  returns v2 natively.
- `tests/it/cmd/config.rs` tests now walk the v2 tree directly (via
  `cfg.run_model_name_str()`, `cfg.run_inputs()`, `cfg.run_sandbox()`,
  `cfg.run_hooks()`, `cfg.run_agent_mcps()`, `cfg.run_prepare_commands()`,
  `cfg.server_storage_root_str()`, etc.). The `bridge_to_old` test
  helper is gone.
- `tests/it/api/settings.rs` asserts against the v2 JSON shape.

Build, test, and quality gates all green:
- `cargo build --workspace --tests`
- `cargo clippy --workspace -- -D warnings`
- `cargo fmt --check --all`
- `cargo nextest run --workspace`: 3758 / 3758 passed, 182 skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:12:59 -04:00
Bryan Helmkamp
449d6abf00 test(settings): update fabro-cli test suite for v2 settings shape
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>
2026-04-09 15:36:09 -04:00
Bryan Helmkamp
41ab919959 feat(settings): stage 6.1 consumer migration builds workspace-wide
Extends the stage 6.1 WIP into a compiling state across the workspace.
Most crates and their unit/integration tests now read run.* / cli.* /
server.* v2 layers directly or through targeted bridge helpers.

Key moves in this commit:

fabro-server
- AppState.settings: Arc<RwLock<SettingsFile>> -- all helpers,
  create_app_state_with_* factories, and tests updated.
- api_server_settings bridges SettingsFile -> legacy Settings via the
  transitional bridge so /api/v1/settings still emits the legacy DTO
  shape until Stage 6.6 replaces it with an allow-list DTO.
- get_system_info, get_system_df, get_github_repo, webhook startup, and
  other read sites use the v2 accessors (github_app_id_str,
  server_web, run_sandbox, run_model_*).
- web_auth.rs wraps each oauth / register / setup-status handler in a
  local `bridged` helper that produces a legacy Settings from the v2
  state, so the complex oauth mutation flow keeps working until its
  Stage 6.6 rewrite.
- diagnostics::check_github_app reads via github_*_str accessors;
  check_crypto bridges to the legacy shape inline.
- serve.rs: load_settings returns SettingsFile; apply_serve_overrides /
  apply_runtime_settings mutate v2 subtrees directly; the config poll
  loop and TLS/webhook startup use bridged() for legacy-shape reads.
- Tests in tests/it/{helpers,api/*,scenario/*} rewritten to construct
  SettingsFile via ConfigLayer::parse or v2 struct literals.

fabro-workflow
- Every test fixture in pipeline/{finalize,initialize,pull_request,retro,
  execute,persist}, operations/{create,rebuild_meta,start}, run_lookup,
  runtime_store, handler/manager_loop, and tests/it/{integration,
  daytona_integration}.rs now uses SettingsFile.
- start.rs hooks into the bridge helpers directly via use-imports.
- run_graph / run_graph_from_checkpoint / initialize / finalize /
  pull_request calls are Box::pin'd to stay under clippy's large-future
  threshold after the v2 tree brought RunOptions size up.
- resolve_run_settings writes resolved model/provider back into
  run.model as InterpStrings; tests assert via run_model_*_str().
- preprocess_and_validate pulls vars from run_inputs_as_strings().

fabro-cli
- manifest_builder uses ConfigLayer.combine(...).into() to get a v2
  SettingsFile for the manifest goal resolution path; file-based
  goal_file handling is deferred to 6.6 when the manifest schema catches
  up.
- runner::maybe_build_github_app_credentials and
  tests/it/cmd/{create,runner}.rs read from v2 accessors.
- commands/config/mod.rs::merged_config returns SettingsFile; the
  server-side retrieve_server_settings is bridged via a stopgap
  legacy_settings_to_v2 shim that Stage 6.6 replaces.
- commands/store/dump.rs sample_run_record constructs SettingsFile.

fabro-store, fabro-checkpoint
- Test fixtures constructing RunRecord values updated to SettingsFile.
- fabro-checkpoint/src/author.rs stays (v2 From impl landed in a
  previous additive commit).

fabro-config
- effective_settings.rs rewrite compiles and passes its unit tests.
- project::resolve_working_directory takes &SettingsFile.

Build status: `cargo build --workspace --tests`, `cargo clippy
--workspace -- -D warnings`, and `cargo fmt --check --all` all pass.
`cargo nextest run --workspace` passes 3,749 of 3,764 tests; the 15
remaining failures are fabro-cli integration tests whose snapshot +
TOML fixture shapes still need manual updates:

- cmd::config::* (seven tests): fixture TOML files still use v1
  top-level keys and the snapshot outputs expect the legacy flat JSON
  shape.
- cmd::inspect::* (four tests): run-record JSON snapshots embed the
  flat Settings shape.
- cmd::run::dry_run_persists_event_history_in_store and
  json_run_implies_auto_approve_for_human_gates: check `settings.dry_run
  == Some(true)` directly on the v2 file; should assert
  dry_run_enabled() instead.
- cmd::attach::attach_json_errors_without_prompting_for_human_input:
  unrelated insta snapshot drift caused by the new SettingsFile JSON
  shape leaking into an events-log snapshot.

Follow-up work for this stage also includes:
- Rewriting web_auth.rs register flow to emit v2 TOML directly and to
  re-parse the written file back into state.settings so in-memory
  state doesn't lag the on-disk file.
- Removing the legacy_settings_to_v2 shim in fabro-cli/config once
  the server-side settings endpoint returns v2 shapes (Stage 6.6).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:25:59 -04:00
Bryan Helmkamp
57140c9361 refactor(events): type stage/parallel ids with newtypes
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>
2026-04-09 12:39:40 -04:00
Bryan Helmkamp
257863d948 refactor(store): flatten EventEnvelope wire shape via serde
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>
2026-04-09 12:09:00 -04:00
Bryan Helmkamp
e76398ef4c refactor(events): tidy schema v2 plumbing
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>
2026-04-09 11:42:02 -04:00
Bryan Helmkamp
44def7866e refactor(events): simplify envelope metadata plumbing
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.
2026-04-09 11:08:38 -04:00
Bryan Helmkamp
3eabc013d1 test(migration): land final Stage 4 fixes — 100% workspace tests green
Close out consumer migration with targeted behavior fixes and the
remaining integration-test fixture rewrites. The full workspace
nextest run now reports 3,760 passed / 0 failed / 182 skipped.

Runtime fixes:
- effective_settings::apply_server_defaults now propagates the full
  server-side Settings shape (llm, sandbox, setup, checkpoint,
  pull_request, artifacts, hooks, mcp_servers, github, slack, fabro)
  into the resolved CLI settings, matching the pre-Stage-3 'merge
  everything server' behavior for RemoteServer/LocalDaemon modes
- fabro-cli commands/run/overrides: route --verbose through
  cli.output.verbosity = verbose instead of a run.metadata stash,
  so it resolves to settings.verbose via the bridge
- fabro-server run_manifest manifest_args_layer: same — emit a
  CliLayer with cli.output.verbosity rather than stuffing the flag
  into run.metadata
- fabro-test settings_storage_dir: detect the managed marker and
  return None instead of parsing the injected server.storage.root,
  so isolated_server correctly spins up a new storage dir
- fabro-server run_manifest_local_daemon test now passes with full
  server-side settings snapshot propagation

Test fixture + assertion updates:
- cmd::config::settings_local_explicit_workflow_path_uses_workflow_project_layers:
  assertion updated for v2 R30 whole-list replacement of
  run.prepare.steps across layers (only workflow-setup survives)
- cmd::config::create_explicit_workflow_path_uses_project_config_relative_to_workflow:
  same correction for the persisted run.settings.setup.commands
- cmd::attach::attach_json_errors_without_prompting_for_human_input
  and cmd::run::json_run_implies_auto_approve_for_human_gates: strip
  the bridge-emitted settings.server and settings.version fields from
  the JSON snapshot so the randomised unix-socket path does not flap
  the insta snapshot
- cmd::server_start::concurrent_autostart_converges_on_one_shared_daemon_and_cleans_up:
  rewrite the injected settings.toml to v2 shape with
  [server.storage] root and [cli.target] type = unix path
- scenario::smoke::attach_smoke_covers_arg_validation_and_remote_server_behaviors:
  two [server] target fixtures rewritten to [cli.target]
  type = http url

Accepted insta snapshots for attach and run JSON outputs. Workspace
build + clippy both clean under -D warnings.
2026-04-09 11:07:18 -04:00
Bryan Helmkamp
58f400c70c feat(api): flatten EventEnvelope wire JSON (schema v2)
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>
2026-04-09 10:40:21 -04:00
Bryan Helmkamp
d951d0bf82 fix(lint): clippy cleanup for Stage 3/4 consumer migration
- effective_settings server_defaults_layer: drop Result wrapper since
  the body never fails after the v2 switch
- merge.rs: allow needless_pass_by_value module-wide since every
  merge helper consumes both sides by design
- fabro-cli overrides.rs: replace &Option<String> sigs with
  Option<&str>, collapse Default-plus-assignment into struct literal
  (avoid clippy::field_reassign_with_default), and box the metadata
  HashMap inline
- fabro-cli manifest_builder.rs: pull DaytonaDockerfileLayer into
  scope so the pattern match stays absolute-path-clean
- fabro-cli main.rs + commands/config/mod.rs: Box::pin the settings
  subcommand future so clippy::large_futures stays happy
2026-04-09 10:16:47 -04:00
Bryan Helmkamp
28d28c593b feat(workflow): carry visit + parallel group/branch ids on stage events
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>
2026-04-09 10:14:45 -04:00
Bryan Helmkamp
783e544308 test(cli): migrate remaining config/exec/create fixtures to v2
Update remaining legacy-shape TOML fixtures in fabro-cli integration
tests to the v2 schema and adjust assertions for v2 merge semantics:

- settings_legacy_cli_config_warns_and_ignores_it: verbose → cli.output.verbosity
- settings_user_config_wins_over_legacy_cli_config: [llm]/[vars] → [run.model]/[run.inputs]
- settings_uses_fabro_home_for_home_config_resolution: same
- settings_fetches_server_settings_and_merges_with_local_config: [server] target → [cli.target]
- settings_cli_server_target_overrides_configured_server_target: same
- exec fixtures across exec.rs: [exec] → [cli.exec.*] + [cli.output], [server] → [cli.target]
- server target fixtures across model/ps/create/rm/run: [server] target → [cli.target]
- settings_local_workflow_name_applies_run_overlay_and_deep_merges:
  assertions updated for v2 R22 (run.inputs replaces), hooks replaced
  by id, checkpoint.exclude_globs replaces, sandbox.env and
  daytona.labels stay sticky merge-by-key per R71
2026-04-09 10:11:28 -04:00
Bryan Helmkamp
5dab0f4c0b fix(bridge): use hook command shorthand to avoid duplicate serde key
The old HookDefinition struct has HookType flattened via
#[serde(flatten)], so emitting hook_type = Some(HookType::Command {...})
produces an inner 'command' key at the same level as the outer
HookDefinition.command shorthand field. Round-tripping through YAML
then fails with 'duplicate field command'.

Bridge script/command hooks via the HookDefinition.command shorthand
instead, leaving hook_type = None. Also: sandbox FABRO_CONFIG in the
manifest_builder unit test so it doesn't pick up the developer's real
~/.fabro/settings.toml, and update settings_local_merges_cli_and_project_defaults
to reflect v2 R22 semantics: run.inputs replaces wholesale across
layers rather than merging by key, while daytona.labels stays a sticky
merge-by-key map per R71.
2026-04-09 10:06:16 -04:00
Bryan Helmkamp
4eec9124fa feat(types): add RunEvent envelope fields + ActorRef (schema v2)
Adds stage_id, parallel_group_id, parallel_branch_id, tool_call_id,
and actor to RunEvent per the v2 concrete-shape proposal. Introduces
ActorRef/ActorKind types. Serialization omits absent fields rather
than writing null. Stubs StoredEventFields with matching defaults;
population in stored_event_fields() follows in a later commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:03:01 -04:00
Bryan Helmkamp
4a48305fd2 feat(tests): migrate fabro-cli fixtures and repo fabro.toml to v2
Stage 4 consumer migration: rewrite test fixtures across fabro-cli
integration tests and the repo's own fabro.toml + workflow.toml files
to use the v2 namespaced schema.

Fixtures migrated:
- repo fabro.toml: [fabro] root → [project] directory, [pull_request]
  → [run.pull_request], [sandbox] → [run.sandbox], daytona labels
  and snapshot moved under [run.sandbox.daytona], [[hooks]] →
  [[run.hooks]] with id, integer memory/disk → '8GB'/'20GB' Size
  values
- fabro/workflows/{implement-issue,implement-plan,gh-triage,smoke}/
  workflow.toml: version → _version, [github] →
  [server.integrations.github]
- fabro-cli integration tests: config.rs (settings/external fixtures),
  repo.rs, repo_init.rs, runner.rs, run.rs, store_dump.rs,
  workflow.rs, workflow_create.rs, support.rs
- fabro-server run_manifest.rs: prepare_manifest test constructs v2
  manifest configs (run.prepare.steps + server.integrations.github)
  and updated assertion to reflect v2 whole-list replacement of
  run.prepare.steps across layers

Validate command tests all green; ~10 tests remain that need targeted
fixes for specific behaviors that shifted between schemas.
2026-04-09 09:57:30 -04:00
Bryan Helmkamp
eb077ed3a2 feat(config): switch parser and layering to v2 schema
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.
2026-04-09 09:49:23 -04:00
Bryan Helmkamp
fa3507d8c7 feat(artifacts): remove scratch artifact cache staging
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.
2026-04-08 17:42:55 -04:00
Bryan Helmkamp
56228ea07e test(cli): add integration tests for fabro uninstall
7 IT tests in cmd/uninstall.rs covering:
- help snapshot
- not-installed detection (plain + JSON)
- dry-run preview without deleting
- --yes removes ~/.fabro/
- --json inventory output (dry-run + execute)

Also fixes the "not installed" check to use marker files
(settings.toml, certs/, storage/) instead of directory existence,
since the CLI's logging startup may auto-create the directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:47:48 -04:00
Bryan Helmkamp
ad4c5f0c98 fix(cli): restore fabro-cli verification after merge
Resolve new clippy failures introduced by the merge and update the root
help snapshot to include the uninstall command so fabro-cli lint and
test verification return to green.
2026-04-08 16:41:13 -04:00
Bryan Helmkamp
f0d57dacc9 Merge remote-tracking branch 'origin/main' 2026-04-08 16:32:44 -04:00
Bryan Helmkamp
166b60e3b1 feat(cli): add fabro uninstall command
Adds a top-level `fabro uninstall` command that reverses `fabro install`
and `install.sh`. Defaults to dry-run (preview) mode, requiring `--yes`
to execute.

Features:
- Inventory and dry-run preview with sizes and `--json` support
- Server shutdown (guarded — only when server is running)
- Safety guardrails (refuses to delete /, $HOME, or dirs without markers)
- Shell config cleanup (exact `# fabro` sentinel match, PATH validation,
  atomic write via temp+rename)
- Binary status reporting with tailored brew/cargo/manual hints
- Exit code: 0 on success, 1 on critical failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:31:32 -04:00
Bryan Helmkamp
367fd9302b refactor(cli): centralize command settings and server access
Add CommandContext to load machine settings once per invocation, cache
server access, and route migrated commands through the shared
ServerStoreClient path instead of reloading settings and reconnecting ad
hoc.
2026-04-08 16:27:25 -04:00
Bryan Helmkamp
45675fc42d feat(server): make web ui optional
Add a server-side web.enabled toggle and CLI overrides so Fabro can run
with API and health only while disabling the embedded SPA, browser auth
routes, and web-only helper endpoints.
2026-04-08 16:22:41 -04:00
Bryan Helmkamp
150443123c Merge remote-tracking branch 'origin/main' into production-web-ui 2026-04-08 16:07:38 -04:00
Bryan Helmkamp
15d1c67be9 fix: resolve clippy warnings across workspace
- fabro-types: remove redundant "freeform" match arm (match_same_arms)
- fabro-server: use let...else and remove needless return
- fabro-cli/runner: use while-let instead of match loop, unwrap Option
  from build_artifact_uploader return type
- fabro-cli/attach: introduce AttachOptions struct to reduce bool
  parameter count (fn_params_excessive_bools)
- fabro-test: fix unused variable and needless continue in session lock

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:56:51 -04:00
Bryan Helmkamp
6b0419bc10 fix(test): eliminate session lock race and openssl /dev/stdin flake
Two flake sources identified across 100+ full-suite runs:

1. Session lock EINVAL race: cleanup_session_root's remove_dir_all
   could delete the session root between with_session_lock's
   create_dir_all and File::create, causing EINVAL. Fix: retry the
   create-dir + create-file sequence as a unit.

2. mTLS cert generation: openssl req -key /dev/stdin failed under fd
   pressure with "Bad file descriptor". Fix: read from the already-
   written server.key file path instead of piping through /dev/stdin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:48:51 -04:00
Bryan Helmkamp
abd87909ae feat(server): support host-only tcp binds
Accept `--bind <ip>` as a TCP bind request while keeping the default
Unix socket behavior unchanged. Resolve host-only TCP binds inside the
serving process so startup output, server metadata, and status always
reflect the concrete host:port, preferring 32276 and falling back to a
random port with a warning when needed.
2026-04-08 15:43:09 -04:00
Bryan Helmkamp
f7d86d72e2 Merge remote-tracking branch 'origin/main' into production-web-ui 2026-04-08 14:44:35 -04:00
Bryan Helmkamp
8726065fd1 feat: embed fabro spa and align interruption semantics
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.
2026-04-08 14:43:44 -04:00
Bryan Helmkamp
2138e0cade fix(test): use no_proxy client in detach signal test to prevent macOS timeout flakes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:51:34 -04:00
Bryan Helmkamp
c33deedb34 fix(test): eliminate recovery, cancel, and label flakes
Stabilize the recovery scenario around rebuilt metadata timing and node
ordinals, make in-process run cancellation converge on a cancelled
reason, and keep the label assertion unit test out of the shared
TestContext session lifecycle.
2026-04-08 10:13:17 -04:00
Bryan Helmkamp
18cb38dac7 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/crates/fabro-cli/src/server_client.rs
2026-04-08 03:53:54 -04:00
Bryan Helmkamp
33a5f2adb4 test(cli): replace slow active-run coverage
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.
2026-04-08 03:47:35 -04:00
Bryan Helmkamp
f0738627c6 test(cli): move timeout-prone run coverage into ITs
Replace bin-scoped localhost HTTP tests with command-facing integration
coverage so they run under the intended IT timeout budget without
changing nextest overrides.
2026-04-08 03:40:48 -04:00
Bryan Helmkamp
bcc697c235 fix(cli): follow paginated run events 2026-04-07 23:43:25 -04:00