Commit graph

2407 commits

Author SHA1 Message Date
Bryan Helmkamp
ea176b6ca1
feat: bundle workflow package files for remote runs 2026-07-29 11:02:03 -04:00
Bryan Helmkamp
8d14b54994
Bound for_each fan-out memory
Addresses a Copilot review comment on #653.

The source array is runtime data, usually produced by a model, so its
length is not something a workflow author reviewed. Two changes, so an
over-long array degrades into a clear error rather than memory pressure.

Cap the item count at 1000. Above that the stage fails deterministically
before `parallel.started`, alongside the other for_each contract
violations, and the message says how to reduce the array.

Fork the parent context inside the branch task, after it acquires a
`max_parallel` slot, instead of at dispatch time. Live context copies now
track `max_parallel` rather than item count. Only the branch's own
preamble entry is moved into the task, so the shared stash is not cloned
per branch either.

The reviewer also suggested replacing spawn-all with `max_parallel`
workers pulling from a queue. Not done here: with the fork deferred, a
pending task holds little beyond its item, and reshaping the dispatch
loop would change cancellation and scope-reservation ordering, which
deserves its own review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:16:20 -04:00
Bryan Helmkamp
a369ea7fc4
Merge remote-tracking branch 'origin/main' into feat/for-each-item-injection
# Conflicts:
#	apps/fabro-web/app/components/stage-renderers/parallel-children.tsx
2026-07-28 20:03:25 -04:00
Bryan Helmkamp
17b3223558
Neutralize runtime for_each labels before display
Addresses three Copilot review comments on #653.

`item_label` comes from a model or a workflow author, and it reaches the
terminal through the CLI progress display. A label could carry ANSI
escapes, newlines, or bidi overrides and rewrite what the operator sees.
It could also be whitespace-only, giving a branch a blank identity.

Add `text::sanitize_display_label`: strip ANSI sequences, drop control
and bidi-reordering characters, trim, and elide past 80 characters.
Return an empty string when nothing printable survives so callers fall
back to an identity they control.

Apply it where the label is created, so events, the store, and the web
UI all get a clean value instead of each consumer having to remember.
`parallel_branch_display` sanitizes again, because a run recorded before
this commit still has raw labels in its event log.

`emit_branch_retrying` now sets `stage.retrying`'s `index` from the
branch stage's execution ordinal, matching the envelope `stage_id` and
the meaning every other emitter gives that field. The branch's position
in the fan-out is already on `parallel.branch.started`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:52:59 -04:00
Bryan Helmkamp
4efaf5c3d7
Let --dry-run walk a for_each fan-out
build_branch_plan read the runtime array before run_branches looked at
`simulated`, so every for_each workflow failed under --dry-run with
"for_each source '...' was not found in workflow context". Nothing had
populated the key yet: upstream LLM nodes take Handler::simulate, which
returns no context updates.

A dry run now stands in one placeholder item when the source is absent or
unusable, and simulates the template target once. Graph-shape mistakes
still fail, since catching those is the point of a dry run.

Also from review:
- ITEM_FENCE_PREFIX replaces the bare "untrusted-" literal that
  render_item_data and its test each spelled out.
- ItemRecordingHandler no longer guesses an item label by substring
  search. Nothing asserted it, and the third item's label "2" matched
  stray hex from the random fence tag about two thirds of the time.
- The twin test reads keys::PARALLEL_RESULTS instead of the raw string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:49:41 -04:00
Bryan Helmkamp
ee9189ddb5
Reuse existing helpers in the for_each fan-out
Simplification pass over the for_each branch. No behavior change.

Share what was duplicated:
- Node::prompt_or_label replaces the "prompt, else label" fallback that
  agent, prompt, and the for_each item injector each wrote out.
- context::lookup_flat replaces the "exact key, then strip context."
  lookup that condition.rs had twice and the for_each source had again.
- is_llm_handler_type replaces the inline agent/prompt match, so the
  runtime and the for_each_contract rule agree by construction.
- find_join_node now takes branch ids, so a for_each fan-out passes its
  template target instead of needing find_join_for_target.
- collect_events moves to test_support; parallel and integration tests
  shared one copy already.
- One ScriptedHandler replaces four test handlers that differed only in
  what they returned.

Straighten the branch retry loop:
- Reserve the branch scope once before the loop instead of guarding it
  with an Option, which removes three expect() calls.
- acquire_branch_permit and backoff_or_cancel replace the cancel-aware
  select! blocks the loop repeated verbatim.
- Keep the match arms in Executor::execute_with_retry order so the two
  loops stay easy to compare.

Drop redundant state:
- BranchPlan::is_for_each derives from template_target_id.
- for_each_contract checks node type before source shape, so one
  mistake reports one diagnostic.

Prove the new wire fields survive the OpenAPI boundary: the fabro-api
round-trip fixture now carries index and item_label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:42:23 -04:00
Bryan Helmkamp
f4c09867e5
Merge origin/main into refactor/remove-env-interpolation 2026-07-28 18:31:16 -04:00
Bryan Helmkamp
fa85bc42a2
Merge pull request #643 from fabro-sh/feat/claude-5-profile
feat(agent): add Claude 5 profile
2026-07-28 18:25:21 -04:00
Bryan Helmkamp
68b90ccc61
fix(agent): honor the TaskOutput schema defaults
The schema advertises defaults for `block` and `timeout`, but the
executor errored when either was absent. Apply the advertised defaults
instead, and keep the type check for values that are present.

The required list stays as the Claude 5 contract declares it. Constants
now hold the defaults and the maximum so the schema and the executor
cannot drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:21:01 -04:00
Bryan Helmkamp
b611d946a6
Merge pull request #673 from fabro-sh/feat/modal-llm-provider
Add Modal as an LLM provider
2026-07-28 18:14:43 -04:00
Bryan Helmkamp
977628000b
Merge pull request #668 from fabro-sh/feat/provider-qualified-fallbacks
Support provider-qualified model fallback selectors
2026-07-28 18:13:43 -04:00
Bryan Helmkamp
6c516ec31d
Merge pull request #660 from fabro-sh/feat/parallel-branch-live-status
Show live status for parallel branches
2026-07-28 18:13:21 -04:00
Bryan Helmkamp
8e4251bb07
refactor(llm): simplify the Modal provider tests and fixtures
Reduce duplication and over-specification introduced with the Modal
provider, without changing shipped behavior.

- Extract enabled_provider_catalog and assert_deep_tool_round_trip in
  the fabro-llm integration tests. The Poolside, Fireworks, OpenRouter,
  and Modal deep round trips were four near-identical copies.
- Add ApiCredential::with_extra_headers for providers that authenticate
  with request headers instead of an API key.
- Replace the unreachable require_env guards in the Modal e2e test with
  the std::env::var form used by every sibling test, and register
  MODAL_TOKEN_ID and MODAL_TOKEN_SECRET in EnvVars.
- Collapse modal_requires_both_vault_proxy_tokens to a single case. The
  loop rebuilt the whole built-in catalog per iteration.
- Drop tautological and over-specified assertions: the api_key_url doc
  URL, the forced default/probe lookups on a single-model provider, and
  the get_on_provider loop that could not fail.
- Inline the single-use modal_env_catalog fixture and note why it
  overrides the shipped secrets templates.
- Sort the MODAL_* keys in .env.example, and record in modal.toml why
  api_id keeps the Hugging Face capitalization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:06:07 -04:00
Bryan Helmkamp
f91e51162f
Describe the parallel branch fields in generated clients
`parallel_group_id` used `oneOf: [$ref StageId, null]`, and the generator
drops a sibling description in that position, so the TypeScript client
documented the field as "Canonical stage execution identifier in
`node_id@visit` form" — the shared StageId text, which says nothing about
what this field means. Switching to `allOf` lets the field's own
description through.

Dropping `type: "null"` also makes the contract match the server, which
omits both fields rather than sending null (`skip_serializing_if` on
`Option`, pinned by list_run_stages_exposes_parallel_branch_identity).
The Rust types are unchanged — still `Option<StageId>` and `Option<u32>`,
which accept an explicit null on input either way — so this only narrows
what clients are told to expect on the wire. Wording updated to match,
and reworded to avoid an apostrophe the generator escapes into the
JSDoc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:05:24 -04:00
Bryan Helmkamp
f8325c5c48
Let the first separator decide the model reference form
Addresses Copilot review feedback on #668.

Checking for `:` before the legacy `/` form broke `provider/model`
references whose selector contains a colon, which Bedrock API IDs do:
`bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0` parsed as the
whole path up to the last colon, then `0`. On main it is a pin to
bedrock with the full ID as the selector.

Now whichever separator appears first decides. A `/` before any `:` is
the legacy pin and its selector may contain colons. Otherwise the token
stays bare and `qualify` promotes it only when the prefix names a
provider, so `openrouter:moonshotai/kimi-k3` still qualifies.

Adds the regression test Copilot asked for, covering both Bedrock-style
legacy input and the colon-before-slash case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:01:05 -04:00
Bryan Helmkamp
00228383dd
Merge remote-tracking branch 'origin/main' into refactor/remove-env-interpolation
# Conflicts:
#	lib/foundation/fabro-types/src/settings/interp.rs
2026-07-28 17:54:35 -04:00
Bryan Helmkamp
503e21fbc1
Merge origin/main into feat/parallel-branch-live-status
Conflicts were between this branch's parallel-branch identity work and
main's stage billing, review targets, and live stage timing.

- Stage fixtures: main added `billing` to each per-file `makeStage`; this
  branch had hoisted one builder into `lib/test-utils`. Kept the hoisted
  builder and gave it `billing: makeBilledTokenCounts()`, so both intents
  hold and the field list stays in one place. `stage-sidebar.test.ts` also
  builds raw `RunStage` wire payloads, so it keeps importing
  `makeBilledTokenCounts` directly.
- Import lists (`run_projection.rs`, `fabro-api/src/lib.rs`,
  `run_state.rs`, `stage_projection_round_trip.rs`): unioned both sides —
  `ParallelBranchId` alongside `timing`, `ReviewTarget`,
  `ReviewTargetKind`, `AttrValue`, `Node`, and
  `StageToolBatchProjection`.
- `fabro-server` tests: git interleaved two unrelated new tests into one
  body. Split them back into
  `list_run_stages_exposes_parallel_branch_identity` and
  `run_billing_includes_live_stage_timing_in_rows_and_totals`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:50:36 -04:00
Bryan Helmkamp
9d828a8688
Merge remote-tracking branch 'origin/main' into refactor/remove-env-interpolation
# Conflicts:
#	lib/components/fabro-workflow/src/pipeline/pull_request.rs
2026-07-28 17:44:03 -04:00
Bryan Helmkamp
239490a553
Merge pull request #664 from fabro-sh/feat/script-value-interpolation
Interpolate goal, inputs, and vars in command node scripts
2026-07-28 17:41:10 -04:00
Bryan Helmkamp
b19dda6aa3
Merge pull request #661 from fabro-sh/fix/small-default-skips-providers-without-small-model
Fix run title generation picking a flagship model, and sharpen the prompt
2026-07-28 17:38:27 -04:00
Bryan Helmkamp
6226c8c517
fix: address env interpolation review findings
Restore the documented SDK env credential facade without reintroducing run fallback behavior. Fail closed on GitHub permission resolution, require worker storage at the CLI boundary, and align interpolation names and generated docs.
2026-07-28 17:30:50 -04:00
Release Repro
81d762aa9d
Merge remote-tracking branch 'origin/main' into feat/script-value-interpolation
# Conflicts:
#	lib/components/fabro-workflow/src/pipeline/transform.rs
2026-07-28 17:27:24 -04:00
Bryan Helmkamp
690ddd2a96
Fix live parallel branch refresh and simplify branch rendering
Branches bypass the engine's stage.started/stage.completed lifecycle, so
no SWR key invalidated the stages list while a fork ran. The new live
branch rows stayed frozen at their first observed state until an
incidental refetch. Map parallel.* events to the stages list, run events,
and graph keys.

Also:
- Label branch rows with formatStageLabel so a re-entered branch renders
  as `review_glm@2`, matching the sidebar and waterfall.
- Build branch rows in one pass and count live outcomes in one loop.
- Name ParallelBranchId in the OpenAPI spec and reuse fabro_types::
  ParallelBranchId, replacing two copies of an inline string format.
- Hoist makeStage and textContent into lib/test-utils so widening Stage
  cannot leave per-file fixtures stale (tests are excluded from
  typecheck, so the two component-test copies had already gone stale).
- Query stat tiles by data-stat instead of an exact Tailwind class.
- Reuse append_scoped_stage_event's body via append_event_with_scope and
  add test_branch_event instead of poking envelope fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:26:05 -04:00
Bryan Helmkamp
8f9b36c0b8
fix(test): propagate storage setup errors 2026-07-28 17:23:31 -04:00
Release Repro
b04684aec4
fix(workflow): harden script value interpolation 2026-07-28 17:21:35 -04:00
Bryan Helmkamp
e91343bbeb
refactor: address run title review findings 2026-07-28 17:14:03 -04:00
Bryan Helmkamp
df0bd58819
Merge remote-tracking branch 'origin/main' into fix/small-default-skips-providers-without-small-model
# Conflicts:
#	lib/apps/fabro-server/src/test_support.rs
2026-07-28 17:06:20 -04:00
Bryan Helmkamp
1aa7a153b0
Merge pull request #652 from fabro-sh/fix/publish-failures
Make publish failures terminal
2026-07-28 17:03:18 -04:00
Bryan Helmkamp
2103e3fbde
Qualify colon model references against the provider registry
Splitting on the first colon in FromStr broke bare model IDs that
legitimately contain one. A reference like "llama3:8b" parsed as
provider "llama3" selector "8b", and since "llama3" is not a provider
the lookup failed instead of passing the ID through to the pinned
provider. Verified against origin/main: canonical_session_model with
"future-model:latest" pinned to openrouter returned the passthrough
before and a 400 after.

This is not fixable by choosing a different separator. Bedrock
inference-profile ARNs contain both colons and slashes, and
docs/public/integrations/bedrock.mdx tells users to put arbitrary
inference-profile IDs in api_id. Only the registry can tell a provider
prefix from a model ID that happens to contain the separator.

FromStr now leaves colon-bearing tokens bare, and ModelRef::qualify
promotes only those whose prefix names a known provider. resolve()
applies it, so the fallback path is covered; sessions.rs applies it
before its own match so it keeps its tailored ambiguity messages.

ModelRegistry is now implemented for Catalog in fabro-types, replacing
the CatalogModelRegistry wrapper that existed only in start.rs, so both
call sites share one registry view.

Covered by regression tests at both surfaces, plus qualify unit tests
for ollama tags and Bedrock ARNs. The pre-existing passthrough test
canonical_session_model_preserves_unknown_passthrough_on_selected_provider
passes again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:02:16 -04:00
Bryan Helmkamp
1af6a3d8be
fix: stop inferring a final commit SHA, route slashed branches in the GitHub twin
Both from Copilot review feedback on #652.

Do not fall back to `base_sha` for `final_git_commit_sha`:
`base_sha` is where the run started, not what it produced. When a run made
commits but no SHA was tracked, the conclusion reported the base commit as the
run's final commit — a durable, API-exposed field — and publish then checked
the pushed branch against it, failing a branch that was pushed correctly.

The SHA is now only required where it is actually used: verifying the remote
head before opening a pull request. Pushing never needed it, since the refspec
sends whatever the branch points at. A run with no tracked SHA therefore still
pushes its branch and succeeds; it fails only if a pull request is requested,
where an unverifiable head is a real problem.

Route branch names with slashes in the GitHub twin:
Run branches are `fabro/run/<id>`. GitHub routes the branch as the remainder
of the path, but the twin declared a single-segment `{branch}` capture, so
every real run branch 404'd against it. Now a wildcard, with a test covering
the slashed case that the existing single-segment tests missed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:55:52 -04:00
Bryan Helmkamp
eb2d612906
Merge pull request #671 from fabro-sh/feat/human-review-target
Add structured review targets to human gates
2026-07-28 16:54:59 -04:00
Bryan Helmkamp
e6cf174578
Merge origin/main into feat/human-review-target
Conflict in apps/fabro-web/app/components/interview-dock.tsx. Main moved
the dock onto the shared collapsible `RunDockShell` and replaced the
local button constants with shared ones.

Kept main's structure whole and re-applied the review target rendering
onto it: the question paragraph in the shell's `body` becomes the linked
`ReviewTargetQuestion` when the target passes `safeReviewTarget`, and
plain text otherwise. Both now share main's paragraph classes through
`QUESTION_TEXT`, so the two renderings stay visually identical.

`peek` keeps using `question.text`, which is the correct plain-text
collapsed summary for a review target question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:48:02 -04:00
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
8771c971d7
Add Modal LLM provider 2026-07-28 16:02:57 -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
b2942519d5
Merge the duplicate selector catalog tests
catalog_from_settings_rejects_duplicate_provider_api_ids was a copy of
catalog_from_settings_rejects_duplicate_model_aliases with the alias
declaration swapped for an api_id. Fold them into one table-driven test
so the shared invariant is stated once: canonical IDs, aliases, and API
IDs occupy a single identifier namespace per provider.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:48:22 -04:00
Bryan Helmkamp
8a41d4665f
Simplify model reference parsing and catalog indexing
Follow-up cleanup on the provider-qualified selector work.

- build_model_indexes now takes the paired (Model, CatalogModelSettings)
  slice it is built from, instead of a separate settings map. This drops
  a per-model map lookup with two cloned key components and removes the
  expect() panic path for an invariant the caller already guarantees.
- get_on_provider expresses the exact-then-legacy lookup as one closure
  applied twice, rather than a nested then/flatten chain.
- ModelRef::from_str selects the separator first and then checks both
  sides once, so the empty-side check is no longer duplicated across two
  branches and the slash split no longer allocates a Vec.
- Shorten the TooManySlashes message to the action the user should take.
- Merge the two near-identical fallback chain tests into one that runs
  both qualified selector forms through the same assertion.
- The fallbacks splice test now asserts through the existing Serialize
  impl instead of hand-rolling the ModelRefOrSplice rendering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:42:03 -04:00
Release Repro
e21e6bcdf9
refactor(sandbox): harden activation recovery 2026-07-28 15:38:35 -04:00
Bryan Helmkamp
fa6f7e5558
fix(workflow): verify remote head before generating PR content, tolerate replica lag
Three follow-ups from the efficiency review of the publish pipeline.

Check the branch before spending an LLM call:
`open_pull_request` generated the PR title and body first and only then
verified the remote branch pointed at the run's final commit. Every stale
branch therefore cost a full content generation before failing. The
verification is the cheap check, so it now runs first.

Tolerate GitHub read-after-write lag:
`GET /repos/{owner}/{repo}/branches/{branch}` is replica-served and can briefly
report the previous commit, or 404 for a branch that is new on the remote,
right after the push publish just made. It was read once with no retry. Since
publish failures are terminal, a replica that had not caught up yet would
discard a fully successful run. It is now read up to three times.

These two land together on purpose: the LLM call was the only thing buying
slack against the race, so reordering without the retry would have made it
more likely.

Keep commit SHAs out of failure classification:
`classify_failure_reason` substring-matches bare "500", "502", "503" and "504"
as transient-infra hints. Both publish messages embed a commit SHA, and a
40-char hex string contains one of those often enough to matter, so a
deterministic failure could be reported as transient. Long hex runs are now
masked before matching; the three-digit status codes those hints look for are
too short to be affected. The hex regex is shared with
`normalize_failure_reason`, which already had its own copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:34:18 -04:00
Bryan Helmkamp
c1335421e4
fix: stop a pipe in a link label from splitting Slack link markup
`slack_link` builds `<url|label>`, and `escape_slack_controls` covers
Slack's documented escapes (`&`, `<`, `>`) but not `|`. Slack has no
escape for `|`, so a label containing one splits the markup and can make
Slack reject the block.

`is_safe_slack_link_url` already guards the URL half against `|`; the
label half was unguarded. It did not matter before because the only
labels were "Open in Fabro" and a PR number. Review target labels are
model-authored, so this is now reachable.

Replace `|` inside link labels, which keeps the link working. Plain-text
labels are untouched, since `|` is fine outside link markup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:31:20 -04:00
Bryan Helmkamp
8e066ecf7b
refactor: remove duplicated review target rendering and validation
The review question sentence was written in four places and the URL
safety rules in three. Collapse each to one definition.

- Add `ReviewTarget::question_text_with_link` as the single definition of
  the question wording. `question_text()` and the Slack header both use
  it, so a wording change is now one edit.
- Delete `ReviewTargetKind::noun()`. The enum already derives
  `strum::Display` with the same snake_case output.
- Share one `review_target_line` helper between the console interviewer
  and the CLI attach client, which held a byte-identical copy. Print only
  the URL: `question.text` already carries the label and the noun.
- Trim the web-side check to the URL scheme, host, and credentials, which
  are what a raw `href` can act on. Label length and control characters
  cannot affect the DOM and stay server-side.
- Split validation from presentation in the web UI. `safeReviewTarget`
  returns the target or null, and each caller picks its own fallback, so
  an unsafe target now falls back to the same Markdown rendering as a
  question with no target.
- Derive the resource noun from `kind` in the web UI instead of
  hardcoding "document".
- Use `ReviewTargetKind.DOCUMENT` and the shared `isRecord` guard when
  parsing events, instead of a raw string and a hand-rolled object check
  that accepted arrays.
- Drop `deny_unknown_fields` from the wire struct. The OpenAPI schema
  leaves `additionalProperties` permissive, so an added field would
  otherwise make persisted events unreadable.
- Import `ReviewTarget` by name, and stop naming Slack in a fabro-types
  error message.
- Document that `review_target=true` replaces the gate's `label`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:14:23 -04:00
Bryan Helmkamp
73f48eeddb
refactor: simplify publish pipeline and collapse duplicated stage errors
Follow-up cleanup on the publish-failures change.

Error model:
- Collapse `Error::{Engine, Publish, Handler}` into one `Error::Stage` with an
  `ErrorStage` discriminator. The three shared a field shape and had to be
  edited together in four match groups; nine near-identical constructors
  become two private helpers.
- Add `Error::failure_reason()`, replacing the same error -> FailureReason
  mapping written out in four places.
- Publish errors are now terminal. Publish runs once, after execution, so no
  caller could ever act on the retryable classification.

Publish phase:
- Fix: a branch that was pushed is now still reported when pull request
  creation fails afterwards. `PublishOutcome` records what happened and
  carries the error separately, instead of hiding both behind a `Result`.
- Drop `PublishOutcome::NoChanges`, which no consumer distinguished from
  `Published { pr_url: None }`.
- Move publish onto `Concluded` as methods and replace three near-identical
  precondition guards with one `publish_target()`.

Pull requests:
- `maybe_open_pull_request` -> `open_pull_request` returning the record
  directly. Both callers already reject empty diffs, so the `Ok(None)` path
  was unreachable.
- Drop `CreatedPullRequest.head_sha`, which echoed back its own input.

GitHub client:
- Delete `branch_exists`, which had no callers and duplicated
  `branch_head_sha`. Give `branch_head_sha` the `_with_client` split every
  sibling has and port the tests to `MockHttpClient`.
- Collapse the copy-pasted credential match in `resolve_clone_credentials`.

Events:
- `PullRequestCreated.head_sha` is `Option<String>` instead of using an empty
  string to mean absent.
- Centralize the run-branch refspec in `lifecycle::git::push_run_branch`, so
  `git.push` reports a branch name from both emitters as documented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:09:10 -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
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