Commit graph

126 commits

Author SHA1 Message Date
Bryan Helmkamp
64dbdf2500
fix(ci): resolve workspace test and lint regressions 2026-04-23 20:49:30 -04:00
Bryan Helmkamp
b5684ead94
fix stale dense run fixtures in types and store tests 2026-04-23 17:51:20 -04:00
Bryan Helmkamp
b0b37c5f5e
fix(status): align archive outcomes and clean lint drift 2026-04-22 17:55:07 -04:00
Bryan Helmkamp
6c3211e3ce
refactor(run-status): unify tagged lifecycle states 2026-04-22 17:15:13 -04:00
Bryan Helmkamp
4865efa499
refactor(store): simplify Repository and blob wiring
- Use hex crate for [u8; 32] RecordId instead of hand-rolled loops
- Drop dead prefix_segments cache field; key assembly consumes
  R::PREFIX.split('/') directly, removing an intermediate Vec<&str>
- Cache BlobStore on RunDatabaseInner (built once in open_writer/
  open_reader via a new build() helper) instead of per-blob construction
- Trim the replay_revocations doc comment to drop a stale plan reference

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:00:00 -04:00
Bryan Helmkamp
b862762b29
docs(store): document record repository pattern
Add a short record-layer overview plus a concrete example for defining a
new record type and wrapping Repository<R> in a domain store, so the
internal SlateDB abstraction is easier to discover and reuse.
2026-04-21 08:39:44 -04:00
Bryan Helmkamp
0d2be41e57
Merge remote-tracking branch 'origin/main' 2026-04-21 08:38:37 -04:00
Bryan Helmkamp
bf1aa6eed5
Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/crates/fabro-store/src/lib.rs
#	lib/crates/fabro-store/src/slate/mod.rs
#	lib/crates/fabro-store/src/slate/run_store.rs
2026-04-21 08:35:07 -04:00
Bryan Helmkamp
f75e5c2ef6
refactor(store): extract Record/Repository abstractions
Replaces hand-written K/V stores in fabro-store with a shared Record trait
plus Repository<R> typed K/V layer. Adds KeyedMutex for per-key serialization
and transaction() for all-or-nothing WriteBatch commits. Renames
SlateAuthCodeStore/SlateAuthTokenStore to AuthCodeStore/RefreshTokenStore and
adds BlobStore and RunCatalogIndex wrappers on top of Repository. Deletes
catalog.rs in favor of RunCatalogIndex. Database gains blobs() and
catalog_index() accessors; auth_tokens() is renamed refresh_tokens().

Plan: docs/plans/2026-04-20-003-refactor-fabro-store-record-abstractions-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:29:34 -04:00
Bryan Helmkamp
705dccdea7
Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/crates/fabro-store/src/lib.rs
#	lib/crates/fabro-store/src/run_state.rs
2026-04-21 07:53:10 -04:00
Bryan Helmkamp
7dd058cc40
refactor: unify run vocabulary and metadata snapshot layout
Implements the plan at
docs/plans/2026-04-20-003-refactor-unify-run-vocabulary-metadata-plan.md.

- Rename RunRecord to RunSpec and RunProjection.run to .spec everywhere
  in Rust source, tests, helpers, test names, and error messages.
- Introduce SerializableProjection wrapper that trims bulky node text
  fields (prompt, response, diff, stdout, stderr) for run.json snapshots.
- Collapse metadata-branch and CLI export to one RunDump::from_projection
  builder emitting run.json + graph.fabro + stages/{stage_id}/... and
  drop legacy top-level start/status/checkpoint/sandbox/retro/conclusion
  split files.
- Replace MetadataStore::write_checkpoint with write_snapshot returning
  the commit SHA; add read_run_projection/read_run_spec; demote
  read_checkpoint/read_start_record to projection-field extractors.
- Switch fork, rewind, rebuild_meta, CLI rewind recovery, and retro
  upload to read the unified projection layout.
- Add additive query methods on RunSpec and RunProjection.

Serde-level `alias = "spec"` shim dropped; `rename = "run"` retained to
keep the server API wire format stable per the plan's scope boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:17:23 -04:00
Bryan Helmkamp
f0f04abf44
refactor(client): extract fabro-client crate
Lift shared client DTOs into fabro-types, move auth/target/error/session
logic into fabro-client, and reduce fabro-cli to orchestration around the
builder-based client path.

This also lands the remaining plan cleanup for ApiError, ServerTarget
canonicalization, and the RunEventStream rename at the CLI boundary.
2026-04-20 20:42:20 -04:00
Bryan Helmkamp
323c797e0f
refactor(auth): simplify CLI auth plumbing after code review
Consolidate three copies of `normalized_http_base_url` and
`build_public_http_client` into shared helpers in `user_config`,
add `Display for ServerTarget`, drop stale `#[allow(dead_code)]`
markers now that login/logout/JWT are wired, remove dead
`LOGIN_SUCCESSFUL` and `_error_description` field, and gate
test-only helpers behind `#[cfg(test)]`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:13:37 -04:00
Bryan Helmkamp
6ac8bf6d81
fix(auth): harden CLI auth review gaps
Add the missing refresh transport guard, actionable auth-store lock errors
for unsupported filesystems, explicit OAuth state expiry, and the remaining
CLI auth regression coverage around replay revocation, HTML headers, and
secret-safe logging.
2026-04-20 09:03:22 -04:00
Bryan Helmkamp
9c68c57bcc
feat(auth): add CLI GitHub login and logout flow
Add the server-side CLI OAuth endpoints and token persistence needed to
mint JWT access tokens and rotating refresh tokens from the existing
GitHub web auth flow.

Add CLI auth storage plus `fabro auth login`, `logout`, and `status`, and
prefer stored OAuth access tokens when building target clients.
2026-04-20 07:12:52 -04:00
Bryan Helmkamp
1f6eb5b336
Merge origin/main into Run Files Changed feature branch
Integrates 39 commits from origin/main (archive/unarchive feature, UI
unification, theme/light-mode polish, Settings nav promotion, server
and CLI hardening).

Conflict resolutions:

- apps/fabro-web/app/routes/run-detail.tsx: origin removed the
  `broken` field from the tab config; local added the Files Changed
  tab. Kept the Files Changed tab, dropped the broken field per
  origin's shape.
- lib/crates/fabro-store/src/run_state.rs: both sides added tests
  in the same region. Kept local's two final_patch tests and all
  four of origin's archive/unarchive tests.
- lib/crates/fabro-spa/assets/: embedded SPA bundle rebuilt from
  the merged web source.
- lib/crates/fabro-workflow/src/operations/archive.rs: origin's new
  archive tests construct Event::WorkflowRunFailed{..}; added the
  final_patch: None field that local's lifecycle change introduced.

Workspace verification after merge: 4247 Rust tests + 95 web tests
all pass; clippy clean; fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:30:36 -04:00
Bryan Helmkamp
a21ab24521
fix(store): harden archive/unarchive projection apply against corrupt logs
Adversarial review surfaced that both RunArchived and RunUnarchived apply
arms were naive — any out-of-spec event in the log (concurrent double
archive, tampered import, replayed retry) would permanently corrupt the
projection:

- RunArchived unconditionally captured current status into prior_status.
  A second RunArchived would set prior_status=Some(Archived). Unarchive
  would then emit restored_status=Archived, the apply arm would set
  status=Archived and clear prior_status, and the run would be unrecoverable.
- RunUnarchived trusted restored_status unconditionally. An imported
  event with restored_status=Running produced a projection reporting
  status=Running with no RunRunning event in the log — breaking
  is_active/is_terminal invariants.

Both arms now require a sensible pre-state before mutating:
- RunArchived only transitions from Succeeded|Failed|Dead.
- RunUnarchived only runs from Archived with a terminal restored_status.

Adds three regression tests:
- double_archive_preserves_prior_status
- run_unarchived_with_non_terminal_restored_status_is_ignored
- run_archived_on_non_terminal_projection_is_ignored

The operations layer (archive/unarchive in fabro-workflow) still validates
at emit time; the projection guards are a defensive second line for
replay, imports, and any future code path that double-writes.
2026-04-19 18:40:21 -04:00
Bryan Helmkamp
beda6b00d8
feat(events): add run.archived and run.unarchived event variants
Adds `RunArchived` and `RunUnarchived` events end-to-end through the engine.
Internal `Event` carries `actor` (and `restored_status` on unarchive); wire
`EventBody` serializes as `run.archived`/`run.unarchived` with typed props.
Projection gains `prior_status: Option<RunStatus>` — `RunArchived` captures
the current status before switching to Archived; `RunUnarchived` applies the
event's `restored_status` payload (authoritative) and clears `prior_status`.
2026-04-19 15:42:39 -04:00
Bryan Helmkamp
296eca568b
feat(workflow): capture final_patch on RunFailed
Previously only Success/PartialSuccess outcomes captured the final
unified-patch string into the run projection. Failed runs left
RunProjection.final_patch empty, which meant the upcoming Files
Changed tab could not degrade to a patch-only view once the sandbox
was gone.

Extend on_run_end to run git diff on Failed too, with a tighter 10 s
timeout (vs 30 s on success) so a pathological workspace doesn't
stall downstream terminal notifications (Slack, SSE, CI). Plumb the
optional field through Event::WorkflowRunFailed, RunFailedProps, and
the projection.

Back-compat: final_patch is serde default-None, so pre-change events
in SlateDB replay cleanly as None. No backfill required; old Failed
runs show R4(c) empty state on the Files tab.

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:39:17 -04:00
Bryan Helmkamp
b8af65a9c6
refactor(runs): blocked status canonicalization cleanup (#165)
## Summary

Stacked cleanup of the `canonicalize blocked run status` work (local
commit `d13cdf374`) plus reconciliation with origin's `canonicalize
paginated run list responses` (origin commit `8ab689da7`). Both efforts
ran in parallel and diverged on the column name (`blocked` vs `waiting`)
and on how the board response is shaped — this PR converges them,
keeping `blocked` as the canonical column id while adopting origin's
`column` field on `RunListItem` and `StoreRunSummary` shape.

Also fixes a production-worker regression introduced by the
canonicalization: the worker's start-precondition only accepted
`Submitted | Starting`, so once runs started transitioning through
`Queued` on the way to `Starting`, every subprocess-worker run failed
with `Precondition failed: cannot start run: status is Queued`. That
cascaded into ~90 failing CLI/server integration tests locally.

## Commits

1. `f65843168` refactor(runs): simplify blocked status follow-ups
2. `1492d956c` chore: resolve clippy warnings
3. `676fd9f44` first merge of origin/main
4. `23fc92a2f` **fix(runs): allow Queued status in start precondition**
← the cascade-fix
5. `36b507a83` refactor: simplify pause/unpause + dedupe web status
tables
6. `8d8d27748` refactor(workflow): encapsulate BlockedStateTracker
inside HumanHandler
7. `1c17fda35` second merge of origin/main — resolves waiting vs blocked
8. `4cd3ef7b1` refactor(workflow): Mutex<usize> → AtomicUsize
9. `2e5a58e8a` fix(demo): align run-4 lifecycle status with Blocked
board column

## Test plan

- [x] fmt, clippy, build, doctests all clean
- [x] `cargo nextest run --workspace` — **4092/4092 pass**
- [x] `bun test` — **26/26 pass**, typecheck + production build clean
- [x] Manual CLI repro of the Queued-precondition fix
- [x] Browser smoke test: all 5 columns render with correct
labels/colors, demo run-4 appears in Blocked lane with question text
intact

## Known follow-up (not blocking)

A "paused-while-blocked" run (status `Paused` + `blocked_reason: Some`)
lands in the `running` column because the visible status chooses
`Paused` over `Blocked`. The pending question is not prominent on the
board. Addressing it would require `board_column()` to branch on
`(status, blocked_reason)` rather than just `status` — worth a separate
ticket.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:53:46 -04:00
Bryan Helmkamp
1048534e2c
ci: switch clippy to pinned nightly, clean up workspace lints
- 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>
2026-04-16 18:59:11 -04:00
Bryan Helmkamp
a147fecc00
feat(slatedb): add disk_cache setting for S3-backed deployments
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>
2026-04-15 08:02:46 -04:00
Bryan Helmkamp
66f7f8d6b3
refactor(store): introduce SlateKey type with null-byte separator
Replace stringly-typed key construction with a SlateKey builder that
encapsulates the segment separator. Switches from '#' to '\0' so the
separator cannot collide with key segment values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:04:16 -04:00
Bryan Helmkamp
8f26a080cf
feat(store): enable Zstd compression for SlateDB
Reduces S3 storage cost and read latency for run data by compressing
SST blocks with Zstd. Existing uncompressed data remains readable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:09:30 -04:00
Bryan Helmkamp
74dfb9f652 Restructure local object store layout 2026-04-14 13:08:07 -04:00
Bryan Helmkamp
e2897fc910 refactor(store): rename SlateDB prefix from "db" to "slatedb"
Makes the on-disk storage subdirectory self-descriptive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:54:25 -04:00
Bryan Helmkamp
6a87f0a071 fmt: apply nightly rustfmt after merge
Restore a clean nightly rustfmt baseline on the merged main branch so
cargo +nightly fmt --check --all passes again after bringing in
origin/main.
2026-04-11 13:43:30 -04:00
Bryan Helmkamp
007cfed240 refactor: remove backwards-compat error type aliases
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>
2026-04-11 12:51:42 -04:00
Bryan Helmkamp
501f0e76d1 fmt: apply nightly rustfmt and document nightly requirement
The rustfmt.toml uses nightly-only options (struct_field_align_threshold,
imports_granularity, etc.) so stable rustfmt silently skips them,
producing different output. Use cargo +nightly fmt going forward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:22:51 -04:00
Bryan Helmkamp
f76e28b7a7 refactor: standardize crate error types 2026-04-11 11:53:53 -04:00
Bryan Helmkamp
5eeacd7864 fmt 2026-04-11 11:27:46 -04:00
Bryan Helmkamp
8014f61054 chore(lint): fix all clippy warnings including --tests
Resolve every clippy warning across the workspace when running with
--tests enabled. Previously only library code was lint-clean; test
code had accumulated issues that were invisible without --tests.

Fixes:
- redundant_closure_for_method_calls: |s| s.as_source() -> InterpString::as_source
  (effective_settings, resolve_cli/root/server/features, run_event/record_serde,
  materialize_run) — add InterpString imports where needed
- absolute_paths: inline fabro_types::settings::* paths -> use imports;
  add #![allow(clippy::absolute_paths)] to fabro-cli and fabro-server
  IT test harnesses (matching the existing pattern in integration.rs)
- bool_assert_comparison: assert_eq!(x, true) -> assert!(x)
- needless_raw_string_hashes: r#"..."# -> r"..." where no inner quotes
- field_reassign_with_default: mut + field assign -> struct literal with ..Default
- match_same_arms: merge Timeout | Disconnected arms in attach.rs
- needless_pass_by_value: signal_rx by ref in attach.rs
- unreadable_literal: 9999999999 -> 9_999_999_999
- default_trait_access: Default::default() -> BTreeMap::default()
- items_after_statements: move use to function top
- large_futures: allow in integration.rs test module (test-only, not prod)
- filter_map_bool_then: .filter_map(bool::then) -> .filter().map()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:16:24 -04:00
Bryan Helmkamp
c5c81d2985 refactor(settings): rename settings layer and move parsing 2026-04-10 08:10:06 -04:00
Bryan Helmkamp
e6db34fa43 chore(simplify): events schema v2 cleanup from review
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>
2026-04-09 22:11:20 -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
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
fb04e17329 refactor(settings): stage 6.3b delete legacy flat Settings struct
Deletes `fabro_types::Settings` — the ~65-field legacy flat view that
has been read-only since Stage 6.1 migrated all production read sites
to the v2 `SettingsFile`.

The last remaining readers all fall out of this commit:

- `fabro-server/src/demo/mod.rs` — the two demo settings fixtures
  (`runs::settings()` and `settings::server_settings()`) are rewritten
  as `serde_json::json!(...)` literals in the v2 `SettingsFile` shape.
  They produce the same wire bytes as the real handlers now return, so
  the demo page keeps rendering identically.
- `fabro-server/src/lib.rs::server_config` — drops the
  `pub use fabro_types::Settings` re-export. Only the inner
  `fabro_types::settings::server::*` module (still around until the
  full runtime-type cleanup) remains.
- `fabro-server/tests/it/openapi_conformance.rs` — drops the
  `server_settings_keys_match_openapi_spec` schema-drift test and all
  of its legacy type imports. The new freeform-object DTO in the spec
  (`type: object, additionalProperties: true`) has no `properties` to
  diff against, so the test was already a no-op. Leaves
  `all_spec_routes_are_routable` in place.
- `fabro-store/src/run_state.rs` — test fixture was building a
  `Settings::default()` JSON payload; switched to `SettingsFile::default()`.
- `fabro-types/src/run_event/mod.rs` — two `EventBody::RunCreated`
  round-trip tests were constructing `Settings::default()`; switched
  to `SettingsFile::default()`.
- `fabro-workflow/tests/it/integration.rs` — the two
  `hook_toml_*_parsing` tests decoded top-level `[[hooks]]` into a
  legacy `Settings`. That parse path was removed in Stage 6.1; the
  tests are deleted and replaced with a comment pointing at the v2
  `settings::v2::tree::tests` fixtures that cover the same ground.

The legacy flat struct's module-level doc comment in
`settings/mod.rs` is updated to explain the transitional runtime
shapes that still live under `hook`, `mcp`, `project`, `run`,
`sandbox`, `server`, and `user` — a follow-up pass will either
promote them into their consumer crates or inline them at the call
sites so the whole `settings/*.rs` file set can go away and 6.5b
flattening can happen.

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:32:03 -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
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
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
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
4389049ac0 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/crates/fabro-types/src/run_event/run.rs
2026-04-07 23:15:06 -04:00
Bryan Helmkamp
75af94dc4f refactor(run): simplify CAS-backed run definitions
Drop compatibility versioning from run-definition blobs, remove the
read-after-write polling added around CAS access, and tighten tests to
assert workflow_bundle.json is never written.
2026-04-07 23:10:00 -04:00
Bryan Helmkamp
38cb2ec250 refactor(artifacts): remove obsolete artifact storage flag
Drop the dead artifact storage capability split from run records,
run.created events, and workflow/server create paths. Worker artifact
upload is now unconditional, and tests/snapshots no longer encode a
legacy object-backed distinction.
2026-04-07 23:01:33 -04:00
Bryan Helmkamp
86841b7171 refactor(run): store manifests and definitions in global CAS
Persist submitted run manifests and accepted run definitions as SHA256
blob refs on run events, remove workflow_bundle.json from the runtime
path, and stop deleting shared CAS blobs when removing runs.
2026-04-07 22:57:48 -04:00
Bryan Helmkamp
acc92e5358 refactor(interview): simplify pending question handling
Collapse the live answer rendezvous into ControlInterviewer, move pending
question storage onto a shared typed record, and route HTTP and Slack answer
submission through one server-side flow.
2026-04-07 19:44:32 -04:00
Bryan Helmkamp
87bc42be70 refactor(interview): move run answers onto control channels
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.
2026-04-07 19:23:36 -04:00
Bryan Helmkamp
58860e5ded Merge remote-tracking branch 'origin/main'
Resolve the artifact lifecycle merge by keeping object-backed upload and sandbox sync behavior alongside durable blob-ref normalization.
2026-04-07 17:52:07 -04:00