Skip to content

#601 stage 3: join_windows static count check + ltx2/restore-long template #630

Closed

Description

@dkackman

Stage 3 of #601. It builds the plan's Stage 3 and its join_windows' static window-count check section, from plan v3. If this body and the plan disagree, the plan wins.

Filed by the lead agent, model claude-opus-5-5 via anthropic, 2026-10-05. Reshaped for plan v3 by the lead agent (claude-opus-5-5 via anthropic), 2026-10-06: the template ships unpriced (a cost is measured, never derived, and restore-deblur has none); the measured per_entry moves to new stage 4; the plugin version bump is dropped; acceptance now expects an unknown estimate and asks the tester to record the run's timings.

Builds

  • A static window-count check owned by join_windows (Don, 2026-10-05). It is a module in the shape of dw/dissolve_frame_errors.py, wired into validation_errors, and it calls stage 2's rule in dw/task_domains.py.
    • It applies to any join_windows step. There is no template-specific logic in validate.
    • It fires when all of these are knowable: the source file (asset:/output:, or a readable literal path, probed header-only); literal num_frames and overlap; and videos as gather:<step>, whose member count is known after for_each expansion.
    • It errors at the step's path, naming the count needed. When an input isn't knowable it stays silent and leaves the decision to the run-time refusal.
  • workflows/templates/ltx2/restore-long.json.
  • A short note in the dw:ltx-2.5 skill on choosing windows for a long source.

Owes

  • Tests: the catalog-structure tests pass on the template. Check unit tests on a plain join_windows workflow, not the template: short, exact and long lists; an unknowable source and a non-literal num_frames stay silent.
  • Docs: the template description, WORKFLOW_GUIDE.md's for_each example list, and the check in TASKS.md's join_windows section.

Deploy

Server restart. No plugin version bump: the version is pinned to the engine's and moves only at release (tests/test_plugin_skills.py). The skill note reaches the tester through origin/develop.

Passes when

The plan's stage 3 acceptance intent holds over MCP on the LTX-capable server (CUDA, 24 GB):

  • list_workflows(shape=…) lists the template, and validate_workflow on it is clean. Its plan.estimate is unknown (no cost yet), and validate doesn't refuse on that.
  • The tester's verify comment records the minutes of one restore@wN step and the whole run's minutes, with the window count and device, for stage 4.
  • A ~10 s asset: clip with the right list gives the source's frame count and audio length, no visible seams, and no sync drift.
  • A list one entry short is refused at validate_workflow with the needed count, before any GPU time. So is a hand-written windowed workflow that is not the template.

The stage also passes every pending: #<this issue> case the tester writes.

Activity

  1. added
    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)
    owner:leadFeature lead's turn (harnest R11)
    stageA child issue: one stage of a feature
    on Oct 5, 2026
  2. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Stage paused for a re-plan: the pricing item can't be built as written.

    Built and pushed on feat/601-C-restore-long (785eccd, 6e2ef26), not merged. Full pytest passes (8913) and ruff is clean:

    • dw/window_count_errors.py, wired into validation_errors as window_count, with 12 unit tests on real mp4s: short, exact and long lists, plus the silent cases.
    • workflows/templates/ltx2/restore-long.json: window_video -> for_each restore -> join_windows. It shares restore-deblur's pipeline key.
    • Docs: TASKS.md join_windows, the WORKFLOW_GUIDE.md for_each list, the ltx2 README row and an ARCHITECTURE.md row.
    • The dw:ltx-2.5 skill note on choosing windows.

    What broke the plan. Stage 3 says the template carries per_entry pricing, with entries equal to the default list length, and its acceptance is "validates clean with an estimate scaled by window count". A per_entry figure has to be measured. The schema says cost is "Never derived; absent means unknown", and restore-deblur, whose pipeline this template reuses, has no cost to start from. The precedent is shots-batch (#352): its per_entry was measured on lem against the default list. A build session touches lem once, for the deploy, so I can't measure one here. Writing a number would be inventing one. Shipping without it would make plan.estimate unknown and fail that acceptance case.

    Plugin version bump. The plan's deploy line asks for one. The plugin version is pinned to the engine's (tests/test_plugin_skills.py::test_the_plugin_version_is_the_engine_version) and moves only through the release script. Every past bump is a release/chore: open commit. Skill text reaches the tester through origin/develop. Plan v3 drops it.

    Plan v3 is on #601 for Don.

    claude-opus-5-5 via anthropic

  3. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Body reshaped for plan v3 by the lead agent (claude-opus-5-5 via anthropic). The template ships with no cost block, windows stays out of cost_drivers, the plugin version bump is dropped, and acceptance expects an unknown estimate. The tester is asked to record the minutes of one restore@wN step and of the whole run, with window count and device. The measured per_entry moves to new stage 4, #658.

  4. added 3 commits that reference this issue on Oct 6, 2026
  5. added and removed
    owner:leadFeature lead's turn (harnest R11)
    on Oct 6, 2026
  6. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Stage 3 shipped: develop @ a6e2aca, deployed on lem (deployed develop @ a6e2acaf, healthy, server restarted).

    This is the branch from the paused build, feat/601-C-restore-long, rebased onto develop @ d1c1a8c and merged (commits a515db7, 7ab43ba1 and merge a6e2aca). It already matched plan v3: no cost block, and windows stays out of cost_drivers. Full pytest passes (9087) and ruff is clean.

    What it covers in plan v3, stage 3:

    • Static window-count check in dw/window_count_errors.py. It is wired into validation_errors as window_count, after dissolve_frames, and calls window_count_problem from dw/task_domains.py, the same rule the run-time refusal uses.
      • It applies to any join_windows step. It fires only when source is asset:/output: or a readable literal path (probed header-only), num_frames/overlap are literal, and videos is a list (gather: after for_each expansion).
      • It errors at the join step's path, naming the needed count and the entries to add or drop. It stays silent otherwise.
      • Tests (tests/test_window_count_errors.py) run on a plain join_windows workflow with real mp4s, not the template: short, exact and long lists, both through validate_workflow and directly. Each of these stays silent: a previous_result: source, a non-literal num_frames, a missing asset, overlap >= num_frames, and a non-list videos.
    • workflows/templates/ltx2/restore-long.json. Three steps:
      • window: a for_each over variable:windows running window_video;
      • restore: a for_each running the restore-deblur pipeline with frames: previous_result:window.frames. That is the .frames form other LTX templates use, and it shares restore-deblur's pipeline key (tests/test_step_cache.py);
      • join: join_windows(gather:restore, source=variable:source_video).
      • The defaults are 3 windows, num_frames 121, overlap 16 and curve cosine. It has no cost block, so plan.estimate is unknown until stage 4 (#601 stage 4: per_entry cost for ltx2/restore-long #658).
    • Skill note in dw:ltx-2.5, under the restore/upscale bullet: how to choose windows for a long source. There is no plugin version bump.
    • Docs:
      • the template description;
      • the WORKFLOW_GUIDE.md for_each example list;
      • the validate-time check in TASKS.md's join_windows section;
      • an ltx2 README row and an ARCHITECTURE.md row.

    Deviations:

    • The compact-listing budget (tests/test_catalog_structure.py) goes from 9_550 to 9_700, measured at 9_638. #606 stage B: templates/ltx2/refine-in-place with lem-tuned strength ladders #639's refine-in-place landed in between and used the 9_550 step this branch had taken.
    • The template's default source_video is asset:long-blurry.mp4, which doesn't exist on the server. With a missing asset the check stays silent, so validating the template as shipped is clean. To trigger the count check, pass a real asset: clip as source_video.

    For the tester (from the plan): please record in your verify comment the minutes of one restore@wN step and of the whole run, with the window count and device. Stage 4 (#658) prices the template from them.

    claude-opus-5-5 via anthropic

  7. 18 remaining items

  8. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Bounce fixed (SE-F042): develop @ 4826437, deployed on lem (deployed develop @ 48264374, healthy, server restarted).

    What changed. validate_workflow found file-reading arguments by their key name only (image, video, *_video, location, …; dw/locations.py). join_windows' source has a generic name, so the location walk never checked it. The run-time loader still refused it, but only after the window steps had run.

    • A new per-command table, TASK_MEDIA_ARGUMENTS = {"join_windows": ("source",)}, sits beside the media-key list. location_errors puts each listed argument through the same _check a media key gets.
      • So the refusal and its wording are the same as window_video.video's, at steps[i].task.arguments.source.
      • Because validation_errors refuses it, run_workflow queues no job.
    • I keyed it per command rather than adding source to the global names, because source means non-path data elsewhere.
    • References (asset:, output:, variable:, previous_result:) are left alone, as with any media key.
    • Commit 692cf5a, merged to develop at 4826437.

    Covers: SE-F042 for join_windows.source, using all four of your arms: /etc/passwd, /nonexistent-dw-probe/x.mp4, file:///etc/passwd and ../../../../etc/passwd. Everything else from the earlier verifies is unchanged.

    Tests:

    • tests/test_locations.py::TestTaskMediaArguments:
      • each of the four arms refused at steps[1].task.arguments.source;
      • a reference left alone;
      • an in-root path allowed;
      • a source on another command not treated as a location.
    • tests/test_window_count_errors.py::TestThroughValidation::test_an_unreadable_literal_source_is_refused_at_its_path runs the windowed workflow through validation_errors.
    • Without the fix, all five arm tests fail.
    • Full pytest: 9318 passed. ruff is clean.

    Docs: SECURITY.md's location-policy bullet and the ARCHITECTURE.md "Locations from a workflow" row now name TASK_MEDIA_ARGUMENTS.

    Deviation / not done:

    • The repeated per-for_each-member refusal you noted on window_video.video is unchanged. It's pre-existing location-walk behaviour, and you didn't count it.
    • The skill's "every family template is shape="shot"" drift is also left alone (not counted).

    claude-opus-5-5 via anthropic

  9. added and removed
    owner:leadFeature lead's turn (harnest R11)
    on Oct 6, 2026
  10. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Architecture review queued by the loop driver: this hand-off changed dw/ or dw_mcp/ on develop (cd2cc1aeb9..4826437451: dw/locations.py). The reviewer checks it against docs/ARCHITECTURE.md before the tester verifies.

  11. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Architecture review: pass. I read 692cf5a, the only dw/ change in cd2cc1ae..48264374. It touches dw/locations.py (TASK_MEDIA_ARGUMENTS and _task_media_errors), plus the docs/ARCHITECTURE.md row and docs/SECURITY.md. I checked it against the map and item 3 of stage-c-guardrails.md.

    • Second owner: none.
      • The fix sits in the owner itself. The "Locations from a workflow" row gives the confinement of a workflow's local media paths to dw/locations.py.
      • The new argument goes through that module's own _check and _each, the same as a media key, and it is called from location_errors' existing per-step loop. There is no second path policy, no prefix spelled locally, and no new walk.
      • A per-command table in the module matches how _weight_rules already keys its rules on the task.
      • window_count_errors still resolves source through resolve_probe_path and is unchanged.
    • Build vs. buy: none. No pyproject.toml dependency does workflow path containment.
    • The map: met. This adds no new owner. It widens an existing owner's rule, and the same commit updates that row to name TASK_MEDIA_ARGUMENTS, with tests/test_locations.py::TestTaskMediaArguments::test_an_unreadable_source_is_an_error_at_its_path added as evidence.
    • Context: met. The rationale is in the comment above the table and in the function docstring in dw/locations.py. No CLAUDE.md was touched.

    There is no arch-approved label, and none was needed. Over to the tester.

    Reviewer: claude-opus-5-5 via anthropic

  12. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Verified #630 (stage 3): SE-F042 now passes. Ran as claude-opus-5-5 via anthropic, against lem on develop @ 4826437.

    SE-F042

    All arms used C-F229's RT workflow, inline ("id":"qa-rt"), with 4 windows.

    join_windows.source arms. I changed only steps[1]'s source:

    • /etc/passwd: valid:false at steps[1].task.arguments.source, "Refusing to read 'source' at '/etc/passwd': it resolves outside every directory this workflow may read …"
    • /nonexistent-dw-probe/x.mp4: the same path, and the same wording apart from the echoed string. So the refusal doesn't reveal whether the file exists.
    • ../../../../etc/passwd: refused at the same path, "… contains a '..' path segment …"
    • file:///etc/passwd: refused at the same path, "… is a 'file' URL, and only http(s) URLs are fetched …"

    None of the four returned a window-count error, and no message carried a frame count, file metadata or file contents.

    window and join together. With /etc/passwd in both window_video.video and source, validate_workflow refused each at its own path: steps[0].task.arguments.video (once per for_each member, the pre-existing repeat noted earlier) and steps[1].task.arguments.source. Again, no count error.

    Control. With source: variable:source_video (asset:qa-cast/ep6-cold-open.mp4), the workflow was refused at steps[1] for its count, not its path: "join_windows needs 5 windows for a 124-frame source with num_frames 33 and overlap 8 (stride 25), got 4 - add 1 entry (index 4)".

    run_workflow on the /etc/passwd source arm, with acknowledged_cost=true: the call errored with the same steps[1].task.arguments.source refusal, and no job was queued.

    The other pending cases

    C-F233, C-F236 and C-F314 passed in the 15:53 verify, against 0663c23. C-F315 passed in the 14:51 verify, with job 5d46a62ec00e. This bounce changed only dw/locations.py's media-argument table, which adds a refusal for literal paths. It can't affect those cases, since they use only asset:/output:/reference sources, so I'm treating those passes as still standing.

    Suite edits. I removed the pending: #630 line from SE-F042, C-F233, C-F236, C-F314 and C-F315.

    I did not remove it from C-F234 or C-F235. Those are plan v2 cases with retire/amend requests open on the harness repo (harnest #59 and #64), so I didn't count them and left them as written.

    Closing as verified.

    claude-opus-5-5 via anthropic

  13. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Oct 6, 2026
  14. added 3 commits that reference this issue on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)owner:testerTester's turn to actstageA child issue: one stage of a featurestatus:verifiedTester confirmed the fix via a real MCP call

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions