Commit graph

1072 commits

Author SHA1 Message Date
Bryan Helmkamp
fae575c193
fix(server): validate owner/repo path params on GitHub repo lookup
Path<(String, String)> percent-decodes segments, so an authenticated
user could send owner=foo%2F..%2Fuser (decoded to foo/../user). After
reqwest URL normalization this rewrote the GitHub API endpoint and
reissued the server's privileged token against an unintended path.

Reject anything outside [A-Za-z0-9._-] with length caps, plus the
literals "." and "..".
2026-04-18 04:39:47 -04:00
Bryan Helmkamp
7f83a627ee
fix(llm): send Gemini API key via x-goog-api-key header
API keys in query strings leak to access logs, proxies, and request
traces. Move to the header form Google documents as equivalent for both
generateContent and streamGenerateContent endpoints.
2026-04-18 04:32:23 -04:00
Bryan Helmkamp
d793046050
fix(server): harden fabro-demo cookie with HttpOnly and conditional Secure
Only the server reads this cookie (via cookie_and_demo_middleware), so
HttpOnly is safe unconditionally. Secure is gated on https:// web.url to
match the existing session cookie pattern — preserves localhost HTTP dev.
2026-04-18 02:32:01 -04:00
Bryan Helmkamp
cf80fe567a
test(harness): scrub ambient creds from spawned fabro CLI
CLI integration tests spawned the real fabro binary while letting the
parent process's env pass through. The pr_view "no credentials" snapshot
failed in CI because the Nightly workflow's minted GITHUB_TOKEN was
inherited by the child and turned the expected "credentials required"
error into a real GitHub API call (404 / 401). On developer laptops the
same leak occurs whenever gh auth login is active.

Introduce apply_test_isolation(cmd, home) in fabro-test: env_clear() +
re-populate PATH, HOME, NO_COLOR, and the FABRO_* test overrides. Route
TestContext::command(), the internal server bootstrap, and the four
ad-hoc spawners in tests/it/cmd/{attach,render_graph,runner,server_start}
through the same helper so the isolation is systemic instead of
per-callsite. Tests that deliberately need a credential (OPENAI_API_KEY,
GITHUB_APP_PRIVATE_KEY, etc.) continue to set it explicitly on the
returned Command; those survive the clear.

Add a regression test that sets sentinel GITHUB_TOKEN and
ANTHROPIC_API_KEY in the parent, spawns /usr/bin/env through the helper,
and asserts the child sees neither credential while still seeing PATH
and the harness's FABRO_NO_UPGRADE_CHECK override.

Verified: the full workspace (4022 tests) passes with GITHUB_TOKEN and
ANTHROPIC_API_KEY set in the parent, which previously broke the
pr_view_reads_pull_request_from_store_without_pull_request_json
snapshot. cargo fmt and nightly clippy are clean.
2026-04-18 01:55:01 -04:00
Bryan Helmkamp
b8ce415551
fix(musl): vendor openssl for musl targets
daytona-sdk transitively pulls native-tls via reqwest (its own
reqwest v0.12, separate from our rustls-configured workspace
reqwest v0.13). native-tls requires libssl headers at build time,
which musl-gcc cannot satisfy from the host's glibc libssl-dev.

Add a target-specific openssl dep with the vendored feature so
openssl-sys compiles openssl from source for musl builds. glibc
builds are unaffected — they continue to link against the system
libssl that CI runners already have.

Verified end-to-end: aarch64-unknown-linux-musl binary built locally
runs on Alpine 3.20 (pure musl userspace).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 01:14:02 -04:00
Bryan Helmkamp
2f2f14562d
fix(api): convert OpenAPI spec to valid 3.1 syntax
The spec declared openapi 3.1.0 but used nullable: true (3.0 idiom)
in 78 places, which Mintlify's parser rejected, breaking doc deploys.

Convert to proper 3.1 patterns (type arrays and oneOf with type: null),
switch the server conformance test from openapiv3 (3.0-only) to a
YAML-level walk so it accepts 3.1 input, and regenerate the typescript
client — it now correctly emits `| null` on nullable fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 01:11:19 -04:00
Bryan Helmkamp
828d686a6f
feat(release): add x86_64 and aarch64 musl Linux targets
Extend the release matrix to two statically-linked musl variants so
Alpine and other musl-based Linux hosts can install without glibc.
Homebrew and the Docker image remain glibc-only.

- release.yml: add x86_64-unknown-linux-musl (ubuntu-24.04) and
  aarch64-unknown-linux-musl (ubuntu-24.04-arm) matrix rows with
  musl-tools, CC_*_musl, CARGO_TARGET_*_LINKER, and LIBZ_SYS_STATIC
- Cargo.toml: enable git2 vendored-libgit2 so libgit2 compiles from
  source for every target (needed because musl cannot link against
  Ubuntu's glibc-built libgit2-dev)
- install.sh: check `ldd --version` for "musl" and rewrite the target
  from -gnu to -musl so Alpine users get the right tarball
- upgrade.rs: add detect_linux_libc() / parse_ldd_libc() helper and
  route detect_target() Linux arms through it, with unit tests
  covering glibc, musl, empty, and unknown output
- tests/it: extend target regex in the dry-run snapshot filter

Ubuntu 24.04 is required for the musl runner: 22.04 ships musl 1.2.2
which SIGSEGVs statically-linked x86_64 test binaries at startup.
Confirmed against graphviz-sys CI before landing here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 01:00:51 -04:00
Bryan Helmkamp
db953c838b
fix(test): resolve clippy and fmt findings in upgrade test helper
Clippy flagged hard_link_or_copy's match as single_match_else;
rewrite as an early-return if. rustfmt reformatted the long
chained path join in brew_command and the multi-arg
hard_link_or_copy call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:39:51 -04:00
Bryan Helmkamp
e891cb3961
test(cli): cover Homebrew upgrade behavior end to end
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Add process-level upgrade tests that invoke the real fabro binary from a
fake Homebrew Cellar path so current_exe() detection is exercised end to
end. Update the CLI reference and changelog to document the Homebrew-managed
upgrade path and the flags that remain self-managed-only.
2026-04-17 18:28:54 -04:00
Bryan Helmkamp
d535edfda4
feat(cli): brew-aware upgrade check and gate
Detect Homebrew-managed installs from the canonicalized executable path
(Cellar/fabro[-nightly]/...) and branch both the background nag and
`fabro upgrade` accordingly.

- Background check fetches the tap's versions.json (raw.githubusercontent)
  for the matching channel instead of GitHub's latest release, so the nag
  tracks what `brew update` can satisfy.
- Cache payload gains an install_source tag; entries from a different
  source (or legacy entries without the field) are ignored, so users who
  switch between fabro/fabro-nightly/tarball don't see stale versions.
- `fabro upgrade` on a brew install refuses to overwrite the
  Homebrew-managed binary and prints `brew upgrade fabro[-nightly]`.
  --dry-run prints the command and exits 0; --version/--prerelease/--force
  are rejected with a clear Homebrew-managed error.
- Manifest/exe-resolve failures on brew installs skip the notice rather
  than falling back to GitHub latest (which would reintroduce the
  tap-lag false positives this change is meant to remove).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:46:55 -04:00
Bryan Helmkamp
c1443ad3f3
fix(test): keep session locks outside session roots
Move the shared session lock file out of the deletable session root so
cleanup cannot unlink the lock another test process is relying on.
This hardens the nextest shared-server harness against dev-token startup
races and adds a regression test for the lock path.
2026-04-17 15:31:05 -04:00
Bryan Helmkamp
7bc3fe0dbc
fix: wrap propagated io errors with anyhow context across prod code
A recent CI flake surfaced as bare "error: No such file or directory
(os error 2)" with no chain, because the failing operation lived behind
a raw `?` on a `std::fs::` / `File::create` / `Command::spawn` call. The
error had no verb, no path, no hint at which step in server startup
broke. Retry loops were explicitly rejected -- the goal is to diagnose
the next occurrence, not mask it.

Wraps 50+ such sites across fabro-cli, fabro-server, fabro-workflow,
fabro-util, fabro-vault, fabro-telemetry, fabro-interview, fabro-llm,
and fabro-devcontainer with `.with_context(|| format!("<verb> {path}"))`
so anyhow's error chain carries both the operation and the path when
an io error escapes.

Where the enclosing function returns `io::Result` (fabro-util run_log,
fabro-interview recording, fabro-llm attachment loader), the error is
re-wrapped via `io::Error::new` to keep the signature stable. Where a
crate uses its own thiserror enum, either a new `io_context` helper
was added (fabro-vault) or the path was folded into the existing
`Error::Io(String)` message (fabro-workflow).

No retry loops. No behavior changes. Skipped sites documented:
`.ok()`-swallowed, `match ErrorKind::NotFound`, `let _ = ...`, typed
error variants that already carry the path, and test modules.

Verified: cargo build --workspace, cargo +nightly clippy --workspace
--all-targets -- -D warnings, cargo nextest run --workspace (3991/3991
pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:44:38 -04:00
Bryan Helmkamp
a1776b7beb
fix(test): skip write-key sender assertions when CI bakes the key in
Two fabro-telemetry sender tests assert that upload/upload_blocking
return an error with "SEGMENT_WRITE_KEY not set" -- a claim that only
holds when SEGMENT_WRITE_KEY is absent at compile time. The release
workflow sets the secret at build time, so these tests now fail under
`cargo nextest run --workspace` in release CI (newly exercised after
switching the release workflow from `cargo test` to nextest).

Guard each assertion with an early return when SEGMENT_WRITE_KEY is
compiled in so the test passes in release CI while still verifying the
no-key path for every other build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:05:39 -04:00
Bryan Helmkamp
7bf0c6678e
feat(cli): add fabro docs to landing Set up section
Surfaces the docs website shortcut alongside the other onboarding
commands so new users can find the web docs without hunting through
`fabro help`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:03:40 -04:00
Bryan Helmkamp
afcbb0628a
refactor(cli): tighten landing footer and drop sandbox cp/preview
- Replace the three-line "help along the way" trio with a Discord
  callout in the style of qlty's landing: one line pointing at per-
  command --help, one inviting users to the Fabro Discord.
- New final line uses qlty's dim/cyan split: "For a full list of
  commands, run `fabro help`."
- Drop `sandbox cp` and `sandbox preview` from the curated list;
  `sandbox ssh` stays. Both are still reachable via `fabro help`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:03:08 -04:00
Bryan Helmkamp
ae2efaabf9
refactor(cli): regroup landing output around lifecycle stages
Moves `server start` and `secret set` into Set up so the first section
covers everything a user does once before they have a working install.
Drops `secret list` from the landing — `fabro --help` still surfaces it.
Merges the run-inspection commands (`logs`, `sandbox ssh`, `sandbox
preview`, `sandbox cp`) under a single "Inspect runs" heading, since
they all answer "what happened in this run?".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:01:08 -04:00
Bryan Helmkamp
a4159f7bb6
feat(cli): show curated landing output for bare fabro
Running `fabro` with no subcommand now prints a short, colorized guide
highlighting the most important commands (install, doctor, repo init,
validate, preflight, run, logs, server start, secret set/list, sandbox
ssh/preview/cp) instead of clap's full --help dump.

`fabro --help` and `fabro help` still render clap's comprehensive
reference unchanged. The CLI makes the root subcommand optional and
intercepts the None case in main_inner before telemetry or logging
init; the empty command name also suppresses the "CLI Executed"
tracking event for this pseudo-command.

Includes an inline-snapshot IT test covering the full landing body and
updates four pre-existing usage-line snapshots (<COMMAND> → [COMMAND])
that reflect the now-optional subcommand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 08:59:03 -04:00
Bryan Helmkamp
1e147c0d88
fix(cli): prevent telemetry from recreating ~/.fabro after uninstall
In release builds with SEGMENT_WRITE_KEY baked in (i.e. CI), the
post-command telemetry flush calls spawn_fabro_subcommand, which in turn
does create_dir_all(~/.fabro/tmp) and writes a JSONL event file. That
silently undoes the directory removal that `fabro uninstall --yes` just
performed — leaving a stray ~/.fabro/tmp/ behind and breaking the
uninstall integration tests on CI release runs.

Fixes:
- run_uninstall calls fabro_telemetry::shutdown() before removal so the
  buffered "CLI Executed" track in main() can't be queued or flushed,
  and the background thread can't spawn the sender subprocess.
- TestContext::command() exports FABRO_TELEMETRY=off so test subprocesses
  never initialise telemetry at all, as a belt-and-suspenders guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 08:40:04 -04:00
Bryan Helmkamp
5946047202
deps: bump rand 0.8 → 0.9 (#163)
## Summary
- Bumps `rand` from 0.8 to 0.9 to resolve Dependabot alert #16 (low
severity: "Rand is unsound with a custom logger using rand::rng()",
fixed in 0.9.3).
- Migrates call sites to the renamed 0.9 APIs.

## API changes applied
- `rand::thread_rng()` → `rand::rng()`
- `Rng::gen_range(..)` → `Rng::random_range(..)`
- `Rng::gen::<T>()` → `Rng::random::<T>()`
- `OsRng.fill_bytes(..)` → `OsRng.try_fill_bytes(..).expect("OS RNG")` —
in 0.9 `OsRng` implements `TryRngCore` instead of `RngCore` directly.

## Test plan
- [x] `cargo build --workspace`
- [x] `cargo nextest run -p fabro-util -p fabro-server -p fabro-workflow
-p fabro-oauth` (1270 passed)
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 07:57:10 -04:00
Bryan Helmkamp
4c835c95b0
fix(test): make release-mode unit tests pass workspace-wide
- server_start::help: correct --watch-web description indent from 14 to 10 spaces
  in the stripping regex so the filter matches in debug builds where the flag
  is present in --help output.
- fabro-telemetry: split telemetry_level default test into debug/release
  variants. The function's default depends on cfg!(debug_assertions), so the
  prior single test panicked under cargo test --release.

Verified: cargo nextest run --workspace (debug) → 3990 passed; cargo test
--workspace --release → all 69 test binaries pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 07:48:08 -04:00
Bryan Helmkamp
7fda2ee25b
fix(test): update snapshots for version and --prerelease flag
- Update VERSION filter regex to handle prerelease suffixes (e.g., 0.204.0-beta.1)
- Add VERSION filter to JSON snapshots in fabro_json_snapshot macro
- Fix attach test to use [VERSION] placeholder instead of hardcoded version
- Update upgrade help snapshot to include new --prerelease flag
- Change fake version in upgrade test from v0.176.3 to v999.0.0 to avoid collision
- Strip --watch-web from server start help (debug-only flag, varies by build)
- Gate test_panic module with #[cfg(debug_assertions)] (debug-only command)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 07:31:40 -04:00
Bryan Helmkamp
0b77a3efa2
feat(cli): add --prerelease to fabro upgrade
Widens the candidate set to include prereleases, picks max semver across
stable + prereleases. Falls back to /releases/latest if no parseable
non-draft tag is returned. Conflicts with --version. Background
auto-upgrade notice remains stable-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 20:20:14 -04:00
Bryan Helmkamp
3ed65d3728
fix(test): consume stdin in render_graph RENDER_ERROR fake subprocess
The fake /bin/sh script in the render_error protocol test printed and
exited without reading stdin, which raced the parent's write_all on
Linux — EPIPE would surface as ChildCrashed (500) instead of the
RenderFailed path (400) the test asserts. macOS pipe buffering masked
the race. Adding `cat >/dev/null` mirrors the sibling
protocol_violation test and makes the child consume the DOT input
before printing the RENDER_ERROR line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:51:59 -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
eb596c057d
clippy 2026-04-16 18:20:37 -04:00
Bryan Helmkamp
6e670e8b54
fix: unblock clippy and nextest on main
- serve.rs: annotate debug-only `bun run dev` spawn with
  #[expect(clippy::disallowed_methods, ...)] and add the missing
  watch_web field to three ServeArgs test fixtures.
- install.rs: replace absolute `fabro_server::serve::DEFAULT_TCP_PORT`
  path with `serve::DEFAULT_TCP_PORT` (use is already imported) to
  satisfy clippy::absolute_paths.
- pagination test: request an explicit page[limit]=100 for the
  "fits in one page" case instead of relying on the server default,
  so the test stays robust as the built-in model catalog grows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 18:16:06 -04:00
Bryan Helmkamp
44e7cbcaa7
Update SPA 2026-04-16 17:35:37 -04:00
Bryan Helmkamp
1dd59ceea5
wip 2026-04-16 13:13:09 -04:00
Bryan Helmkamp
77c8ac8ed3
feat(cli): add server restart command
Stops any running server then starts a fresh one, passing through all
the same flags as `server start` (--watch-web, --foreground, etc.).
Works even if no server is currently running.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:49:16 -04:00
Bryan Helmkamp
67baf8b4d8
feat(cli): add --watch-web flag to server start (debug builds only)
Spawns `bun run dev` in apps/fabro-web as a child of the server process,
so a single command starts both the API server and the web asset watcher.
The flag is gated behind #[cfg(debug_assertions)] and does not exist in
release builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:42:54 -04:00
Bryan Helmkamp
a79f9ae43f
fix(install): use consistent web URL for GitHub App callback and generated settings
--web-url defaulted to http://localhost:3000 but merge_server_settings
hardcoded http://127.0.0.1:32276, causing GitHub OAuth redirect_uri
mismatch. Now both derive from the same --web-url flag (default:
http://127.0.0.1:32276 via DEFAULT_TCP_PORT).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:29:48 -04:00
Bryan Helmkamp
2d6742d416
feat(cli): show Web UI URL and auth methods in server start output
Print the HTTP URL (cyan) and enabled auth methods after server start,
so users can see at a glance how to access the server and what login
methods are available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:21:55 -04:00
Bryan Helmkamp
1830724fa4
fix(cli): improve device code login readability with blank line spacing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:20:57 -04:00
Bryan Helmkamp
e4f2208255
fix(tests): stabilize graph path snapshots and skip LLM preflight for non-LLM graphs
- Update 5 more dry_run_examples snapshots to use [GRAPH_PATH] filter
- Skip LLM preflight check when graph has no LLM nodes (fixes
  preflight_allows_pull_request_enabled_without_github_credentials)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:41:17 -04:00
Bryan Helmkamp
79fee95c97
fix(ci): stabilize snapshot test and double CLI test timeout
- Add [GRAPH_PATH] filter to run_output_filters so dry_run_simple
  snapshot is path-independent
- Double fabro-cli slow-timeout (3s → 6s) to prevent ps test timeouts
- Preserve cloud sandboxes; bump snapshot to fabro-v7 with 8 CPU / 16GB

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:39:06 -04:00
Bryan Helmkamp
af408dc66c
fix(tests): make bulk_skip model test independent of local credentials
The snapshot was overfit to local env — it hardcoded Anthropic models
passing because the server inherited ANTHROPIC_API_KEY from the test
runner. On CI with no API keys, all models are skipped and the snapshot
diverged.

Replace the snapshot with targeted assertions: exit code 0 and "Skipped"
appears in stderr. This works regardless of which credentials are
available in the test environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:22:02 -04:00
Bryan Helmkamp
2106aba53b
fix(tests): isolate CLI integration tests from repo project config
Config discovery walks from the workflow file's parent directory, so
tests using fixtures at their repo path (test/simple.fabro) would find
the repo's .fabro/project.toml. This caused settings like preserve=true
to leak into tests and break sandbox cleanup event assertions.

Add TestContext::install_fixture() which copies fixtures into the test's
temp dir. Update all CLI run/attach/start tests to use it. Remove the
now-unused example_fixture() function.

Restore preserve=true in .fabro/project.toml — tests are now isolated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:18:19 -04:00
Bryan Helmkamp
5fa6b6e7a8
fix: resolve test failures and clippy warnings across workspace
- Remove EnvGuard and env-mutating test from fabro-auth (shared mutable state)
- Revert project.toml preserve=true that broke sandbox cleanup event tests
- Fix clippy: use is_some_and, scoped imports for StageStatus and render
- Update snapshot tests for new Run: ULID line and model_test output
- Fix preflight test assertion (name said "allows", asserted failure)
- Update cancel_queued_run test: cancelled runs now appear on board
- Add graph direction query param support to get_graph endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:02:20 -04:00
Bryan Helmkamp
de78094d7e
fix: include duration_ms in stage.failed events for sidebar display
StageFailedProps lacked duration_ms, so extract_stage_durations_from_events
only found durations from stage.completed events. Failed stages showed "0s"
in the sidebar despite the command.completed event having the real duration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:42:58 -04:00
Bryan Helmkamp
3a97c1fdd9
server 2026-04-15 10:15:33 -04:00
Bryan Helmkamp
130d76729c
fix(server): show running stage immediately via checkpoint next_node_id
The stages API used checkpoint.current_node to identify the running
stage, but current_node is the last *completed* node — always already
in completed_nodes, so the running-stage check was always false.
Switch to checkpoint.next_node_id which correctly identifies the
currently-executing stage.

Also move SSE subscription from run-detail parent layout into the
StageSidebar component with since_seq=1 to replay all events and
close the race between loader fetch and SSE connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:03:26 -04:00
Bryan Helmkamp
75ab9965dd
feat(web): wire up non-demo run detail pages to real data
- Fix run detail status: display actual API status (submitted, running,
  succeeded, failed, etc.) instead of always showing "Working"
- Implement /runs/{id}/stages endpoint in non-demo mode, reading from
  checkpoint + events to build stage list with statuses and durations
- Fix /runs/{id}/graph to fall through to durable store when run is not
  in the live map
- Render real workflow graph SVG on overview and graph pages instead of
  hardcoded demo graph; remove unused DotDiagram component from overview
- Add dark mode CSS overrides for server-rendered SVG graphs
- Wire stage detail page to real event data: fetch from /events, filter
  by node_id, and render as system/assistant/tool blocks
- Fix stage page 500: use apiJsonOrNull for unimplemented /turns endpoint
- Filter start/exit graph control nodes from stage lists in the UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:42:49 -04:00
Bryan Helmkamp
80cb51300b
fix(web): update demo data to use BoardColumn::Initializing
Missed in the pending → initializing rename.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:26:50 -04:00
Bryan Helmkamp
a90038f7a7
refactor(web): rename board column pending → initializing
Clarifies that Submitted/Starting runs are initializing, not just
pending. Also refactors run-detail to display the actual run status
via runStatusDisplay instead of mapping to board columns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:22:17 -04:00
Bryan Helmkamp
e79baeab8e
fix(cli): align Run ID with other run header lines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:20:07 -04:00
Bryan Helmkamp
abfdb9e523
fix(cli): restore Run ID in fabro run output header
Lost during the execute.rs → create.rs refactor in db3231e9.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:16:38 -04:00
Bryan Helmkamp
054dc5a85a
fix(model): use Self instead of Provider in method signature (clippy)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:14:21 -04:00
Bryan Helmkamp
6d4b26579b
fix(sandbox): wire vault DAYTONA_API_KEY through to Daytona SDK client
The Daytona SDK client was created via Client::new() which only reads
DAYTONA_API_KEY from process env vars. When the key is stored in the
fabro vault (via `fabro secret set`), it was never forwarded to the SDK,
causing "api_key or jwt_token must be provided" errors.

Thread the API key from the vault through SandboxSpec, DaytonaSandbox,
and reconnect paths so the SDK receives it via new_with_config().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:13:55 -04:00
Bryan Helmkamp
84f3c80566
refactor(api): move features flags from /auth/me to /system/info
Features like session_sandboxes and retros are server-level capability
flags, not user settings. Expose them on GET /system/info where they
belong alongside other server metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:51:22 -04:00
Bryan Helmkamp
a12ceb0ad0
fix(web): board runs endpoint reads from store, not in-memory state
The /boards/runs endpoint was driven by the in-memory state.runs map,
which is empty after server restart. Now reads from SlateDB store so
runs persist across restarts.

Also makes board columns dynamic from the API response instead of
hardcoded in the frontend. Real mode returns: pending, running, waiting,
succeeded, failed. Demo mode returns: working, pending, review, merge.

Board layout changed from fixed 3-column grid to horizontal scroll.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:36:55 -04:00