Commit graph

4261 commits

Author SHA1 Message Date
Bryan Helmkamp
4ab959f821
Merge pull request #666 from fabro-sh/fix/output-schema-outermost-object
Fix nested output schema validation
2026-07-28 16:40:13 -04:00
Bryan Helmkamp
9f32e14400
Merge pull request #667 from fabro-sh/fix/retry-transient-sandbox-clone
Retry the sandbox clone when GitHub token replication lags
2026-07-28 16:39:43 -04:00
Bryan Helmkamp
4727ee8e75
Harden sandbox clone retries 2026-07-28 16:26:41 -04:00
Release Repro
7bacd4cf12
refactor(workflow): scan for outermost JSON objects everywhere
Replace the include_nested flag and its two wrapper functions with a
single outermost-only scanner. Routing extraction used the nested scan
and reverse iteration, so a routing object nested inside a wrapper could
win over its parent -- the same bug class this branch fixes for custom
schemas. No caller needs nested candidates.

Custom schema validation now walks candidates from the end and takes the
last one that parses, instead of parsing only the final candidate. Prose
after the object can contain braces, and outermost-only scanning made
that trailing text a candidate that shadowed the real JSON. Schema errors
are still reported from the last parsable object, so an earlier object
that happens to validate cannot mask a later violation.

Add direct scanner coverage for nesting, adjacent objects, unclosed
braces, and braces inside strings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:10:47 -04:00
Bryan Helmkamp
450a1701b2
Merge pull request #670 from fabro-sh/fix/sandbox-activate-before-access
Reactivate stopped sandboxes at workflow boundaries
2026-07-28 15:54:57 -04:00
Bryan Helmkamp
ca5423197d
Merge pull request #669 from fabro-sh/feat/run-dock-collapse-and-clearance
Give the run docks a shared collapsible shell
2026-07-28 15:47:15 -04:00
Release Repro
e21e6bcdf9
refactor(sandbox): harden activation recovery 2026-07-28 15:38:35 -04:00
Bryan Helmkamp
96604429af
Merge remote-tracking branch 'origin/main' into codex/pr669-merge-main-3d8cf48
# Conflicts:
#	apps/fabro-web/app/routes/run-detail/model.ts
2026-07-28 15:35:42 -04:00
Bryan Helmkamp
3d8cf48f9b
fix(web): harden collapsible run docks 2026-07-28 15:24:58 -04:00
Bryan Helmkamp
f4384e9901
Merge pull request #659 from fabro-sh/feat/file-primary-artifacts-view
Show run artifacts by file with version history
2026-07-28 15:02:08 -04:00
Bryan Helmkamp
34656aa7a4
Merge pull request #672 from fabro-sh/refactor/validation-catalog-cleanup
fix(tool): finish the catalog-free validation work from #651
2026-07-28 14:59:28 -04:00
Bryan Helmkamp
c083fa5209
Merge pull request #658 from fabro-sh/feat/stage-model-popover-billing
Show stage tokens and cost in the model popover
2026-07-28 14:58:51 -04:00
Bryan Helmkamp
1b987528dd
Simplify artifact version history 2026-07-28 14:57:05 -04:00
Bryan Helmkamp
0681d1e6ee
docs: clarify run-create undefined variable handling 2026-07-28 14:52:02 -04:00
Release Repro
acff084afd
refactor: simplify stage billing reuse 2026-07-28 14:51:47 -04:00
Bryan Helmkamp
40372340c0
fix(tool): keep fabro_run_create validation catalog-free
PR #651 stopped `fabro validate` and `fabro create` from judging model and
provider availability locally, but left the `fabro_run_create` tool path
doing exactly that. Both of its callers build a *client-side* catalog and
then POST the manifest to the server, so an agent naming a server-owned
model got `Model selection failed: unknown model provider '...'` while the
same workflow succeeded through the CLI.

- `build_run_tool_manifest` now validates structurally, matching the CLI.
  It no longer takes a catalog at all.
- The MCP builder drops its `load_llm_catalog_settings` +
  `Catalog::from_builtin_with_overrides` pair, and `WorkerRunManifestBuilder`
  drops its catalog field, becoming a unit struct.
- `validate_manifest_with_catalog` had no callers left, so it is gone.
  `validate_manifest` documents why every remaining caller is catalog-free.

The new test fails with the pre-fix client-side check, reproducing the
reported error exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 14:40:54 -04:00
Bryan Helmkamp
ee89caf649
Merge pull request #647 from fabro-sh/fix/live-active-time-accumulation
fix(timing): accumulate active time for in-flight stages
2026-07-28 14:32:31 -04:00
Bryan Helmkamp
09ef90ba1e
refactor: pass the catalog by value and correct the RenderMode docs
- Take `Arc<Catalog>` by value again through the validation entry points.
  `AppState::catalog()` returns an owned `Arc`, so `&state.catalog()` was
  cloning, borrowing the temporary, then cloning again at the leaf. Every
  consumer ends up owning the `Arc`, so by-value is the honest shape and it
  drops one clone per call. The one caller holding the catalog in a field
  now says `Arc::clone(&self.catalog)` explicitly.
- Correct the `RenderMode` doc comment. It claimed `Strict` is "used by
  run-create", but run-create renders `Structural` and promotes the
  resulting warnings to errors itself; `Strict` has no production caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 14:30:48 -04:00
Bryan Helmkamp
6bd6712a5f
Merge pull request #657 from fabro-sh/feat/runs-list-size-tooltip
Show run size chips consistently across list, board, and detail
2026-07-28 14:29:34 -04:00
Bryan Helmkamp
c5d3345144
Merge pull request #651 from fabro-sh/fix/offline-validation-catalog-free
fix(cli): keep offline validation catalog-free
2026-07-28 14:28:35 -04:00
Bryan Helmkamp
a10ffb02b5
Merge pull request #654 from fabro-sh/fix/reject-edge-only-nodes
Reject nodes referenced by an edge but never declared
2026-07-28 14:28:11 -04:00
Bryan Helmkamp
d6ac910e73
refactor: reuse ModelResolutionTransform instead of a parallel options type
`ModelResolutionOptions` was a field-for-field duplicate of the existing
public `ModelResolutionTransform`, down to a verbatim copy of its `new()`.
`pipeline::transform` then unpacked one to rebuild the other, cloning the
catalog Arc and the eligible-provider set on the way.

- Delete `ModelResolutionOptions`. `TransformOptions.model_resolution` now
  holds an `Option<ModelResolutionTransform>` directly, so the TRANSFORM
  step is `resolution.apply(graph)?` with no rebuild and no clones. This
  is consistent with `custom_transforms`, which already holds transforms.
- Add `ModelResolutionTransform::catalog()` so the VALIDATE step can reach
  the same catalog for its lint rules. That is the only new code needed.
- Drop `CatalogScope` from `operations::validate`, which was a third copy
  of the same fields. The three entry points now hand a partially built
  transform to `validate_resolving_models`, which completes it with the
  workflow's default provider once the workflow is resolved.
- Extract `validate_child_workflow` in `manager_loop`, collapsing two
  near-identical validate-and-unwrap blocks.
- Point the transform tests at their own `transform_options()` helper via
  struct-update syntax instead of respelling all seven fields, and drop a
  HashSet -> Vec -> HashSet round trip from the create test helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 14:24:26 -04:00
Bryan Helmkamp
96f70a9aea
fix(timing): simplify live timing projections 2026-07-28 14:20:59 -04:00
Bryan Helmkamp
04c931e9d3
Simplify edge-only node validation 2026-07-28 14:14:32 -04:00
Bryan Helmkamp
8592a34968
refactor: collapse the split validation paths
Follow-up cleanup on the catalog-free validation split. Same behavior,
fewer parallel code paths.

- Make the catalog an explicit `Option<&Catalog>` on `pipeline::validate`
  instead of a `validate` / `validate_with_catalog` pair, so each call
  site states whether catalog rules run.
- Collapse `preprocess_and_validate`, `preprocess_and_validate_structural`,
  and `preprocess` into one function that takes `TransformOptions`. Its
  `model_resolution` field is now the single source of truth for catalog
  awareness, which drops a 12-argument signature and the
  `too_many_arguments` allow.
- Replace the duplicated resolve-and-preprocess block in
  `operations::validate` with one `validate_in_scope` helper, and drop the
  HashSet -> Vec -> HashSet round trip on the catalog path.
- Extract `configured_default_provider`, previously duplicated between
  `operations::create` and `operations::validate`.
- Delete `validate_manifest_with_environment_defaults`, which had no
  callers outside its own module.
- Share the `server-model.fabro` fixture between the two CLI tests instead
  of inlining it twice. The validate test now asserts the rendered output
  through the usual snapshot helper, which also removes a hand-rolled
  `std::fs::write` and its clippy allow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 14:13:45 -04:00
Bryan Helmkamp
dc79da0dab
Clean up size chip rendering and tests 2026-07-28 14:04:58 -04:00
Bryan Helmkamp
465bdd6c27
Redirect test storage through with_storage_override
The test helper set server.storage.root directly, leaving the derived
local object-store roots (artifacts, slatedb) pointing at the real
~/.fabro/storage. Route the redirect through
ServerSettings::with_storage_override so every derived root moves to the
test directory together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:50:16 -04:00
Bryan Helmkamp
cc57a731f6
Merge pull request #662 from fabro-sh/perf/test-storage-root-off-real-home
Keep server tests off the developer's real ~/.fabro/storage
2026-07-28 13:45:55 -04:00
Bryan Helmkamp
80f321073f
fix(sandbox): unpause paused Docker containers 2026-07-28 13:38:47 -04:00
Bryan Helmkamp
7906af3f3f
fix(sandbox): reactivate stopped run sandboxes 2026-07-28 12:50:20 -04:00
Bryan Helmkamp
46e8d92195
Give the run docks a shared collapsible shell
The interview question panel could take half the viewport, and the page
reserved a fixed 18rem beneath it regardless of how tall it actually was,
so a long question covered the stage rows it was asking about.

Add a shared `RunDockShell` for the two controls docked at the bottom of
the run detail route. It is three zones: a header that is always visible
and doubles as the collapsed bar, a body that scrolls, and actions that
stay pinned so the controls needed to answer or send never scroll out of
reach.

The interview dock drops the question-type subtitle the answer buttons
already state, turns the 160px context box into a closed disclosure with
a first-line preview, drops the "or" divider row, and reveals the
keyboard hint on focus inside the composer row instead of standing below
it. Options stack into a list once a label is too long to sit in a pill.
For the sample question this is 506px down to 325px, or 43px collapsed.

The steering dock gains the same header. `Interrupt` moves into it,
because it acts on the run rather than on the message being composed, and
the waiting notice folds into the header status instead of adding a row.
Both docks now share one composer.

Clearance is measured from the rendered dock rather than assumed. The
former constants remain as the pre-measurement first frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:11:04 -04:00
Bryan Helmkamp
d2510447fe
Retry the sandbox clone when GitHub token replication lags
A Daytona-backed run could fail five seconds after start when the sandbox
git clone hit a transient GitHub "Repository not found" error. Clone-based
providers mint an installation access token and clone with it in the same
breath, but GitHub replicates a new token to its edge cache sites
asynchronously. A clone that starts within a second of the mint can be
rejected before the token is visible to the site serving it, and on a
private repo that rejection arrives as "Repository not found" because
GitHub answers unauthorized reads with 404.

Nothing retried the clone, and the failure classified as `deterministic`,
which is the one category `loop_restart` refuses to restart. An identical
run relaunched 46 seconds later succeeded with no changes.

A successful mint is what makes the message safe to retry.
`resolve_clone_credentials` already fails loudly on every deterministic
explanation for a clone 404: the installation lookup 404s when the App is
not installed for the owner, and token creation 422s when the installation
does not cover the repo. Once credentials are in hand, "not found" from the
clone itself cannot mean "no access".

Add `clone_retry` and use it from both clone-based providers: 3 attempts
with 3s then 9s backoff, reusing the same token so replication keeps making
progress instead of restarting the clock. Token-replication signatures
retry only when credentials are present, so a public clone of a wrong URL
still fails fast. Infrastructure failures retry either way.

The Docker provider had the identical single-shot clone and is the default
runtime provider, so it is covered too.

Also fix two nearby issues found while reading the area:

- The GitHub-URL-parse path in the Daytona clone skipped `fail_init`,
  unlike every sibling path, so `InitializeFailed` was never emitted.
- The `classify_exec_failure` hint for "repository not found" asserted the
  App installation may not cover the repo. After a successful scoped mint
  that diagnosis is impossible, and it sent operators hunting a
  configuration problem that did not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 09:03:47 -04:00
Bryan Helmkamp
813e73a0bb
fix(workflow): validate outermost custom output object 2026-07-28 07:50:31 -04:00
fabro-releases[bot]
8cc711463b Bump version to 0.308.0-nightly.0 2026-07-28 10:11:13 +00:00
Bryan Helmkamp
b97b253dee
Drop the special-case timeout for all_spec_routes_are_routable
The test needed room only because it was walking the developer's real
~/.fabro/storage. Now that it runs in about a second, the package-wide
fabro-server timeout covers it with plenty of margin.

The override was not doing anything anyway: nextest resolves each setting
from the first matching override, and `package(fabro-server)` was defined
above it, so the narrower filter never applied. Removing it makes the
config say what was already true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:53:49 -04:00
Bryan Helmkamp
9e744f8072
Keep server tests off the developer's real ~/.fabro/storage
Test settings usually omit `[server.storage] root`, so it resolved to the
production default. Handlers that walk that tree read whatever the machine
happened to have.

That is why all_spec_routes_are_routable was slow. Timing every request in
it showed 91% of the runtime in two routes:

  6304ms  GET /api/v1/system/resources
  4574ms  GET /api/v1/system/df
   583ms  POST /api/v1/system/prune/runs
  ...
  the remaining 134 operations: 8ms combined

Both size Fabro-managed storage. On this machine that meant 193MB and 90,795
entries under scratch/, so the test's duration tracked how long the developer
had been running Fabro locally. Run-creating tests were writing there too.

Redirect settings that still carry the production default to a `storage`
directory beside the test vault, alongside the existing `server.env` and
`settings.toml` siblings. A test that chose its own root keeps it.

all_spec_routes_are_routable drops from ~15s to 0.6s, and the full workspace
run from ~33s to ~21s. All 7402 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:45:27 -04:00
Bryan Helmkamp
4d4eb2c4e6
Show run artifacts by file with version history
The artifacts page grouped captures by stage, which is the storage key
`(stage, retry, path)` rather than anything a reader thinks in. A file
rewritten by four stages appeared as four separate rows under four
headings, with no indication they were the same file.

Group by path instead. Each file is one row showing its latest capture;
earlier captures disclose inline behind a chevron with the producing
stage, size, and the byte change that capture introduced.

Three fixes fall out of the regrouping:

- Order versions by the producing stage's `startedAt`. The previous sort
  was alphabetical by stage label, which scrambled history — a report
  that grew 8.42 KB -> 13.16 -> 14.32 -> 17.48 rendered newest-first
  under a heading implying it was the earliest.

- Drop captures from graph control nodes (`start`, `exit`) via the
  existing `isVisibleStage` helper. Those nodes run no work, so the
  files they match are pre-existing workspace files swept up by the
  capture globs, not run output. This is display-side only; the capture
  path still stores them.

- Show the retry badge at `retry > 1` rather than `retry > 0`. Attempts
  are 1-based, so the old condition matched every capture and rendered
  a "retry 1" badge on every group.

Grouping lives in a separate module so it is testable without React.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:45:47 -04:00
Bryan Helmkamp
7841a77f2c
feat(web): show stage tokens and cost in the model popover
The model indicator on a stage page hovered to provider, model, and
reasoning effort only. Seeing what a stage actually spent meant leaving
for the Billing tab, which reports per node rather than per visit.

The stage list had no token data to show, so add a per-visit `billing`
block to `GET /runs/{id}/stages`. The Billing tab's pricing rule (a
provider-reported cost wins, otherwise the server catalog prices the
tokens) was private to `billing_rollup`; move it to
`StageProjection::billed_usage` and drive both call sites from it so the
two views cannot drift.

The popover's buckets use the Billing tab's labels verbatim. It stays
scoped to one visit, so a looped node's row on the Billing tab is the sum
of what each of its visits shows here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:06:19 -04:00
Bryan Helmkamp
53c580ce5f
Swap the board card's elapsed time for a size chip
The board cards showed wall-clock duration in the footer's bottom-right
corner. Replace it with the same SizeChip the list view and run detail
header use, so the cost signal is consistent across all three views.

The chip inherits the tooltip, which names the tier and adds the cost
once a run has terminal billing.

Add SizeChip tests pinning the tooltip label for each tier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:25:44 -04:00
Bryan Helmkamp
991f160a0b
Drop "billed" from the size chip tooltip
The tooltip now reads "Size M · $12.34" instead of
"Size M · $12.34 billed".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:01 -04:00
Bryan Helmkamp
716ba17780
Show billed amount in runs list size tooltip
The Size column in the runs list rendered SizeChip without the billed
total, so its tooltip read "Size M" while the run detail header showed
"Size M · $12.34 billed".

The tooltip was also unreachable: the row title link paints a
`before:absolute before:inset-0` overlay across the whole row, which sat
above the chip and swallowed hover. Wrapping the chip in `relative z-10`
lifts it above that overlay, matching how the created-by and pull request
cells already handle interactive content.

Runs without terminal billing keep the plain "Size M" label, same as the
header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:19:57 -04:00
Bryan Helmkamp
c501c67185
Show each diagnostic's suggested fix in CLI output
Diagnostics have carried a `fix` field all along, but the CLI renderer
never printed it — the suggestion was only reachable through --json. The
actionable half of every validation failure was invisible to the person
running the command.

print_diagnostics now emits the fix as a dim-labelled continuation line
under any diagnostic that has one, at both error and warning severity.
Gating it behind --verbose would defeat the point, and printing it only
for errors would read as "this warning has no fix" — the warning
suggestions are useful on their own. Diagnostics that set no fix simply
omit the line.

The severity match moved into print_diagnostic so the fix line is
appended once in the loop rather than copied into all five arms; the
rest of the diff is reindentation.

print_diagnostics is shared by validate, preflight, graph, exec, and
dry-run, so this covers all five. Eleven inline snapshots across four
files gain a fix line; every change is additive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:06:31 -04:00
Bryan Helmkamp
59b1c2e59f
Reject nodes referenced by an edge but never declared
The DOT parser created a node for every edge endpoint, and nothing
recorded whether a node came from a declaration or was synthesized from
an edge. The edge_target_exists rule only checked whether the node id
was present in the graph, which was always true by then, so a misspelled
endpoint became an attribute-free node that defaulted to shape=box — an
LLM stage. Validation emitted a prompt_on_llm_nodes warning and exited 0.

Node now carries `implicit`, set only when the parser synthesizes the
node from an edge endpoint. A declaration anywhere in the workflow
clears it, so order does not matter and subgraph declarations count.
Node::new leaves it false, so programmatic construction and graphs
deserialized from older checkpoints read as declared.

edge_target_exists treats an endpoint as valid only when it exists and
is declared, reporting each undeclared node once. The near-identical
missing-source and missing-target branches collapse into one path. The
import transform copies the flag onto spliced nodes so an edge-only node
inside an imported fragment is caught too.

parse_and_validate_human_gate had two edge-only nodes and now declares
them; it was an instance of the bug rather than a casualty of the fix.
No shipped workflow, docs example, or CLI fixture relied on the old
behavior.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:53:54 -04:00
Bryan Helmkamp
6efba896f4
Add Chisel quality calibration 2026-07-27 11:58:54 -04:00
Bryan Helmkamp
6bb6b5efcc
Add codebase cartography map 2026-07-27 10:15:42 -04:00
Bryan Helmkamp
0b24649e76
fix(cli): keep offline validation catalog-free 2026-07-26 09:25:47 -04:00
Bryan Helmkamp
dd9f75fb05
fix(timing): harden live active projections 2026-07-25 23:43:43 -04:00
fabro-releases[bot]
2bcf94fed8 Bump version to 0.305.0-nightly.3
Some checks failed
Rust / Format (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Generated Docs (push) Has been cancelled
Rust / Test (Linux) (push) Has been cancelled
Rust / Test (macOS) (push) Has been cancelled
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
2026-07-25 19:35:43 +00:00
Bryan Helmkamp
e88cabbb75
Merge pull request #645 from fabro-sh/feat/spa-build-version-detection
Tell open tabs when a new build ships
2026-07-25 15:19:36 -04:00
Bryan Helmkamp
fcf469adc2
Merge pull request #646 from fabro-sh/fix/remove-read-before-write-guard
fix(agent): remove the read-before-write guard
2026-07-25 15:17:59 -04:00