fix: five CLI-fixable tool-error clusters from the Aug 25–28 Langfuse sweep - #820
Conversation
|
Warning Review limit reachedNext included review available in 21 seconds. View limit detailsLimit details: You’ve used all 4 included reviews currently available. Your 65 included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (14)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (14)
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour. 📝 WalkthroughWalkthroughChangesAsset error reporting
Workflow emission model support
Destructive workflow editing
CQL port and edge validation
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The PR fixes five localized CLI behaviors and reports passing validation; no actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
|
@coderabbitai review |
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
2edaec3 to
de3d37c
Compare
2d63b32 to
1857f3e
Compare
|
@coderabbitai review |
|
|
@coderabbitai review |
|
de3d37c to
db071b0
Compare
1857f3e to
145161f
Compare
db071b0 to
c42579a
Compare
145161f to
867a57e
Compare
905070f to
aea6766
Compare
867a57e to
a558702
Compare
aea6766 to
3505c85
Compare
a558702 to
7b892b0
Compare
3505c85 to
3fa7a52
Compare
7b892b0 to
68a5637
Compare
3fa7a52 to
0abfda8
Compare
68a5637 to
57faebb
Compare
…the host, or the node linked to it (BE-10305) The frontend (ComfyUI_frontend ADR 0009 "Subgraph promoted widgets use linked inputs", SubgraphNode.ts) represents a promoted widget as a linked subgraph input whose value the HOST instance owns: `widgets_values[i]` on the instance, consumed positionally by the i-th subgraph input that resolves to an interior widget. The interior widget is only the default; "the host/exterior value wins over the interior/source value during repair, persistence, and prompt serialization". The CLI still followed the legacy `properties.proxyWidgets` list into the interior node for every read and write, so on the z-image turbo template `set-widget 57.width 768` edited a value the frontend neither ran nor displayed (scenario 1), an outside PrimitiveInt could not be wired to `57.width` (scenario 2), and `comfy run` / `validate` submitted the interior prompt on post-migration templates — `audio_minimax_music_3` ships an interior caption of '' while the host holds the whole prompt. cql/promoted.py (new) is the host-owned value model, pure over workflow JSON: `promoted_inputs` (the frontend's own rule — first boundary link whose target is a widget-backed input or a nested instance's promoted input; sockets own no slot), `host_value` (quarantined hostValue first, then the positional slot), `effective_value`, `set_host_value` (materializes the host array in declaration order, never touches the definition), and `resolve_write`: `<instance>.<input>` → host; an outside link feeding it → its primitive (PrimitiveInt / PrimitiveString* / legacy PrimitiveNode, through Reroutes), refused with the driver named when a non-primitive computes the value; `<instance>/<inner>.<w>` fed by the subgraph input node → the same host (op carries `redirected_from`); fed by another interior node → refused; anything else → the definition, as before. Legacy proxies that the definition does not declare as inputs keep the interior route. set-widget / set-slot / vary share that resolution. connect materializes a declared input on the instance (with the frontend's `widget` marker) and wires it, type-checked against the declared type. slots advertises promoted widgets at the instance address with the value the frontend runs, flags link-driven widgets (`linked_from`), keeps unpromoted interior widgets reachable (nested instances included) and no longer advertises the interior address behind a promotion. convert_ui_to_api overlays host values onto the expanded interior nodes with the frontend's precedence (outside link, host, interior); inner instances first so an outer host wins. Verified against the workflow_templates corpus: 75/75 instances that carry host values match the positional rule with 0 mismatches. Fixtures are verbatim gallery templates plus a trimmed cloud-catalog object_info. The host-write op carries `promoted.value_index` and the materialized `promoted.host_widgets_values`: the comfy-multi-player applier stores a subgraph instance opaquely and rejects a named write to an uncatalogued class, so the in-app path needs a matching applier branch (tracked on BE-10305). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce; wrap list host values; tolerate untyped declared inputs Review findings on #815. `host_widgets_values` was read from the instance dict captured during resolution — for a nested host inside a definition shared by two instances, apply forks the definition on the way down and writes the copy, so the op carried the pre-write array and an opaque applier would have replaced the host state with stale values. The payload is now read from the instance apply wrote. The converter overlay wraps host values like every other widget value, so a two-item list host value is not read back as a `[node, slot]` link. A declared subgraph input with no usable type reads as unknown ("") rather than the repr "None": connect skips the type check and stamps the slot with the source type instead of refusing a valid wire with a misleading mismatch. The reuse test now proves the earlier link was replaced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t docstrings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…everywhere Review (annehe9) on #815: `resolve_write` already checked that the link id serialized on a promoted input still exists (the frontend drops a dangling link on load, so the input is unlinked and the host value runs), but the converter overlay and `slots` used the raw id — so `set-widget` wrote the host while `comfy run` submitted the interior default and `slots` pointed at a source node that does not exist. One helper, `live_external_link`, now decides for all three; the converter and the slots walk thread the link scope through (the workflow for a top-level instance, the containing definition for a nested one, whose links are dicts rather than arrays). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ups print as links `comfy workflow print` rendered two things wrongly: * A subgraph instance read its promoted widget values positionally off the instance's serialized `inputs[]` entries that carried a `widget` marker — the frontend stores them positionally per widget-backed DECLARED input (ADR 0009, cql.promoted). Post-migration templates therefore printed the caption under `switch=` (audio_minimax_music_3), a boolean under `drop_audio="lanczos"` (api_seedance2_5_video_extend), and a pre-migration template (image_z_image_turbo) showed `text=None` with every other promoted widget invisible — while the interior lines showed only `IN.width`, so the reader had no value and no address to edit. The instance line now prints one argument per declared subgraph input in declaration order: a promoted widget shows its EFFECTIVE value (`promoted.effective_value`: host value, else interior default) under the address `set-widget` takes (`57.width`); an outside link into it prints as that link (`width=primitive_int`), with the same PrimitiveNode/Reroute markers a regular node gets; sockets print their link. A declared input no boundary link backs falls back to `proxyWidgets`, exactly as `slots` does. Interior lines keep `IN.<name>`, and the definition header gains a `promoted widgets: IN.width, … — edit 57.<name>, never 57/<id>.<name>` line. Nested promotion resolves through `promoted_inputs`' bounded recursion. * An auto-grow group (`COMFY_AUTOGROW_V3`, e.g. `model.images` under a dynamic combo) was invisible on an agent-built node (`add_node` writes no grown slots), and a top-level group's base entry printed as a phantom `images=None` link input. The group's grown slots now print as links, one open slot stays visible under the exact name `connect` grows next (`"model.images.image_3": None` — the free trailing slot the frontend itself keeps), and the line comment names the group with its element type and capacity (`model.images grows IMAGE (max 14)`). Groups follow the node's current selection via `Graph.autogrow_groups`; widgets stay at their value-aware names and positions, never shifted. Tests: tests/comfy_cli/test_workflow_print_promoted_autogrow.py (21 cases, written red-first against the gallery fixtures and object_info_nested_autogrow), plus the seedream golden line updated for the group annotation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erate Every schema-less positional entry printed under the name control_after_generate, so an older save's LoadImage upload slot (["a.png", "image"]) printed as control_after_generate="image". Injected slots (upload / audioUI / PREVIEW_3D image) carry the new frontend_injected flag from #809: they are walked for position and never printed, matching slots (which omits them) and every write surface (which refuses them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ut, not by name Review on #816: the instance line called `promoted.effective_value(workflow, instance, name, graph)` once per promoted widget, and that entry point re-derives `defs_by_id(workflow)` and relocates the input via `find_promoted` -> `promoted_inputs` — both of which the caller already held (`_State.promoted_defs`, and the enclosing loop's `pi`). On 50 z-image instances that was 451 `promoted_inputs` walks for 51 needed; 200 instances rendered in 251 ms against 41 ms on base. Add `promoted.effective_value_for(workflow, instance, sg, pi, graph, defs)` — host value if materialized, else the interior source value — and make `_effective` delegate to it so the by-name path is unchanged. The printer uses it at the call site. The `ValueError` handler there is dropped: `pi` comes from `promoted_inputs` on the very definition `_render_nodes` resolved (`is_subgraph_uuid(t)` and `defs_by_id[t]` both hold), so neither "not a promoted input" nor "not a subgraph instance" could fire. Pinned by a test that counts `promoted_inputs` / `defs_by_id` calls while rendering 40 instances (<= N+2 and <= 2), plus a unit test that `effective_value_for` never re-walks `promoted_inputs` and agrees with `effective_value` for both the interior-default and host-value cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onal payload for PrimitiveNode writes (amendment v1.5) The comfy-multi-player applier (PR #104) implemented the host-write and promoted-connect op shapes #815 introduced and sent two convergence asks back: * A promoted subgraph input is ONE register named by the definition, but `_apply_connect` did not gate it: two concurrent connects onto `57.width` left the entry with whichever link arrived first, and `grow_id` stayed on the first arrival. The grow is now gated by `_lww_gate`/`_lww_commit` on `("input", to_node, "grow", <full name>)` exactly like a concrete input (§11.1): the higher stamp owns the entry in either apply order, `grow_id` follows the winner and the loser's link is retired. The register uses the full declared name — sg input names such as `images.image0` contain a dot, and splitting on it would alias two inputs onto one register. * A frontend-only PrimitiveNode has no catalog entry, so an opaque store rejected the `legacy_primitive` write it had no positional payload for. The op now carries the same `promoted` payload a host write carries (`value_index: 0`, `instance_path`, `host_widgets_values`), and apply treats a `promoted` payload on a non-instance node as a plain positional write. docs/op-vocabulary-v1.md gains Amendment v1.5, which also records the shapes #815 introduced without a doc amendment (`promoted` host writes, `redirected_from`, `grow.promoted`). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…extend truncated positional arrays from the payload Review findings on #818. A later connect to an already-materialized promoted input minted a concrete `to_slot` op on the concrete register: a replica receiving it before the materializing op found no slot, dropped it with its op_id consumed, and then installed the older link — the newer link could never replay. Every connect to a declared promoted input is now the promoted grow (apply reuses the entry by name), so the register is the declared name for the life of the input. `_apply_positional_write` extends a truncated opaque array from `host_widgets_values` whether or not the written index is inside it, so replicas converge on the node's whole state. Fences in Amendment v1.5 carry a language (MD040). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed bail; index addresses share the name register Review (annehe9) on #818: * The grow branch still opened with the pre-v1.5 "source deleted → return", so a promoted connect whose source was concurrently deleted never reached the gate: two promoted connects into 57.width plus a delete of the higher op's source gave two canonical states across the six orders. The bail now applies to autogrow only; a promoted grow gates, claims, retires the loser's link and leaves the entry empty at the existing `src is None` check — delete wins over the link, not over the claim (§11.1), and all six orders converge. * `57.<index>` landing on the materialized `width` entry minted a concrete `to_slot` op on the concrete register, so it never gated against `57.width`. An index that resolves to a declared promoted input now maps onto the name at mint time; both addresses share `("input", 57, "grow", "width")`. * Amendment v1.5: the tail extension of a truncated positional array is best-effort repair (only the written index is under the register); index addressing and the pre-v1.5 concrete-op scope are stated explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hem, the way the frontend does on load (BE-10305)
A `properties.proxyWidgets` entry the subgraph definition does not back
with a linked input used to route `set-widget`/`set-slot`/`vary` into the
interior node — an edit under layer 2 that the frontend's load-time
migration re-seeds the host from (so it looked right) but that no other
promoted write shares and that `comfy run` never submitted as a host value.
`cql.promoted` now carries a port of the frontend's forward migration
(`proxyWidgetMigration.ts`, ADR 0009): `plan_proxy_migration` classifies
every entry of an instance exactly as `classify` does (already linked;
`createSubgraphInput`; `PrimitiveNode` fan-out bypass, all-or-quarantine;
`$$`/preview pseudo-widget exposure; quarantine with the frontend's reason
codes), and `flush_proxy_migration` performs it: a subgraph input named with
`nextUniqueName`, a boundary link from the subgraph input node into the
widget's backing input slot (synthesized in the shape the frontend
serializes when an older save omits it), the host value read positionally
by legacy proxy order, quarantine rows `{originalEntry, reason, hostValue?,
attemptedAtVersion: 1}`, repaired entries consumed. Preview entries are left
in `proxyWidgets` so the frontend's own preview-exposure migration (and its
auto-exposure of preview nodes) produces the state it would have produced
from the untouched file.
`resolve_write` turns such an entry — addressed flat, by its legacy widget
name, or through the interior widget/primitive target it owns — into a HOST
target carrying the repair; the op records `promoted.repair = {entry, ids}`
with input/link ids derived by SHA-256 from (instance path, source node,
widget), never random, so replay on another replica is byte-identical and
concurrent repairs of one instance converge. A shared definition is forked
first, like an interior write. Entries the migration quarantines
(`control_after_generate` has no backing slot) keep the interior widget as
the live one, so those writes land there as before.
Reads present a pending repair where the frontend will show it:
`slots`/`effective_value` advertise `<instance>.<name>` with the legacy host
value or the interior source, and hide the interior address; reads never
mutate. The converter now overlays a host value onto EVERY boundary-link
target, which a repaired primitive fan-out needs.
Fixtures: three verbatim gallery templates (primitive fan-outs under user
titles, `$$canvas-image-preview` exposures next to a value widget with no
serialized input slot, and the common `seed` + `control_after_generate`
pair) with a trimmed cloud catalog. `TestSetWidgetSubgraph` is rewritten for
the migration semantics on a realistic legacy fixture, with the old
declared-but-unlinked shape pinned separately (`nextUniqueName` mints
`text_1` beside the dangling socket).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecting input; guard the primitive link scan (PR #819 review) A legacy proxyWidgets entry whose source is a nested subgraph instance is backed by that instance's own host input for the PROJECTING subgraph input (`getSlotFromWidget(promotedInputWidget(input))`). When the instance serializes no such slot, the repair synthesized one named after the interior widget instead — a name the inner definition cannot resolve when the two differ, so the outer promotion never reached the concrete widget. The synthesized slot now carries the projecting input's name (the shape a subgraph instance serializes), pinned on a fixture where the names differ. The primitive-bypass link scan now screens non-dict link entries like every other scan in the module. Also pins the `<id>:widget` legacy prefix rule (`normalizeLegacyProxyWidgetEntry`) and the disambiguator match through a nested instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… legacy alias, document link replacement (PR #819 review) An interior input slot an older save serializes by `name` alone (no `widget` marker) is the widget's backing slot: the repair now matches it by name and restores the marker instead of appending a duplicate entry beside it. The legacy-widget-name alias (`143.value`) now raises when more than one entry shares that widget name, listing the minted input addresses, instead of silently taking the first match. A slot already fed by an interior link is repaired as before — the frontend calls `SubgraphInput.connect` unconditionally and `connect` replaces the incumbent link (`replaceLinkTopology` + `_disconnectNodeInput`) — now pinned by a test and explained where `resolve_write` deliberately redirects before the interior-feeder refusal. `missingSubgraphInput` for a widget with no backing slot is the frontend's own reason code; commented as such. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…LoadVideo) `comfy run` refused every content hash an agent uploaded to Comfy Cloud and wired into `VHS_LoadVideo.video` (`unknown_enum_value '<64hex>.mp4' not in 1 known options for video`) — an eight-shot assembly never rendered. The port exemption for upload-backed COMBOs only honoured the core loaders' `<kind>_upload` marker; VideoHelperSuite lists the input folder itself and attaches its own upload button by class name, so its catalog entry is a bare `[["bedroom.mp4"]]`. When the catalog declares nothing, the listing decides: a COMBO whose every option is a media file name is a folder listing, not an install-time vocabulary. An explicit `<kind>_upload: false` still wins. Model folders (`.safetensors`) and plain enums stay constrained. Pinned with the real cloud catalog entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…separated accept list ComfyUI types a multi-type socket as a comma-separated union (`INT,FLOAT` on a math operand, `MESH,FILE_3D_GLB,…` on a 3D importer). The validate edge check compared raw strings, so a `FLOAT` output feeding an `INT,FLOAT` input warned `edge_type_mismatch` — 12 of the 23 such warnings in the 08-25..28 prod window were this shape, on edges the frontend draws and the server runs. Route the check (and the "use X[i] instead" hint) through the converter's `_is_valid_connection`, the mirror of the frontend's `isValidConnection`, which expands the union on both ends. Wildcards (`*`, `COMFY_MATCHTYPE_V3`) keep short-circuiting first; the report's other four warnings were that wildcard, which the pin already honours (TestMatchTypeWildcard). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…flow_not_found An agent passed a file name (`comfyorg_logo.png`) where the content hash belongs; the API answered 404 and the CLI said `workflow_not_found` / "workflow not found (ensure)" with a hint to `workflow list` — the `cloud_http` helper's 404 branch hardcodes the saved-workflow vocabulary. Route `ensure` through the parameterized `_cloud_errors` helper with a new registered `asset_not_found` code: the message names the hash, `details.hash` carries it, and the hint says how to get a real one (`assets library ls`, or upload first). The registry test now recognises `not_found_code=` as a code kwarg so callers of the parameterized helper register like any other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`generate --emit-workflow` leaves an API-format draft in the tab's scratch file; every edit then fails `workflow_not_frontend_format` — including `clear`, so nothing could replace the draft and the agent abandoned the tab (Langfuse 2026-08-26). Clearing discards the content, so the format of what is thrown away is no reason to refuse. `clear` and `reset-doc` now load through a loader that accepts either format, substituting the empty frontend baseline for an API-format document so the op applies to a frontend document and the file is editable afterwards. A JSON value that is a workflow in neither format still fails as before; the slot-editing commands keep their frontend-only gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ist` `generate list` advertises the whole proxy catalog while `--emit-workflow` can render five of them as partner nodes; an agent asked for a workflow for `flux-pro` and got the umbrella `emit_workflow_failed` with the supported set buried in prose (Langfuse 2026-08-27). It had no way to know beforehand. Every `generate list` row now carries `emit_supported: bool` (schema-required, `generate_list.json`), computed by the same lookup `build_workflow` uses so the two cannot drift. Asking for `--emit-workflow` on an unsupported model raises a typed `UnsupportedModelError` and surfaces as its own registered code, `emit_workflow_unsupported_model`, with `details.model` and `details.supported` as data; `emit_workflow_failed` is left for bad inputs and unwritable paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0abfda8 to
813c7cd
Compare
57faebb to
f1c5973
Compare
Why
A Langfuse sweep of every comfy-agent tool call from 2026-08-25 to 08-28 (1,764 observations, all on comfy-cli pin
8b9c8b01) found 57 failing tool observations in 23 clusters. Most are cleared by #809/#812/#815 or belong to the doc host / agent service (report:langfuse_tool_errors.md, attached to the Linear thread). These five were new and fixable in the CLI, each with the production payload pinned as a red→green test:fix(cql): accept uploaded files on unmarked input-folder COMBOs (VHS_LoadVideo) (
0f2cd990)comfy runrefused every content hash an agent uploaded to Comfy Cloud andwired into
VHS_LoadVideo.video(unknown_enum_value '<64hex>.mp4' not in 1 known options for video) — an eight-shot assembly never rendered. The portexemption for upload-backed COMBOs only honoured the core loaders'
<kind>_uploadmarker; VideoHelperSuite lists the input folder itself andattaches its own upload button by class name, so its catalog entry is a bare
[["bedroom.mp4"]].When the catalog declares nothing, the listing decides: a COMBO whose every
option is a media file name is a folder listing, not an install-time
vocabulary. An explicit
<kind>_upload: falsestill wins. Model folders(
.safetensors) and plain enums stay constrained. Pinned with the realcloud catalog entry.
fix(cql): no edge_type_mismatch when the produced type is in a comma-separated accept list (
ac701283)ComfyUI types a multi-type socket as a comma-separated union (
INT,FLOATon a math operand,
MESH,FILE_3D_GLB,…on a 3D importer). The validate edgecheck compared raw strings, so a
FLOAToutput feeding anINT,FLOATinputwarned
edge_type_mismatch— 12 of the 23 such warnings in the 08-25..28prod window were this shape, on edges the frontend draws and the server runs.
Route the check (and the "use X[i] instead" hint) through the converter's
_is_valid_connection, the mirror of the frontend'sisValidConnection,which expands the union on both ends. Wildcards (
*,COMFY_MATCHTYPE_V3)keep short-circuiting first; the report's other four warnings were that
wildcard, which the pin already honours (TestMatchTypeWildcard).
fix(assets):
assets library ensure404 is asset_not_found, not workflow_not_found (016d28c6)An agent passed a file name (
comfyorg_logo.png) where the content hashbelongs; the API answered 404 and the CLI said
workflow_not_found/"workflow not found (ensure)" with a hint to
workflow list— thecloud_httphelper's 404 branch hardcodes the saved-workflow vocabulary.Route
ensurethrough the parameterized_cloud_errorshelper with a newregistered
asset_not_foundcode: the message names the hash,details.hashcarries it, and the hint says how to get a real one (
assets library ls,or upload first). The registry test now recognises
not_found_code=as acode kwarg so callers of the parameterized helper register like any other.
fix(workflow): let
clearandreset-docaccept an API-format draft (cec8eadb)generate --emit-workflowleaves an API-format draft in the tab's scratchfile; every edit then fails
workflow_not_frontend_format— includingclear, so nothing could replace the draft and the agent abandoned the tab(Langfuse 2026-08-26).
Clearing discards the content, so the format of what is thrown away is no
reason to refuse.
clearandreset-docnow load through a loader thataccepts either format, substituting the empty frontend baseline for an
API-format document so the op applies to a frontend document and the file is
editable afterwards. A JSON value that is a workflow in neither format still
fails as before; the slot-editing commands keep their frontend-only gate.
feat(generate): flag --emit-workflow support per model in
generate list(2d63b325)generate listadvertises the whole proxy catalog while--emit-workflowcan render five of them as partner nodes; an agent asked for a workflow for
flux-proand got the umbrellaemit_workflow_failedwith the supportedset buried in prose (Langfuse 2026-08-27). It had no way to know beforehand.
Every
generate listrow now carriesemit_supported: bool(schema-required,generate_list.json), computed by the same lookupbuild_workflowuses sothe two cannot drift. Asking for
--emit-workflowon an unsupported modelraises a typed
UnsupportedModelErrorand surfaces as its own registeredcode,
emit_workflow_unsupported_model, withdetails.modelanddetails.supportedas data;emit_workflow_failedis left for bad inputsand unwritable paths.
Verification
Full suite on this head (the stack head,
7b892b0f): 6379 passed, 38 skipped; public-repo-hygiene green across the stack. ruff 0.15.15 check + format clean.🤖 Generated with Claude Code