Commit graph

66 commits

Author SHA1 Message Date
Scott Werner
0001cfba02
Merge pull request #789 from fabro-sh/codex/run-intent-none-target
Add empty workspace run target
2026-08-25 11:25:41 -04:00
Scott Werner
3e0adde73d Simplify the empty workspace run target plumbing
- Use a derived deserializer for RunTarget by making `None` an empty struct
  variant, which keeps `deny_unknown_fields` strict without a hand-rolled impl
- Make clone_source_for_run the single owner of the empty-workspace decision
  and drop the duplicated target checks in RunSession::new
- Collapse duplicated target/provider compatibility matches in admission and
  start into single matches, using a strum-derived kind name for messages
- Drop the redundant git override in persist_create_run
- Extract a shared helper for the duplicated unavailable-integration test loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:21:54 -04:00
Scott Werner
2f3b6477f2 Add empty workspace run target 2026-08-24 14:49:03 -04:00
Scott Werner
d65785d888 Simplify blob activation and share the test store fixture
Blob activation cleanups:
- Reuse fabro-db's append_to_path, remove_file_if_exists, and
  set_private_permissions instead of local duplicates.
- Return the store directly from activate_blob_storage; the report
  wrapper existed only to be logged internally and then discarded.
- Collapse compute_disk_preflight to return the required free bytes
  instead of echoing its inputs back through a struct.
- Deduplicate the "exactly one ok row" PRAGMA integrity_check protocol
  into one executor-generic helper used by the backup and live checks.
- Skip re-validating a freshly published backup; the staging copy was
  validated immediately before the atomic rename, so only a
  concurrently published file needs its own validation.
- Replace the manual anyhow wrapping plus duplicate error log in
  serve.rs with a plain .context(), matching other startup errors.
- Extract the disk-candidate enumeration in resource_sampler.rs that
  available_space_for_path had copy-pasted from sample_disk_resources.

Test fixture cleanups:
- Route all hand-assembled Database::new(..., test_blob_store()) test
  fixtures (32 sites) through fabro_store::test_support::test_database,
  and make that helper infallible instead of returning an unconditional
  Ok.
- Install the test blob schema from fabro_db::BLOBS_MIGRATION_SQL via a
  test-support-gated optional dependency instead of a four-level
  relative include_str! into fabro-db's migrations directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 14:02:35 -04:00
Scott Werner
2814c1fd45 Activate verified SQLite blob storage 2026-08-24 14:02:34 -04:00
Scott Werner
26b2c091ca Derive clone sources from the persisted run target
Start reconciled the persisted target against its stored GitContext
projection field by field and failed the run on any drift, which forced
every RunSpec writer to keep the pair in lockstep forever. The target is
validated at admission and owns the grammar, so derive the clone source
from it alone; the projection stays persisted as display metadata that
can no longer fail an otherwise-healthy start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 13:02:40 -04:00
Scott Werner
18d98794ae Move Git-target validation onto RunTarget in fabro-types
The Git-target grammar (slug, branch, and SHA rules plus the derived
origin URL) was implemented twice with no shared code path: once in
server admission and again in sandbox start, so the two could drift and
disagree about which persisted targets are valid.

Own it once as RunTarget::validate() in fabro-types, next to the
primitives it uses, returning the canonical target together with its
derived GitContext projection. Admission consumes it directly, and the
start path re-derives the expected clone source from the same rules
before checking the persisted projection against it. The start path now
also moves the derived strings into the sandbox spec instead of cloning
them.

While reordering admission around the shared validator, run the pure,
in-memory checks (target grammar, environment id) before the blob-store
closure fetch and lowering so malformed requests no longer pay for
version-store I/O.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 11:55:44 -04:00
Scott Werner
040bc6c043 Add version-backed run intent creation 2026-08-24 11:55:43 -04:00
Bryan Helmkamp
d80dde2320
Merge remote-tracking branch 'origin/main' into codex/fail-closed-run-event-persistence
# Conflicts:
#	lib/components/fabro-workflow/src/operations/start.rs
2026-08-21 19:49:44 -04:00
Bryan Helmkamp
61394ba2f1
Simplify run-event persistence failure plumbing
- Make the failure watch channel the single record of the latched
  failure; drop the worker task's mirrored local state.
- Replace the hand-rolled wait loop with watch::Receiver::wait_for.
- Extract race_persistence/flush_or_stop helpers so the select!/flush
  scaffolding in RunSession::run exists once instead of three times.
- Return RunEventPersistenceError from append_event_to_sink and add a
  From impl on Error, replacing four hand-written per-event message
  strings with the event name derived from the event itself.
- Dedupe the RunCreated test seed literal in initialize.rs and drop the
  dead BlockingHandler::simulate override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ryyhtbc1eNtCLw8GjrFQXZ
2026-08-21 19:29:14 -04:00
Bryan Helmkamp
45e06d2a6e
Merge pull request #775 from fabro-sh/claude/additional-github-repositories
Additional GitHub repository access
2026-08-21 18:55:31 -04:00
Bryan Helmkamp
09f5bb0f84
Simplify clone depth plumbing
Make RunCloneSettings::DEFAULT_DEPTH the single owner of the default
depth, and interpret the "0 = full history" sentinel in one place via
RunCloneSettings::depth_limit(). Docker's clone_depth becomes
Option<usize> to match Daytona's encoding, with a shared
depth_argument() helper for both git command builders. Drop the
unreachable Option on the resolved depth field, the hand-written
DaytonaSettings::Default, and the pure-forwarding
daytona_git_clone_options helper. The blob-import test helper reuses
the pool's own connect options instead of rebuilding a partial copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bgXj5J218RXfiT72qhbLV
2026-08-21 18:37:39 -04:00
Bryan Helmkamp
129fa0ea0c
fix: fail runs when event persistence is lost 2026-08-21 18:31:22 -04:00
Bryan Helmkamp
6179470eb2
feat: default sandbox clone depth to 100 2026-08-21 17:25:58 -04:00
Bryan Helmkamp
438bab29f0
feat: support shallow sandbox clones 2026-08-21 17:11:24 -04:00
Bryan Helmkamp
d8edd410f3
feat(workflow): bridge git and gh to the shared token
Carry the resolved GitHub integration (permissions plus declared
additional repositories) as one value from run materialization into
workflow startup, and make the sandbox environment reach every declared
repository through the single managed GITHUB_TOKEN.

- `StartServices.github_permissions` becomes
  `github_integration: ResolvedGithubIntegration`; CLI and server
  workers build it with `resolve_integration()` after interpolation and
  pass it through `SandboxEnvSpec` as one unit.
- `build_sandbox_env` constructs the validated
  `GitHubRepositoryAccess` and scopes the App token source to the whole
  effective set. Missing credentials or a missing origin are hard
  initialization errors when additional repositories are declared;
  legacy permissions-only configuration keeps its best-effort behavior.
- When additional repositories are declared, initialization eagerly
  resolves each repository's App installation (naming any repository
  the App cannot see) and the token itself, so an inaccessible declared
  repository fails before the first workflow stage.
- A new `git_bridge` module injects secret-free `GIT_CONFIG_*` entries
  into the stage environment: a github.com credential helper that reads
  `$GITHUB_TOKEN` at invocation time, per-repository SSH-to-HTTPS
  `insteadOf` rewrites, and `GIT_TERMINAL_PROMPT=0`. Entries append
  after a valid user-provided Git config overlay and fail clearly on a
  malformed one. Contract tests drive the installed git binary against
  local fixtures for the rewrite, credential, prefix-collision, and
  overlay-preservation behaviors.
- The long-running ACP notice now says all declared repository access
  expires together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 14:47:31 -04:00
Scott Werner
9d3aa7a4d4 Consolidate workflow-version lineage test coverage
The lineage field's `skip_serializing_if` behavior was asserted five times
across three crates. Keep the two assertions in fabro-types, which owns the
attribute, and drop the duplicates:

- Delete `run_created_omits_absent_workflow_version_id` from event/convert.rs,
  a copy of the test above it that re-checked another crate's serde attribute.
  convert.rs's own responsibility is covered by the existing field assertion.
- Delete `legacy_create_input_persists_without_workflow_version_id`, which ran
  the full create() pipeline to prove a hardcoded `None` literal is `None`.
  `CreateRunInput` has no such field, so no input could change the result.
- Fold `run_spec_omits_absent_workflow_version_id` into the adjacent legacy-spec
  test, which already holds an all-`None` record.
- Drop the off-topic spec re-serialization from run_state.rs's retried_from test.

Add `test_support::test_workflow_version_id()` alongside `test_run_provenance()`
and use it everywhere, replacing eight copies of the same magic seed across five
crates plus two assertion sites that recomputed the hash inline. This also
subsumes retry.rs's private helper of the same shape.

Revert the `run_spec_json` parameterization in the projection round-trip test:
`RunProjection` is a `with_replacement` alias for the canonical type, so the
`Some` and `None` call sites exercise identical code.

Have the two run.created literals that mirror a `RunSpec` read the spec's
lineage field instead of hardcoding `None`, so the mirrors stay accurate once a
producer populates it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 12:39:02 -04:00
Scott Werner
27fd48c603 Persist workflow version lineage on runs 2026-08-21 12:39:02 -04:00
Scott Werner
75fa8eca8b Merge remote-tracking branch 'origin/main' into codex/exact-target-checkout
# Conflicts:
#	lib/components/fabro-sandbox/src/clone_retry.rs
#	lib/components/fabro-sandbox/src/daytona/mod.rs
#	lib/components/fabro-sandbox/src/docker.rs
#	lib/components/fabro-sandbox/src/provider/docker.rs
2026-08-21 12:15:59 -04:00
Bryan Helmkamp
ca6d9a46da
Merge remote-tracking branch 'origin/main' into fix/redaction-corrupts-executable-spec
# Conflicts:
#	lib/components/fabro-dump/src/lib.rs
#	lib/components/fabro-store/src/run_state.rs
#	lib/components/fabro-store/tests/serializable_projection.rs
#	lib/components/fabro-workflow/src/billing_rollup.rs
#	lib/components/fabro-workflow/src/run_lookup.rs
#	lib/components/fabro-workflow/src/runtime_store.rs
#	lib/foundation/fabro-api/tests/run_projection_round_trip.rs
#	lib/foundation/fabro-test/src/lib.rs
#	lib/foundation/fabro-types/src/run.rs
#	lib/foundation/fabro-types/src/run_event/run.rs
#	lib/foundation/fabro-types/src/run_projection.rs
#	lib/foundation/fabro-types/tests/run_spec_methods.rs
2026-08-20 20:42:20 -04:00
Bryan Helmkamp
2b095612c8
Address run spec persistence review findings 2026-08-20 20:35:52 -04:00
Scott Werner
2575ab85fc
Merge pull request #747 from fabro-sh/codex/blob-hash-vocabulary
Some checks failed
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Unify blob hash vocabulary
2026-08-18 15:55:43 -04:00
Scott Werner
14cc56b25f Remove stale env-interpolation promises from docs
Config {{ env.NAME }} interpolation was removed workspace-wide (tokens
still parse only to fail with a migration message), but several doc
comments and the server-secrets strategy doc still presented it as a
live mechanism, including run goal file paths where the new
workflow-version validation now makes the contradiction user-visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:36:53 -04:00
Scott Werner
bf4265e1b8 Unify blob hash vocabulary 2026-08-17 13:56:19 -04:00
Scott Werner
8c45b870b4 Check out exact sandbox commits 2026-08-14 17:10:46 -04:00
Scott Werner
62ed7cb8a2 Rename RunBlobId to BlobHash 2026-08-12 11:33:41 -04:00
Bryan Helmkamp
3421c4f06f
Keep the executable run spec out of reach of event redaction
Two root-cause fixes for the sandbox failure where an inline Dockerfile
came back from the store as `ARG REDACTED` and the Daytona snapshot
build died on the unset variable.

Entropy redaction measures values, not assignment pairs. The detector
matched `NAME=value` as one token, so an uppercase name merged its
charset into a pure-hex value (which alone can never exceed 4.0 bits)
and pushed the pair over the 4.5-bit threshold — then replaced the
whole pair, destroying the name. `find_entropy_regions` now strips an
identifier-shaped `NAME=` prefix before measuring and redacts only the
value, matching the gitleaks layer's `key=REDACTED` shape.

Execution no longer reads redacted content. Every stored event passes
through the redaction sink, and `load_from_store` rehydrated the
worker's RunSpec from the projection folded from those events — so a
redactor false positive silently rewrote the spec the sandbox builds
from (and changed its snapshot identity). The creation path now writes
the exact spec bytes to the content-addressed blob store and records
`spec_blob` on run.created; `load_from_store` loads the spec from the
blob, keeping the event stream authoritative for run identity,
provenance, and event-recorded blob ids. Retry and fork carry the
source run's `spec_blob` forward, so derived runs stop inheriting the
redacted copy. Runs created before the blob existed fall back to the
folded spec.

The projection and every API surface keep serving the redacted fold;
blobs were already stored unredacted (the workflow bundle carries the
same bytes), so this adds no new exposure at rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 21:44:46 -04:00
Scott Werner
3d77d371c6 Remove the recorded pre-run push outcome, keep the push
The manifest builder's best-effort pre-run push converted every result
into a PreRunPushOutcome that was serialized into GitContext, expanded
into five OpenAPI union arms, and generated into API clients — but no
production path ever read it; every field read was a test.

Delete the concept while preserving the behavior:

- Drop the PreRunPushOutcome enum and GitContext.push_outcome from
  fabro-types; GitContext keeps origin_url, branch, optional sha, and
  dirty, which remain real execution inputs and provenance.
- Rename the manifest outcome builder to push_manifest_branch_best_effort,
  a side-effect-only helper with the same decision rules: skip without an
  origin, skip on configured-repository mismatch, skip when the branch is
  already synced, otherwise push noninteractively and discard the result
  without failing manifest creation or logging raw Git stderr.
- Prove the push through repository state instead of the deleted enum: a
  branch ahead of a local bare origin is pushed during manifest build, a
  mismatched configured repository is not, and a failing remote helper
  still cannot fail manifest creation.
- Remove push_outcome from GitContext in OpenAPI, delete the five-arm
  union schemas, and drop the fabro-api type replacement and re-export.
- Keep one regression proving historical run.created events with a nested
  push_outcome still deserialize through ordinary unknown-field tolerance
  and reserialize to the reduced shape. No migration or event rewrite.

Old JSON carrying the removed field stays readable. Newly generated
clients omit a field older servers required, so new-client-to-old-server
compatibility is intentionally not promised for this pre-1.0 contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 19:27:33 -04:00
Scott Werner
85dfcd36f9 Scope synchronous fixture I/O allowance to tests 2026-08-03 15:32:36 -04:00
Scott Werner
4194ff4a26 Update run creation event fixtures and docs 2026-08-03 15:09:32 -04:00
Scott Werner
cff0a2461a Trim unused run creation event fields 2026-08-03 15:09:25 -04:00
Scott Werner
5a4a25ccc7 Remove the run-creation reopen fallback 2026-08-03 12:34:37 -04:00
Fabro
76f61f163b fabro(01KYQN78K19NY7PNSCDYP6CG9G): simplify_fable (succeeded)
Fabro-Run: 01KYQN78K19NY7PNSCDYP6CG9G
Fabro-Completed: 6
Fabro-Checkpoint: 1254f4ca92f7b1f822cdfa7ca549ef8595a50fed

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-31 14:09:32 -04:00
Fabro
ac6e3ced6a fabro(01KYQN78K19NY7PNSCDYP6CG9G): implement (succeeded)
Fabro-Run: 01KYQN78K19NY7PNSCDYP6CG9G
Fabro-Completed: 5
Fabro-Checkpoint: 879008d9d5

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-31 14:09:32 -04:00
Bryan Helmkamp
e6eb36f852
refactor: simplify model-keyed fallback internals
Consolidation pass over the fallback feature, no intended behavior
changes beyond noted validation and event-shape cleanups:

- Unify the two parallel notice types: FallbackPlanNotice is gone;
  ModelFallbackNotice now owns the runtime NoNearbyReasoningLevel case
  and the shared ChainEmpty wording. Notices emit through a new
  Emitter::notice_scoped with their own level, and each distinct notice
  is emitted once per run instead of on every LLM call.
- Move canonical_model_id onto Catalog so chain keys are written and
  read through one function; reject provider-qualified fallback keys,
  which could never match at dispatch and were silently dead config.
- Type FallbackTarget as ProviderId/ModelId, removing repeated
  ProviderId::new re-wrapping at every use site.
- Derive FallbackPlan's current route from a position index instead of
  storing current/requested_controls copies; advance() no longer has
  unreachable None branches.
- Bundle the agent invocation's live state (session, bridge, lease,
  forwarder, accounting) into LiveAgentInvocation; failover_agent_session
  drops from 21 parameters to 7 and the six copies of the
  abort/discard/classify teardown collapse into two methods.
- Share one route_request builder between one_shot and its failover
  loop; complete_one_shot_request takes the request by value instead of
  deep-cloning the message payload per call.
- Event::Failover carries FailoverProps directly; the props' original
  route and attempt fields are now required, and reasoning efforts are
  typed ReasoningEffort instead of strings.
- Reuse RunModelSettings/RunModelControls in fabro-api via
  with_replacement, add the missing controls property to the OpenAPI
  schema, regenerate the TS client, and add the type-identity/JSON
  parity test.
- Smaller cleanups: ReasoningEffort::closest_supported uses enum
  discriminants; ModelFallbackPolicy gains len(); resolve_model_fallbacks
  takes a provider slice; duplicate-target filtering lives only in the
  resolver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:51:36 -04:00
Bryan Helmkamp
ba82656656
feat: add model-keyed fallback policies 2026-07-30 12:02:41 -04:00
Bryan Helmkamp
8a597ca264
fix(workflow): report the real cause when a fallback cannot resolve
Addresses review feedback on the fallback notice work.

A provider-only fallback such as `openrouter` needs the primary model's
catalog entry to find the closest capability match. When the primary is
itself a passthrough selector there is no entry, so every provider-only
candidate was skipped with "provider `X` has no compatible model" even
when that provider had plenty. Adds a `PrimaryNotInCatalog` notice that
names the missing primary instead of blaming the provider.

Also from review:

- `code()` was a wildcard fallthrough, which docs/internal/events-strategy.md
  forbids for new variants. Now exhaustive.
- `NoConfiguredOffering` discarded the `providers` list that
  `NoEligibleOffering` hands it. The notice now names the providers that do
  offer the model.
- `ModelFallbackNotice::reference` was a rendered `String`; it is now the
  `ModelRef` it came from, which also drops the per-candidate double
  allocation the previous refactor introduced.
- `ResolvedStartLlm` unpacked and repacked `ResolvedFallbackChain`
  field-for-field; it now holds it directly.
- Added `FallbackTarget: Display` as `provider:model`, replacing two
  hand-written `"{}:{}"` format strings.
- `Catalog::select` still inlined the `require_provider` body.
- Emission moved to `ModelFallbackNotice::emit_all`, covered by a new test
  proving notices reach the event stream with the right level, code, and
  message. Nothing tested that hand-off before.

Documented in `resolve_fallback_chain` why an unknown provider stays a hard
error while an unconfigured one is skipped, and that an unqualified unknown
selector pins to the primary's provider.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:37:51 -04:00
Bryan Helmkamp
48fd09aaab
refactor(workflow): simplify fallback chain resolution
Follow-up cleanup on the portable fallback chain work.

- Add `Catalog::require_provider` and `Catalog::provider_id`, replacing the
  `catalog_provider_id` free function in `start.rs` and two copies of the same
  `provider(..).ok_or_else(UnknownProvider)` block inside the catalog.
- Add `FallbackTarget::new` and use it for the six struct literals that each
  stringified a provider and model by hand.
- Extract per-candidate resolution into `resolve_fallback_candidate`, returning
  a `FallbackCandidate` that is either a target or the skip reason. This flattens
  `resolve_fallback_chain` from four levels of nesting to one loop and splits the
  qualified/unqualified model arms into separate match patterns.
- Drop the `seen` HashSet and its per-candidate key clones in favor of a
  `contains` check on the chain being built; fallback chains hold a handful of
  entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:23:25 -04:00
Bryan Helmkamp
e0f73f963a
feat(workflow): make model fallback chains portable 2026-07-29 18:08:26 -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
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
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
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
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
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
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
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