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>
- Expose apply_bearer_token_auth and ensure_refresh_target_transport
from fabro-client; drop the CLI's duplicate copies.
- Collapse AuthStore's two read paths into one NotFound-tolerant
reader and drop the pre-existence checks in get/remove/list.
- Avoid rewriting auth.json when remove found nothing.
- Inline the one-line user_config::build_public_http_client wrapper.
- Trim unused pub use fabro_api::types re-export and the narrating
doc comment in fabro-client/src/lib.rs.
- Clean up pre-existing unused imports in run/create.rs and
loopback.rs tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
The `foreground_start_writes_tracing_to_storage_server_log` test
consistently took ~10.4 s. 10.3 s of that was spent inside `fabro
server stop`, which polls `process_running(pid)` every 100 ms until
the server exits. The test's server is spawned as a child of the test
process (`child.spawn()`), and the test only reaps it via
`child.wait_with_output()` after `fabro server stop` returns. After
Step A's revert, `process_running` is a plain `kill(pid, 0)`, which
returns true for a zombie — so the poll saw the dead-but-unreaped
server as alive and burned the full 10 s timeout.
Add `fabro_proc::process_running_strict(pid)` — the same
ps-shelling zombie-aware predicate commit 1ed8e6cbd introduced — and
use it only in `fabro-cli`'s server stop poll. The hot paths that
motivated Step A (test-harness marker scans, daemon-liveness probes)
continue to use the cheap `process_running`.
The ps cost (~2 ms per call) is paid at most once per 100 ms poll
interval and only while the server process still exists. In a normal
clean shutdown that's zero calls (process exits before the first
poll). In the zombie scenario the loop exits after ~1 poll instead
of running out the full timeout.
Verified on this branch:
cargo nextest run -p fabro-cli -E 'test(foreground_start_writes_tracing)'
before: 10.48s, 10.45s, 10.42s
after: 0.35s, 0.32s, 0.25s (30x faster)
The zombie regression test removed in commit da87f978c returns as
`process_running_strict_returns_false_for_unreaped_zombie_child`,
and also asserts that the cheap `process_running` keeps its
"zombie == alive" semantics so the harness hot paths stay honest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delete the CliTargetTls settings, ClientTlsSettings struct, rustls-pemfile
dep, and the fabro-http wrapper methods (use_rustls_tls/identity/
add_root_certificate) that only the CLI client-auth path used. The server
no longer terminates TLS in-process and the CliAuthStrategy::Mtls variant
had no construction or match sites.
Also simplify ServerTarget::HttpUrl to a tuple variant (HttpUrl(String))
now that tls is gone, removing the struct-variant ceremony across 22
construction and destructure sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CLI/server tests racing against peer state on the shared fabro server
session, surfaced by running the default nextest profile 20 times.
pr_list_missing_github_credentials_errors depended on an empty shared
store; if pr_view_reads_pull_request_from_store_without_pull_request_json
ran first it left a PR record behind and this test hit the
credentials-required branch instead of "No pull requests found." The
snapshot captured the empty path, but the test name promises the error
path. Seed a PullRequestCreated event against the test's own run so the
store is guaranteed non-empty and the credentials-required error fires
deterministically.
full_http_lifecycle_cancel asserted that the cancel response body's
pending_control == "cancel", but that field is re-read from the store
projection after the worker has been signaled. The worker is sitting at
a human gate; on hot CI it can emit a clearing event before the handler
re-reads the projection, yielding a legitimate null. Relax the
assertion to accept "cancel" or null; durable convergence to
failed/cancelled is still asserted below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Finish the rename started with the Client alias: drop the alias and use
the Client name directly for the server-facing CLI client struct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dedupe bearer-token extraction and the trailing from_bundle construction
in the managed Unix-socket connect path, return Bind from the
ensure_server_running_on_socket helper to match its sibling, and drop
"target" from its overlong name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ensure configured Unix socket targets still autostart on their requested
socket path during concurrent connection races, instead of falling back to
the storage-owned default bind.
Tighten CLI loopback target classification to use literal host checks,
update explicit local TCP auth coverage to match the remote-target
contract, and align server CSP assertions with the current external-script
SPA bundle. Also enable reqwest cookies in fabro-http so package-scoped
server tests compile without relying on workspace feature unification.
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>
- Drop dead http_client() accessor and its allow(dead_code).
- Scope map_api_error to module-private; all call sites are in-file.
- Rename test helper test_api_client to test_client to match what it returns.
- Run system df's two independent server GETs concurrently with try_join!.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Harden the CLI browser auth flow by moving auth-code issuance behind
an explicit same-origin confirmation step, and update the real-browser
test harness to submit the confirmation page.
Wrap the remaining CLI server API calls in server_client::Client,
remove the api/connect_api_client escape hatches, and migrate
model/install/tests to the new facade.
Split raw PID existence from actual process liveness in fabro-proc and
switch the server shutdown paths to the running-process predicate. This
avoids waiting out stop timeouts for unreaped zombie children while
keeping process-group behavior covered by measured regression tests.
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.
The CLI façade is the primary type callers reach for, so it deserves
the bare `Client` name (per `reqwest::Client`, `hyper::Client`
convention). The raw generated HTTP binding is secondary and is more
accurately named `ApiClient`. "Store" in `ServerStoreClient` was
leftover from the SlateDB-ownership refactor and no longer describes
the type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the real CLI auth integration harness into shared test support so
scenario/auth.rs keeps only the scenario cases and mock-browser helpers.
This makes the real-server auth setup reusable for future CLI integration
tests without duplicating the bootstrap code.
Add shared axum/reqwest response assertion helpers in fabro-test,
migrate the Rust HTTP test surface to use them, and document the
new rule in the testing strategy.
Retry CLI API calls once after a 401 access_token_expired response by
refreshing the stored OAuth session and rebuilding the generated API
client. Clear local auth when the refresh chain is expired or revoked,
and add end-to-end CLI scenarios covering login, authenticated use,
refresh, and logout.
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.
Enable clippy::allow_attributes_without_reason at the workspace level.
Add concise, callsite-specific reasons to existing allow attributes, including generated code paths.
Resolve rm/archive/unarchive selectors through the server-owned
runs/resolve endpoint instead of CLI-side summary matching, and move
active-run delete force semantics into DELETE /runs/{id}.
Two follow-ups the workspace lint now catches:
- fabro-server tests/it/api/install.rs: a newer install-router integration
test was missing the `.await` after `build_install_router(...)` -- the
fn became async when the devcontainer/install-mode resolver was
converted to tokio::fs in commit 19939c5f0.
- fabro-cli main.rs: add #[expect(clippy::disallowed_methods)] to the
#[cfg(test)] module whose write_test_settings helper uses sync
std::fs::write to stage CLI settings fixtures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a server-native run selector endpoint and migrate CLI single-run flows to
use it instead of local workflow-store heuristics. This also moves store dump
export assembly into the CLI, removes the production CLI dependency on
fabro_workflow run lookup and dump helpers, and records the remaining
cli-to-workflow coupling in an audit document.
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>
Origin brought 21 commits of UI/install/test-helpers work that lived in
parallel with the archive feature. Only the SPA build outputs conflicted
(old bundle hashes on both sides). Resolution: accept origin's
resolution on the deleted files, then re-run scripts/refresh-fabro-spa.sh
from the merged source so the embedded bundle reflects both sides —
origin's Settings-nav/theme/stage-sidebar work plus this branch's
archived-status TypeScript changes in apps/fabro-web/app/data/runs.ts.
Verification:
- cargo build --workspace: clean
- cargo nextest run --workspace: 4198 passed, 182 skipped
- cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings: clean
- cargo +nightly-2026-04-14 fmt --check --all: clean
- apps/fabro-web bun run typecheck: clean
- apps/fabro-web bun test app/data/runs.test.ts: 8 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three cleanups from `/simplify` review:
- Promote `archived_rejection_message` and `ensure_not_archived` to `pub`
via operations/mod.rs and reuse them from `resume`, the CLI rewind
caller, and the server's `reject_if_archived` guard so the canonical
error string lives in exactly one place.
- Tighten `RewindInput.current_status` from `Option<RunStatus>` to
`RunStatus`. The runtime check for None was enforcing a compile-time
invariant. CLI callers already load the projection and now surface a
clean error up-front if it's missing. Drop the None-branch test that
existed only to cover the removed runtime check.
- Collapse `archive_run` / `unarchive_run` HTTP handlers into a shared
`run_archive_action` body with an `ArchiveAction` enum, mirroring the
CLI pattern. Removes ~20 lines of copy-paste and unifies error-mapping.
Also drop narrative comments that referenced plan unit numbers in the
scenario tests, and clean up the convoluted `ps_runs` helper pattern
that built an empty-slot arg vec before filling it in.
No behavior change. Full workspace: 4185 tests pass, clippy clean.
Bulk error messages now read 'could not be archived' / 'could not be
unarchived' instead of the broken 'could not be archive'. Caught by
manual smoke: the previous `verb_ing()` helper returned the base verb
for both forms. Dropped `verb_ing()` and reused the already-correct
`past()` helper.
- Empty-list hint in `fabro ps` now mentions archived explicitly so users
discover the new surface (plan Unit 7 follow-up).
- apps/fabro-web runs.test.ts gains an `isRunStatus('archived')` +
`runStatusDisplay` assertion so the web UI type stays in lockstep with
the Rust enum.
- `operations::rewind` now requires callers to pass `current_status`
rather than silently skipping the archived guard when absent, closing a
silent-bypass hole.
Single #[test] that exercises the full CLI archive flow: run a dry-run to
succeeded, verify ps -a shows it, archive, verify default ps hides it and
ps -a shows archived, unarchive, verify the prior terminal status is
restored, then re-archive and rm to confirm archived runs remain
delete-able (plan Scope Boundaries).
Adds the CLI-layer integration coverage the archived-run plan called for
in its Unit 6 test scenarios but never landed: help snapshots, required-arg
handling, happy paths (including ps/ps -a visibility switching), precondition
errors (archive on active runs, unarchive on not-archived runs), unknown-id
errors, idempotent no-ops, JSON output shape, and mixed-batch per-id error
aggregation. 15 new tests across archive.rs and unarchive.rs mirror rm.rs's
fabro_snapshot style.
Nothing behavioral — each change is what clippy asked for:
- fabro-test: wrap the three polling-helper thread::sleep calls in a
single poll_sleep() with an #[expect(clippy::disallowed_methods,
reason = …)] since the helpers are deliberately blocking
- fabro-test: server_log_files now uses Path::extension() with
eq_ignore_ascii_case("log") instead of a case-sensitive ends_with
- fabro-workflow: import default_storage_dir rather than calling it
through its full module path
- fabro-cli/server/record: same absolute_paths fix
- fabro-cli/main tests: use a `use tokio::runtime::Runtime` to stop
referencing `tokio::runtime::Runtime` by full path
- fabro-cli/tests: replace three `as u32` casts on as_u64() results
with u32::try_from(...).expect(…)
- fabro-cli/tests: six `format!("...", var)` assertions switched to
the inline `{var}` form clippy prefers
Full verification passes: fmt, clippy, cargo nextest (4141 tests),
bun typecheck, bun test (40 tests), bun build, SPA embed diff clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four fixes from post-merge review:
- Freeze archived runs on the remaining write surfaces the Unit 4 audit
missed: `put_stage_artifact`, `put_sandbox_file`, and `write_run_blob`
now all call `reject_if_archived` so a client cannot mutate artifacts,
sandbox files, or blobs on an archived run.
- Widen the `append_run_event` lifecycle denylist to cover every event
with a dedicated operation endpoint: archive, unarchive, and the three
control-request events (cancel/pause/unpause). Worker-emitted lifecycle
transitions and rewind's `RunRewound` / `RunSubmitted` replay still flow
through the endpoint as before.
- Map `fabro_store::Error::RunNotFound` to a distinct `Error::RunNotFound`
at the operations layer so the archive and unarchive HTTP handlers return
a 404 on unknown run ids instead of collapsing into a generic 500.
- Centralize the archived-run guard in `operations::rewind` by threading
`current_status` through `RewindInput` and calling the new
`ensure_not_archived` helper alongside a shared canonical error message.
The CLI caller drops its ad-hoc string comparison in favor of the typed
status it already loads from the server.
Phase 2/3 of the std::fs lint initiative (Phase 1 refactors landed in
commit 9d1c0d98c).
clippy.toml additions (appended to disallowed-methods):
std::fs::read, read_to_string, write, read_dir, copy, canonicalize
std::fs::File::open, File::create, File::create_new
std::fs::OpenOptions::open
File::options was deliberately excluded — it returns an OpenOptions
builder with no syscall. OpenOptions::open is where the block happens.
Non-blocking std::fs items (metadata, exists, create_dir_all, remove_*,
rename, and all std::fs types) remain legal.
Annotation policy (per updated plan):
- Mixed async/sync production source: function- or statement-scoped
#[expect(...)] so future accidental Tokio-path regressions in the
same file still fire.
- Fully-sync production source, test modules, integration tests,
build.rs: file-level #![expect(...)].
- Every #[expect] has a specific reason identifying the sync context.
Annotations added in ~90 files across the workspace. Notable narrow
placements: fabro-server server.rs current_server_target,
build_disk_usage_response, create_test_app_state_with_session_key;
fabro-server install.rs read_to_string rollback snapshot;
fabro-sandbox local.rs list_recursive; fabro-agent cli.rs FOLLOW-UP on
the JSON-stdout writer; fabro-llm providers/common.rs FOLLOW-UP for
load_file_as_base64 (7 translator call sites; revisit if file:// URL
usage grows).
build.rs blanket allows: fabro-api/build.rs, fabro-util/build.rs.
Pre-existing unrelated nightly-clippy warnings fixed under scope:
fabro-sandbox sandbox_spec.rs (unused_imports, unused_async),
reconnect.rs (unused_variables, unused_async).
Verified: cargo +nightly-2026-04-14 clippy --workspace --all-targets
-- -D warnings passes; fmt clean; 4129/4131 tests pass (two known
flakes under parallel nextest load, both pass individually and are
unrelated to this change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites apps/fabro-web/app/routes/run-files.tsx to consume the real
PaginatedRunFileList response and removes the fallbackFiles fixture
and the Steer subsystem. The new component:
- Loads via apiJsonOrNull, so a 404/501 (dev without the route)
renders the empty state instead of the root error boundary
- Branches on meta.degraded + meta.patch to render PatchDiff with a
DegradedBanner whose copy reflects degraded_reason
- Renders per-entry placeholders for sensitive, binary, symlink/
submodule, and truncated files with the priority order
sensitive > binary > symlink/submodule > truncated -- security
flags never get hidden behind a lesser placeholder
- Renders one MultiFileDiff per regular entry
- Uses role="region" + aria-label on each file row
Also unhides the Files Changed tab in run-detail.tsx by flipping
broken: true -> false. Adds missing final_patch: None to the runner
RunFailed test fixtures to match the lifecycle change from Unit 2.
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>