Skip to content

Develop - #68

Merged
dkackman merged 43 commits into
masterfrom
develop
Sep 12, 2026
Merged

dkackman merged 43 commits into
masterfrom
develop

Conversation

@dkackman

Copy link
Copy Markdown
Owner

No description provided.

dkackman and others added 30 commits September 11, 2026 17:16
… 1 plan

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tution

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and-written steps

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nition

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…AUDE.md mirror, manifest note

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…done

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…able, keep source paths

Three findings from the whole-branch review.

expand_for_each deep-copied every leaf of every step. It runs on every
run of every workflow, after realize_args has turned 'asset:' and
'*_image' arguments into loaded PIL images and decoded frame lists, so
that multiplied the media a run holds - and a leaf that cannot be copied
(an open handle, a live model object) failed a run that had always
worked. A leaf is now copied only where the copy is needed: inside a
member, where one template value is about to appear in every one of
them, and there the copy falls back to the object itself when deepcopy
raises, the choice the step cache already makes. The 'input untouched'
contract holds because the pass never mutates a leaf.

An unrelated undeclared 'variable:' made the pre-flight blame for_each:
replace_variables raises on the first one, and the whole definition was
then left unsubstituted, so a perfectly good 'shots' list reached the
expansion as the literal string 'variable:shots' and the error said the
list was never substituted - false, and it hid the actual typo.
validation_errors now reports every undeclared reference at the path it
sits at. That is an error rather than a warning because it is always
fatal: once a workflow declares variables, replace_variables refuses an
undeclared reference, so it is a run that cannot start. The check found
one in workflows/templates/inpaint.json, which declared 'mage'.

Reference errors on the expanded definition rendered expanded step
indices - a path that exists in no file the author wrote. expand_for_each
now records each expanded step's source index in a caller-supplied list
(not a key on the step: step_data is what the step cache keys on and
what the schema validates), and previous_result_reference_errors renders
that index, naming the member in the message so the reader knows which
expansion failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…otes

CLAUDE.md's older previous_result bullet still said a reference spelled
by a 'variable:' is left alone, which the substitute-then-check order
has made false in both directions: a declared variable's reference is
checked by its value, and an undeclared one is itself an error now.

The guide said two for_each steps over the same list are paired by name;
the mechanism is by key - the entry's name, else its index - and it now
states where an error's path points, since expansion no longer leaks
expanded indices.

The proposal records four observations for the stage-2 template rewrite:
pipeline_reference naming a group gets no directed error, an entry field
named image/*_image/*_video is realized before expansion, an empty list
expands to no steps at all, and expanded_definition skips
realize_constants.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n, schema, agent docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…generation

T020 - run_workflow refused an argument *name* it did not recognise and
queued a bad 'asset:' reference anyway, so a typo came back as a job id and
a status: running, then died on the first step. The name half of the check
lives in JobManager.submit, where the definition is loaded; the reference
half needs the workspace's search path, so POST /api/jobs now makes the same
_argument_reference_errors call POST /api/validate does. Same message from
both, and nothing is queued.

T021 - a ModularPipeline (H3, LTX-2, Qwen-Image) has no
callback_on_step_end, so the route that reports every other pipeline's
denoise steps never fired: one 'generating' phase and then nothing for the
whole loop, which reads exactly like a hung run. Its denoise block drives a
tqdm bar instead, so reported_progress_bars patches that bar per instance
for the length of the call - every advance is a pipeline_step event and a
cancellation checkpoint, and the patch is undone on the way out. Uses
_blocks, not the public blocks, which hands back a deepcopy.

Beside it, a running job carries a `progress` block on GET /api/jobs/{id}
and on the MCP poll: the step, the phase and how long it has been in it,
seconds_since_event, and the denoise counter once that loop runs - kept as
events arrive rather than derived from a log that gets trimmed. That is what
separates a slow run from a stuck one between two otherwise identical polls.

T016 - proposal only, per Don: docs/proposals/output-folders.md.

Additive throughout; no field renamed or removed.
…oise progress + job progress block, T016 proposal
…ther variable

Resolved once before realize_args, reported as undeclared at the entry's
path, cycles refused. This is how a for_each template's optional voice
variable reaches every shot the character speaks in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n error, fold lookup

A variable cycle now surfaces as a validation error at path "variables"
instead of escaping as a bare ValueError. resolve_variable_values reuses
_resolve_variable_reference for the "variable:name" lookup instead of
duplicating its not-found message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ps over one shots list

Breaking for scripted callers: shot_1_wide_open .. shot_4_finale are
entries of 'shots' now, each {name, prompt, start_frame}.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ver a shots list

Breaking for scripted callers: shot_1_cold_open .. shot_5_tag, num_frames
and tag_num_frames are entries of 'shots' now, each {name, prompt,
references, num_frames}. An entry's voice reference names
character_a_voice / character_b_voice, so one variable still sets a voice
in every shot the character speaks in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…me variables

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e before multiplying

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing lead-in

A modular pipeline spends up to ~90s inside `generating` before the denoise
loop - encoding the prompt and any reference image or audio - and emits
nothing. With `denoise_step` only appearing once there was a count, two polls
a minute apart came back byte-identical with no counter, which is exactly the
signature the docs call a hung job.

`Job.progress()` now always carries `denoise_step`/`denoise_total_steps`, null
until the loop starts, so the lead-in is distinguishable from a loop that has
stopped advancing, and the MCP docstrings and docs state the rule that way:
`seconds_since_event` only means something once `denoise_step` is a number, or
in any phase other than `generating`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…der; test hygiene

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ntry strings validated; docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…warning, rulings, flow-view edges

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n workflow's editor is not an orphan

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dkackman and others added 13 commits September 11, 2026 22:20
A phase says what a step is waiting on; nothing said what it cost. The
events a job records carry no clock, so 'each H3 shot has a slow start'
was an impression - the reference steps emit no loading phase (the
pipeline is reused), and the gap between step_start and the first
pipeline_step is the per-call encoding and CPU->GPU streaming, but there
was no number to put on it.

Job.add_event now stamps each event with `at`: seconds since the job
started (since it was queued, for the events before that), beside `seq`.
The SSE stream, the event-log page and MCP get_job_events pass it
through unchanged; where a step's time went is a subtraction between
two events. Documented in SERVER.md, MCP.md and the tool docstring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
list_fields (dw/for_each.py) reads each step whose for_each names a
variable and collects the item:<field> references its members read, so
the catalog's listing and details carry a `lists` block - per variable,
the fields an entry takes (name first), the steps that read it, and the
default list's length. Compact listings carry it only when set, like
`configures`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he entry's path

entry_field_warnings (dw/for_each.py) folds good arguments into a copy
of the declared variables and, for each list-driven variable whose
value is a list of dicts, reports every key of every entry that no
step's item:<field> reads - a caller's typo (num_frame for num_frames)
silently falls back to the template's default otherwise. Hooked into
POST /api/validate's warnings alongside workflow_argument_warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reaches inside list entries

per_entry on a cost entry names the list-driven variable it was measured
over, the minutes one entry costs, and the entries the default list held
- so a run over a different-length list can be priced from it rather than
guessed. Required, all three fields, additionalProperties: false. No
bundled template carries one yet (the numbers come from a later GPU run);
test_a_per_entry_cost_names_a_list_the_steps_read checks any that appear
against list_fields and the default list's length, and a synthetic case
in test_catalog_structure.py exercises the mismatch it exists to catch.

GET /api/workflows/{name}/variables' preview now recurses into list and
dict values, so a shot's prompt inside a list-driven default is cut to
200 characters like a top-level one, named in `truncated` as
`shots[0].prompt`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ts; step cache holds a maximal run

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…of raising

expanded_definition realized constants over the whole variables dict, so a
'constant:' default that fails to resolve (unresolvable name, malformed
name, or one outside the trust allowlist) escaped validation_errors as an
unhandled exception rather than the usual [{path, message}] shape. Realize
per top-level variable instead, wrap the three exceptions fetch_constant
and realize_constants can raise in a new ConstantError(path, message), and
catch it in validation_errors beside ForEachError - so POST /api/validate
reports 'variables.<name>' with the real message instead of swallowing it
into a generic 500. Workflow.run is unchanged.

Also: moved the empty-list check in for_each.py's _entry_keys ahead of the
ceiling check, and dropped the tombstone comment left where the deleted
empty-group gather test used to be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…by the skill and the guide

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…trings match lists/per_entry

dw/validate.py never called set_trust_workflows and had no --trust-workflows
flag, so validating a workflow whose variable default realizes an
out-of-ecosystem 'constant:' name failed with a message telling the user to
pass a flag the command lacked. Mirror dw/run.py: add the same argument and
call, same help text. Pins the UntrustedWorkflowError -> ConstantError branch
in dw/workflow.py with a test that validation_errors() reports it as
[{"path": "variables.<name>", ...}] rather than raising, and that the same
definition validates once trusted; also pins that dw-validate's
--trust-workflows actually flips the gate before the workflow loads.

dw_mcp/server.py's list_workflows/get_workflow tool docstrings and the
server-level instructions still described the pre-list-driven catalog shape
(no `lists`, wrong wording for per_entry and the 200-char preview into list
defaults) - bring them in line with what dw_mcp/catalog.py and docs/MCP.md
already say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rnings surface

docs/SERVER.md's full GET /api/workflows listing paragraph named neither
`lists` nor per_entry cost, though the compact-view bullet right below it
already did; "plus the four new fields" was also stale since lists and
per_entry pushed the count past four. Its POST /api/validate paragraph did
not mention that `warnings` also covers an unread entry key of a list-driven
variable. docs/MCP.md's validate_workflow row didn't mention warnings at
all. docs/WORKFLOW_GUIDE.md's for_each Limits paragraph didn't yet say an
empty list is a validation error or that validation realizes a constant:
default before checking it - CLAUDE.md's Type System bullet already states
both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…batim

The brief asked for the same help text as dw/run.py's flag; the first pass
paraphrased it instead of copying it, which would have let the two commands'
--help drift apart again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dkackman
dkackman merged commit 46298f6 into master Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant