Commit graph

5137 commits

Author SHA1 Message Date
Bryan Helmkamp
ba82656656
feat: add model-keyed fallback policies 2026-07-30 12:02:41 -04:00
fabro-releases[bot]
4ab090cae3 Bump version to 0.310.0-nightly.2 2026-07-30 11:14:09 +00:00
Bryan Helmkamp
e80be78c81
chore(workflows): use Claude Opus 2026-07-30 07:01:23 -04:00
Bryan Helmkamp
2cb796c636
ci: raise macOS file descriptor limit for release tests 2026-07-30 06:58:10 -04:00
fabro-releases[bot]
69d4482da5 Bump version to 0.310.0-nightly.1 2026-07-30 10:56:04 +00:00
Bryan Helmkamp
2efcc72124
Merge pull request #684 from fabro-sh/feat/portable-model-fallbacks
feat(workflow): make model fallback chains portable
2026-07-30 06:42:53 -04:00
fabro-releases[bot]
381f603bfb Bump version to 0.310.0-nightly.0 2026-07-30 10:05:24 +00: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
b5885b15dc
Merge pull request #686 from fabro-sh/fix/space-separated-node-classes
Fix space-separated node class parsing
2026-07-29 22:31:48 -04:00
Bryan Helmkamp
727cd52c3b
fix(types): trim class names in Node::add_class
The doc comment promised blank names were ignored, but the guard only
rejected the empty string. Stylesheet selectors match class names exactly,
so a padded name would sit in `classes` and match no rule.

No current caller can pass one: the parser splits on whitespace, and the
subgraph and import paths strip everything but alphanumerics and hyphens.
This makes the public contract on the shared type match what it claims.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:25:42 -04:00
Bryan Helmkamp
bff51900c0
Merge pull request #683 from fabro-sh/feat/model-stylesheet-comments
Support comments in model stylesheets
2026-07-29 22:23:27 -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
01c4a4a23b
test(graphviz): table-drive the class separator cases
One row per separator rule, so a regression names the input that broke
instead of pointing at a combined fixture string. Also record why
`add_class` keeps insertion order: `fidelity` falls back to the first class
for the thread ID.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:17:37 -04:00
Bryan Helmkamp
f932a0763b
refactor(graphviz): simplify stylesheet comment stripping
Replace the hand-rolled byte scanner in strip_css_comments with a
str::find loop over "/*" and "*/".

Drop the quote and backslash tracking. The stylesheet language has no
string literals: parse_declarations ends a value at the first ';' or
'}' with no quote awareness, and values flow into AttrValue::String
verbatim, so a quoted model name is just an unknown model. Tracking
quotes here also created a failure mode the simple scan does not have.
An unpaired apostrophe, as in `model: don't`, disabled comment
stripping for the rest of the input and then blamed a well-formed
comment for the parse error.

Also drop the Cow and its copied_through watermark. They avoided one
allocation on a graph attribute of a few hundred bytes, parsed once per
workflow load, in a function whose caller already clones the attribute
and whose parser allocates a String per property and per value.

Extract excerpt() for the error snippets. The two existing call sites
sliced raw bytes at index 20, which panics when a multi-byte character
straddles the cutoff; model_stylesheet is arbitrary user text, so that
was reachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:16:36 -04:00
Bryan Helmkamp
85586151e5
refactor(graphviz): parse node classes in one place
Node classes were built in two places. The parser split the `class`
attribute on commas and whitespace, but the import transform re-split the
raw attribute on commas only. A space-separated class on an import
placeholder became a single class name, so stylesheet rules did not match.
That included the `class="fast shared"` example in the imports docs.

- add `Node::add_class`, replacing the duplicate append helpers in
  `SemanticState` and `ImportTransform`
- read `node.classes` in `placeholder_config` instead of re-parsing the raw
  attribute, so class splitting happens in exactly one place
- name the separator rule `split_class_attr`, splitting on commas and then
  whitespace so empty entries need no trimming
- drop the unused `Node::class` accessor that invited the re-parse
- keep the comma-compatibility note in the DOT attribute reference only

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:15:30 -04:00
Bryan Helmkamp
af9aa53088
fix(graphviz): parse whitespace-separated node classes 2026-07-29 21:51:57 -04:00
Fabro
22e869c029 fabro(01KYQMV1VW6139EGNHEM1RGF2G): verify (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 10
Fabro-Checkpoint: e01dab2d26

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-30 00:51:46 +00:00
Fabro
84d3ad5be4 fabro(01KYQMV1VW6139EGNHEM1RGF2G): fixup (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 9
Fabro-Checkpoint: d05213a2b3

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-30 00:46:06 +00:00
Fabro
d4815423a1 fabro(01KYQMV1VW6139EGNHEM1RGF2G): verify (failed)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 8
Fabro-Checkpoint: 7425261f11

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-30 00:25:35 +00:00
Fabro
5f46acdf3f Merge remote-tracking branch 'origin/main' into fabro/run/01KYQMV1VW6139EGNHEM1RGF2G 2026-07-30 00:22:09 +00:00
Fabro
a94233407b fabro(01KYQMV1VW6139EGNHEM1RGF2G): simplify_sol (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 7
Fabro-Checkpoint: edcf7467e9

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-30 00:22:06 +00:00
Fabro
6ce5ea76a2 fabro(01KYQMV1VW6139EGNHEM1RGF2G): simplify_fable (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 6
Fabro-Checkpoint: b9c14c247e

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-29 22:31:26 +00:00
Bryan Helmkamp
e0f73f963a
feat(workflow): make model fallback chains portable 2026-07-29 18:08:26 -04:00
Bryan Helmkamp
692301d867
feat(workflow): support comments in model stylesheets 2026-07-29 17:25:04 -04:00
Bryan Helmkamp
81deda3c69
Merge pull request #682 from fabro-sh/feat/raise-stdin-source-ceiling
Some checks are pending
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) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
feat(workflow): raise the stdin_source ceiling to 30 MiB
2026-07-29 17:05:41 -04:00
Bryan Helmkamp
9ef96651cf
feat(workflow): raise the stdin_source ceiling to 30 MiB
The 10 MiB cap on resolved stdin_source values is tight for wide
fan-in: a context.parallel.results batch from a large for_each round
carries tens of structured agent outputs, and a merge step that feeds
them to a deterministic command hits the ceiling as a hard
deterministic failure. Raise the ceiling to 30 MiB; it still bounds
peak memory and remote uploads, just with headroom matched to the
fan-out sizes for_each already allows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:50:08 -04:00
Bryan Helmkamp
456b82e9b7
Merge pull request #681 from fabro-sh/fix/subagent-tool-hooks
fix(workflow): pass parent tool hooks to subagent sessions
2026-07-29 16:49:00 -04:00
Bryan Helmkamp
47fcf917b1
Merge pull request #680 from fabro-sh/fix/repeated-stage-artifacts
Fix artifacts from repeated stage visits
2026-07-29 16:41:50 -04:00
Bryan Helmkamp
1e5dc685d2
fix(workflow): pass parent tool hooks to subagent sessions
Workflow agent sessions run at PermissionLevel::Full with the whole
tool registry exposed, so blocking pre_tool_use hooks are the only
policy boundary they have. The child-session factory built for
spawn_agent dropped tool_hooks from the child's SessionOptions, so a
subagent's tool calls never reached the run's hooks: any agent that
could spawn a subagent got an unguarded read-write-shell escape from
every hook-enforced policy.

Clone the parent's tool_hooks into the factory, the same way the
permission level is already carried, so child sessions inherit the
parent's hook boundary. The new end-to-end test drives the real
create_session path against a scripted mock provider: the parent
spawns a child, the child executes read_file, and the hooks must see
both the parent's spawn_agent and the child's read_file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:38:30 -04:00
Bryan Helmkamp
0f2b66565a
Address artifact review feedback 2026-07-29 16:37:59 -04:00
Fabro
4746d143fd fabro(01KYQMV1VW6139EGNHEM1RGF2G): implement (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 5
Fabro-Checkpoint: c84d4147ba

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-29 20:35:54 +00:00
Fabro
ed59858bd6 fabro(01KYQMV1VW6139EGNHEM1RGF2G): preflight_lint (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 4
Fabro-Checkpoint: e8a60ed97b

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-29 19:20:56 +00:00
Fabro
d23a9dd17a fabro(01KYQMV1VW6139EGNHEM1RGF2G): preflight_compile (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 3
Fabro-Checkpoint: 6ce5170d04

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-29 19:18:16 +00:00
Fabro
8fba42ed18 fabro(01KYQMV1VW6139EGNHEM1RGF2G): toolchain (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 2
Fabro-Checkpoint: 07b27f2f75

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-29 19:15:49 +00:00
Bryan Helmkamp
5f040d3107
Fix artifacts from repeated stage visits 2026-07-29 14:27:37 -04:00
fabro-releases[bot]
854f71f2c5 Bump version to 0.309.0-nightly.2 2026-07-29 17:41:07 +00:00
Bryan Helmkamp
18f07f4d4e
Merge pull request #679 from swerner/fix/remove-workflows-write-permission
Stop requesting GitHub workflow write permission
2026-07-29 13:25:57 -04:00
Scott Werner
1575cc174f Stop requesting workflow write permission 2026-07-29 13:19:55 -04:00
fabro-releases[bot]
1855362354 Bump version to 0.309.0-nightly.1 2026-07-29 15:55:44 +00:00
Bryan Helmkamp
92db1664e3
Merge pull request #677 from fabro-sh/feat/command-stdin-source
Add context-sourced stdin to command nodes
2026-07-29 11:40:31 -04:00
Bryan Helmkamp
6e2a2ac652
Merge remote-tracking branch 'origin/main' into feat/command-stdin-source
# Conflicts:
#	docs/public/workflows/stages-and-nodes.mdx
#	lib/components/fabro-validate/src/rules/inert_attribute.rs
#	lib/components/fabro-workflow/src/handler/command.rs
2026-07-29 11:37:27 -04:00
Bryan Helmkamp
cd6a614ed7
Make stdin_source validation message handler-neutral
CommandHandler also serves type="tool" nodes, so the runtime message now
says "Node '...'" to match the stdin_source_valid lint wording.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 11:24:13 -04:00
Bryan Helmkamp
0eda219376
Simplify stdin_source plumbing after review
- ExecStreamingRequest: drop #[non_exhaustive] and the six Option-taking
  builder setters; call sites use struct literals over ::new(), matching
  GrepOptions/WalkOptions, and providers can destructure exhaustively
- Docker: pass ExecStreamingRequest through docker_exec_shell_streaming
  instead of seven positional args; revert the no-op StartExecOptions
- Daytona: stdin temp-file cleanup is now best-effort (mirrors
  DaytonaSession::close) so a failed delete cannot fail a completed
  command or double-delete from Drop; upload overlaps session creation;
  one shared DAYTONA_CLEANUP_TIMEOUT
- write_process_stdin tolerates ConnectionReset/ConnectionAborted so a
  command that stops reading stdin does not fail on TCP Docker daemons
- Local sandbox aborts the stdin writer after process exit instead of
  joining unbounded
- Cap stdin_source payloads at 10 MiB, mirroring the for_each bound
- Add Node::context_key_attr() tri-state so the handler and lint rule
  share one definition of a valid context-key attribute
- inert_attribute canonicalizes handler types via StageHandler, fixing
  false warnings for command attrs on tool nodes
- Share resolve_flat_context_value between command stdin and for_each;
  resolve_json_value takes Value by value, removing a deep clone
- Reuse MockSandbox in command handler stdin tests instead of extending
  SpySandbox with a hand-rolled streaming override

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 11:14:48 -04:00
Bryan Helmkamp
8a985c2d54
Merge pull request #656 from fabro-sh/feat/infer-command-node-from-script
feat(workflow): infer command nodes from the script attribute
2026-07-29 11:06:39 -04:00
Bryan Helmkamp
d37fc0027c
fix(workflow): align inferred command behavior 2026-07-29 10:54:43 -04:00
Bryan Helmkamp
cb24f47b59
Merge remote-tracking branch 'origin/main' into feat/infer-command-node-from-script
# Conflicts:
#	docs/public/workflows/stages-and-nodes.mdx
#	lib/foundation/fabro-types/src/graph.rs
2026-07-29 10:40:26 -04:00
Bryan Helmkamp
5d72f9a538
Add context-sourced command stdin 2026-07-29 10:25:51 -04:00
Bryan Helmkamp
de7bb61ef5
Remove nonfunctional run agent permissions setting 2026-07-29 10:23:00 -04:00
Bryan Helmkamp
0e2ee787bc
fix(auth): serialize CLI token refreshes across processes 2026-07-29 10:07:03 -04:00
fabro-releases[bot]
d8434e7672 Bump version to 0.309.0-nightly.0 2026-07-29 10:11:28 +00:00