Commit Graph
264 Commits
Author SHA1 Message Date
961a95b9bd feat(ltx-director): reference-image identity — Ghost Mask + schedulable guide attention (#264)
* feat(ltx-director): Ghost Mask reference image (REF / character sheet)

Add a no-LoRA reference-image identity anchor to LTXDirector__koolook, the
way a WAN VACE reference works: attach reference image(s) and the model keeps
the character's face / mouth consistent even when keyframes show a closed (or
different) mouth than the audio-driven motion needs. Targets the mouth /
identity drift seen in the talking-goat lip-sync workflow.

Mechanism (Ghost Mask): the reference image(s) are appended as masked guide
frames *after* the clean video region, so the transformer attends to them for
identity but they never appear in the output; the trailing reference frames
are then sliced off the result.

- LTXDirector__koolook: new optional `reference_images` (IMAGE) +
  `reference_strength` inputs, and appended `clean_latent_frames` /
  `clean_pixel_frames` outputs. All additive/optional -> saved workflows
  unaffected. Refs are appended AFTER snap/ease so they are never grid-snapped
  or ease-expanded; the auto latent is grown to hold them. ltx_director.py is
  under ruff's forks/ exclude.
- reference_ghost.py (new, Koolook-original, pure stdlib): trailing-reference
  geometry (insert frames, grown latent length, slice bounds). Unit-tested.
- k_clean_latent_slice.py (new root node `CleanLatentSlice`): torch.narrow
  slice to drop the trailing reference frames (start=0,
  length=clean_latent_frames). torch imported lazily; clamp math unit-tested,
  slice path tested when real torch is present (skips under the CI stub).
- Registered the node; added to sync_to_dev RUNTIME_PATHS.
- Attribution: the Ghost Mask APPROACH is adapted (idea only, reimplemented)
  from CGlide/WhatDreamsCost-CSGlide (main_cs @ 21dd076, GPL-3.0, license
  verified 2026-06-24, GPL->GPL OK). THIRD_PARTY.md, forks_manifest.yaml,
  UPSTREAM_PIN.yaml, CHANGELOG updated.

Phase 1: Ghost Mask only. The LoRA-backed Licon MSR prefix mode and the
editor's character-slot UI are intentionally NOT ported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(sidebar): action-led star footer ("Give a ★ on GitHub")

Reword the GitHub discovery footer from the passive "Help creators find
us on GitHub" to an action-led "Help creators find us! Give a ★ on
GitHub", with the star as the explicit ask. Render the sentence as one
inline text flow (not separate flex children) so the footer's `gap`
doesn't space out the words; the ★ glyph keeps its gold styling inline and
now carries role="img"/aria-label="star" so it is announced. Verified
rendering in an isolated harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx-director): Ghost Mask broke motion in the append-and-crop pipeline

The upstream guide pipeline (LTXDirectorGuide + LTXDirectorCropGuides)
appends every guide_data image as an extra latent frame and crops them
back off via `exact_crop_frames = latent.shape[2] - initial_latent_length`.
The Ghost Mask code (modeled on CGlide's CleanLatentSlice flow) ALSO
pre-grew the video latent by n_refs, so initial_latent_length became
clean+n_refs: CropGuides then left n_refs frames uncropped AND the audio
latent stayed at the clean length, desyncing the A/V joint transformer and
breaking motion.

Fix: don't pre-grow the latent. References are added to guide_data exactly
like timeline keyframes and ride the same append-and-crop path — the guide
node appends them and CropGuides removes them, audio untouched. Auto latent
is sized to the clean length again. CleanLatentSlice/clean_latent_frames
remain available for bare pipelines that don't use LTXDirectorCropGuides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): LTX Guide Reference Strength — per-stage reference boost

Adds a small Koolook node that rewrites the strength of ONLY the Ghost
Mask reference frames in guide_data (the trailing entries the Director now
tags with `reference_count`), leaving the keyframe pins untouched. Insert
it before a stage-2 refinement guide and set ~0.9 to lock identity during
the low-denoise pass without disturbing the motion set in stage 1 — the
deterministic form of "more reference in stage 2".

Chosen over vendoring the 600-line upstream LTXDirectorGuide (which would
force a node-ID rename + graph rewire): this is one small node inserted on
the stage-2 guide_data wire. The sigma-ramp alternative (ramp reference up
at low sigma) is deferred — the reference hook is noise_mask + a core-model
attention term, so it needs a live-model spike, not a clean static patch.

- Director tags guide_data["reference_count"] = n_refs.
- k_ltx_guide_reference_strength.py: pure strength rewrite (absolute /
  multiply, clamped), unit-tested; registered; added to sync paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): LTX Reference Bind Schedule — sigma-ramp the reference attention

Confirmed against core (comfy/ldm/lightricks/model.py): the reference's
guide attention strength is turned into a log-space additive self-attention
bias in `_build_guide_self_attention_mask`, which is rebuilt EVERY denoise
step and explicitly supports strength > 1.0 to amplify. That's a clean
per-step hook — so the "inverse sigma curve" idea is buildable, not just a
stage knob.

New node mirrors LTXAVBindSchedule: a model_function_wrapper captures the
per-step sigma and stashes a gain in transformer_options; an object-patch on
diffusion_model._build_guide_self_attention_mask multiplies the strength of
ONLY the trailing `num_references` guide entries (references are appended
after keyframes) by a smoothstep ramp — neutral early (motion forms), rising
to peak_strength late (identity locks during refinement). Keyframe pins and
noise-mask freezing untouched; orthogonal to reference_strength. One pass, no
re-noise, no LoRA.

- ref_gain ramp + _boosted_entries unit-tested (11 tests); model wiring
  validated by live render.
- Registered; added to sync paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): mark Reference Bind Schedule inert without an IC-LoRA

Live diagnostic on the AV model showed the builder is reached and the sigma
gain flows in, but resolved_guide_entries=None despite 3276 guide tokens.
Root cause: LTXDirectorGuide only records guide_attention_entries when an
IC-LoRA is active (`if is_lora_active:` gates _append_guide_attention_entry).
With no LoRA there is no per-guide attention bias to scale, so the
attention-ramp lever does not exist in the Ghost Mask path — it can only
attenuate/amplify entries that an IC-LoRA created.

Rather than silently no-op, the node now logs a clear INERT warning when the
entry list is empty, and the docstring/DESCRIPTION/CHANGELOG state the
IC-LoRA requirement. The node remains correct for the IC-LoRA path (its
partner). No-LoRA reference strengthening stays on the noise-mask lever
(reference_strength / LTX Guide Reference Strength).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): Reference Bind Schedule works without an IC-LoRA (fabricates entries)

The attention-bias identity channel (_build_guide_self_attention_mask) is
base-model functionality, but LTXDirectorGuide only populates the per-guide
entries when an IC-LoRA is active, so the node was inert in the no-LoRA Ghost
Mask path. Now, when entries are missing, the node FABRICATES them for the
trailing reference tokens: it splits num_guide_tokens across
(num_keyframes + num_references) guide frames and assigns the sigma-ramped
strength to the reference share, then hands off to the core builder. This
gives the reference the same amplified-attention channel an IC-LoRA would use
— inside the Director, no guide-node vendor, no graph rewire (the node is
already in the user's setup). When an IC-LoRA IS active it scales the real
entries as before.

- New `fabricate_reference_entries(num_guide_tokens, num_references,
  num_keyframes, strength)` (pure, unit-tested) + `num_keyframes` input.
- Wrapper now does scale-or-fabricate; clearer one-shot diagnostics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): keep num_keyframes last so pre-existing RefBind widgets stay aligned

num_keyframes was inserted mid widget-order, which shifted ramp_start/ramp_end
for workflows saved before it existed. Move it to the end of the optional
inputs so saved [num_references, peak_strength, ramp_start, ramp_end] values
keep mapping correctly; num_keyframes just defaults to 0 on reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(test): drop unused pytest import (ruff F401)

The guide-reference-strength test uses plain asserts; the stray
`import pytest` tripped ruff F401 and reddened the Lint & Smoke CI job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx-director): default keyframe guidance 0.8 (smooth), not 1.0

The timeline editor auto-built the guide_strength string from each clip's
per-segment guideStrength, filling 1.0 for any clip left unset (and the
Python fallback matched). 1.0 is a hard pin, so a clip you forgot to lower
snapped instead of transitioning smoothly. Unset clips now default to 0.8
in the editor (display + serialization) and the node; explicit per-clip
values still override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): Keyframe Soften Schedule — smooth motion early, sharp poses late

New model-patcher node LTXKeyframeSoftenSchedule: sigma-schedules the LTX
keyframe denoise-mask pins so the model forms smooth motion between poses
early (pins loosened at high sigma) and resolves sharp poses late (pins
restored by a crossover). Uses the sampler's per-step denoise_mask_function
hook — no IC-LoRA required — and adds no guide frames (so it can't fabricate
extra poses). Audio-safe: only the video half of the NestedTensor A/V mask is
softened; the audio mask is untouched, so lip-sync is unaffected.

Two normalized 0-1 widgets map onto the sensitive useful range: max_soften
(ease, ->0-0.20) and crossover (invention window, ->0-0.05). Pure soften_gain
ramp unit-tested (11 tests); model wiring render-validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(review): chain denoise hook, optional_latent clean-length, audio test, CGlide manifest

Addresses PR #264 review findings:

- MEDIUM (Soften node): chain a pre-existing denoise_mask_function instead of
  clobbering it (mirrors the sibling LTX patchers' wrapper-chaining), so the
  node composes with anything else that sets that hook.
- HIGH #2 (Director): clean_pixel_frames/clean_latent_frames now derive from the
  ACTUAL optional_latent shape when one is connected, so Ghost-Mask reference
  geometry and the reported counts follow the real latent instead of the
  timeline duration (with a safe fallback + warning).
- LOW (Soften node): reconcile docstring (crossover is sigma-progress, maps to
  internal 0-0.05), one-time warn on the sigma-read fallback, and extract a
  testable soften_denoise_mask() helper.
- LOW: lock the audio-safety guarantee with NestedTensor-stub tests — video
  mask softened, audio mask untouched, part-ordering asserted.
- #6: add a formal forks_manifest.yaml entry for the CSGlide Ghost Mask
  idea-only adaptation (was prose-only in another entry's notes; THIRD_PARTY.md
  already carries the full attribution).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 21:30:46 +02:00
ba37ec3d41 feat: LTX Director 2.0.2 fork, keyframe grid-snap, and audio→video bind schedule (#259)
* feat(forks): LTX Director v2_0_2 fork + keyframe latent-grid snap (#258)

Bump the WhatDreamsCost Koolook fork to upstream 2.0.2 (pinned fe09f73) in a
new versions/v2_0_2 namespace, and add `snap_keyframes_to_grid` (default on)
to fix hard-pinned image keyframes "jumping" at latent-bucket edges.

Why: a Director image keyframe is injected as a single latent frame at the
segment's left edge, quantized to the model temporal stride s
(L(f)=0 if f==0 else 1+(f-1)//s). Pins near a bucket boundary round
ambiguously and two pins in one bucket collide. Snapping each pin to its
bucket center removes the ambiguity without changing which latent frame it
lands on; colliding pins are bumped to the next free bucket with a warning.

- forks/whatdreamscost_koolook/versions/v2_0_2/: vendored 2.0.2 sources with
  the v1.3.9 Koolook deltas carried forward (relay_overrides,
  audio_transcript_json, per-segment sigma — build_segments is byte-identical
  1.3.9->2.0.2 so the sigma patch drops in verbatim), plus new pure
  keyframe_grid.py snapper wired into the insert_frames build.
- Canonical LTXDirector__koolook rolls to 2.0.2 (entrypoint loads v2_0_2
  last); LTXDirector__koolook_v1_3_2 alias preserved; v1.3.9 kept on disk.
- LICENSE re-verified GPL-3.0 (2026-06-22). Manifest + THIRD_PARTY + CHANGELOG
  + audio-lipsync findings/CHEATSHEET updated.
- 37 new unit tests for the snapper; 107 fork tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(forks): append Koolook LTX Director widgets last for clean load

Move the v2_0_2 Koolook widgets (relay_overrides, audio_transcript_json,
snap_keyframes_to_grid) to the end of the schema, after override_audio. They
were inserted after img_compression, which shifted the last upstream widget
when an upstream-2.0.x-saved workflow is loaded onto LTXDirector__koolook.
With them last, an upstream graph's positional widgets_values map onto the
upstream widgets untouched and only the Koolook widgets append with defaults
(snap_keyframes_to_grid=on). Execution is unaffected (kwargs by name).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(forks): make v2_0_2 a clean upstream-2.0.2 replica + grid-snap only

Per maintainer direction: the v2_0_2 Koolook LTX Director should faithfully
replicate upstream 2.0.2 (so all latest features work) rather than carry the
older customizations, which are unused.

- Drop relay_overrides, audio_transcript_json, and the per-segment sigma patch.
  prompt_relay.py and patches.py are now vendored VERBATIM from upstream 2.0.2;
  _relay_overrides.py removed.
- ltx_director.py is upstream 2.0.2 verbatim except two additive changes:
  the namespaced node ID LTXDirector__koolook, and the snap_keyframes_to_grid
  latent-bucket keyframe snap (issue #258), appended last in the schema.
- v2_0_2 registers only LTXDirector__koolook; the legacy
  LTXDirector__koolook_v1_3_2 ID stays backed by the v1_3_9 package. v1.3.9
  (with the older customizations) remains on disk for rollback.
- Manifest / THIRD_PARTY / UPSTREAM_PIN / CHANGELOG updated to the clean scope.
  grid-snap unit tests unchanged (107 fork tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(forks): vendor upstream 2.0.2 timeline editor JS for LTXDirector__koolook

The v2_0_2 Python was upstream-exact but the node still rendered with the old
1.3.9-era timeline editor (fewer toolbar actions, no IC-LoRA track, leaked
*_second widgets, frame_rate NaN). Replace web/whatdreamscost_koolook/
ltx_director.js with upstream 2.0.2's js/ltx_director.js vendored verbatim,
retargeted with only two edits: unique extension name 'LTXDirector_Koolook'
and node guard nodeData.name === 'LTXDirector__koolook'. UX now matches
upstream 2.0.2 exactly; only the Python differs (grid-snap).

The old koolook base64-scrub deltas are dropped as obsolete — upstream 2.0.2
persists timeline media via imageFile/audioFile + /api/view URLs (saved
timelines are already lean), and the localStorage quota guard stays in the
global web/koolook_draft_guard.js (upstream does not override localStorage).

Rewrote tests/sidebar/test_ltx_director_draft_guard.py to the new contract
(no embedded guard, retargeted to the koolook node, pinned-upstream
provenance). 110 fork+contract tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(scripts): resolve .env from main repo when dev-sync runs in a worktree

Unrelated to the LTX work: plain `dev-sync` failed with "KOLOOK_COMFYUI_DEV_PATH
not set" from any worktree because scripts/sync_to_dev.py only loaded
`.env` from the worktree root, where the committed `.env` does not exist
(it lives in the main checkout). sync_to_dev_audio.py already had the
worktree->main fallback, which is why dev-sync-audio worked.

Move that fallback into a shared sync_to_dev.find_dotenv() and use it in both
scripts so they can't drift. Adds regression tests for the worktree fallback,
the worktree-own-.env preference, and the not-a-worktree case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: gitignore root-level generated workflow JSONs

Node-swapped/converted workflow copies (e.g. *.koolook.json) are dropped at
the repo root during dev iteration; they're scratch deliverables, not package
content. Ignore root-level *.json only (leading `/`), with a `!config.json`
exception, so the 30+ tracked JSON in subdirs (test fixtures, web/, .claude/,
docs/runs/*/workflow.json) stay tracked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: LTX A/V Bind Schedule node — gate audio→video binding by sigma

New Koolook node LTXAVBindSchedule decouples big motion from lip-sync in LTX
2.3. The AV transformer (comfy/ldm/lightricks/av_model.py, BasicAVTransformerBlock)
adds audio into video via a gated `audio_to_video_attn` per block, run only when
transformer_options["a2v_cross_attn"] is set. This node patches each block's
audio_to_video_attn.forward to scale its output by a per-step gain, and a
unet-function-wrapper computes that gain from the current sigma: early/high-sigma
steps run near `early_gain` (coarse motion settles audio-blind), ramping
(smoothstep) to full at low sigma (lips bind onto settled motion). One pass, no
re-noise — the fix for keyframe transitions jumping on audio peaks.

early_gain=1.0 reproduces stock behavior; a graph without the node is unchanged.
Registered as a root node + added to RUNTIME_PATHS so dev-sync ships it. Pure
ramp (a2v_gain) unit-tested (9 cases); model wiring validated by live render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: LTX A/V Bind Schedule — add transition-window mode (lips synced + smooth)

The global sigma ramp decoupled motion from lips but desynced the mouth (lip
timing is coarse motion too, so holding audio out globally moved the mouth
audio-blind). Add a WINDOWED mode: audio binds at full strength everywhere
(lips sync across the whole clip) EXCEPT a +/-transition_window band around each
keyframe transition, where the sigma ramp applies — so only those moments are
motion-protected early and rejoined late.

Implemented as a per-latent-frame gain on a2v_out: read grid_sizes from
transformer_options to map frame-major video tokens -> latent frames, build a
gain vector (1.0 outside bands, smoothstep-tapered to sigma_gain at each
transition center), expand per-frame -> per-token, scale. New optional inputs:
transition_frames, total_frames, transition_window. Empty transition_frames =
prior global-ramp behavior. windowed_frame_gains unit-tested (7 cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: LTXAVBindSchedule — one-shot diagnostic log (shapes/grid/mode)

Logs out.shape, grid_sizes, transition_fracs, and whether the per-frame mask
fired vs fell back to scalar — so a single render reveals whether the a2v patch
hooks the model and whether the windowed token->frame mapping aligns, instead
of guessing blind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(audio-lipsync): record validated a2v bind-schedule knee (0.35/0.25/0.55)

Promote the finding that LTX 2.3's audio->video coupling must be sigma-ramped
(not full-on) to keep motion smooth AND lips synced, with the validated knee
early_gain=0.35 / bind_start=0.25 / bind_end=0.55 on the 8-step base pass, plus
the trade-axis table and the per-frame-mode caveat (grid_sizes None + appended
guide frames -> scalar fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(forks): keyframe ease — strength-ramped neighbor pins to de-robotic transitions

Adds `keyframe_ease` / `ease_falloff` to the Koolook LTX Director (default off).
Each hard image pin keeps its exact center pose, but emits same-pose neighbor
pins one latent bucket apart at decreasing strength (center*falloff**k), so the
model glides into and out of the locked pose instead of snapping to a dead stop
— softening the "stop/dissolve/stop" robotic feel without softening the pose.

Implemented as a pure list expansion (`expand_keyframe_ease` in keyframe_grid.py,
6 unit tests) applied to guide_data after grid-snap, so the upstream guide node
is untouched (no fork). Center stays exact; neighbors that fall off-clip or onto
another center are skipped. Costs extra guide frames (more compute), hence opt-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(audio-lipsync): downgrade a2v knee to 'promising' — note lows-placement confound

The 0.35/0.25/0.55 win was measured on a clip whose transitions sit on audio
lows, so 'no jumps' may be the placement, not the node. Mark the controlled
on-peak test (early_gain 1.0 vs 0.35 on the same on-peak layout) as still pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: mark vendored WhatDreamsCost fork files linguist-vendored

The koolook Director ships upstream 2.0.2 code (the 8.5k-line timeline-editor JS
+ verbatim prompt_relay/patches) so the pack runs and distributes, but it's
upstream, not authored here. Mark those linguist-vendored (drop from language
stats) and the JS -diff (collapse the large blob in PR diffs). Our edited/new
files stay attributed to us.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx-av): drop unused `import math` (ruff F401)

a2v_gain uses an explicit smoothstep polynomial, not math.*, so the
import was dead. Unblocks the Lint & Smoke CI gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(codeql): advanced setup that excludes vendored upstream code

CodeQL default setup cannot read a config file, so it scanned the
verbatim-vendored WhatDreamsCost editor (web/whatdreamscost_koolook/
ltx_director.js) and raised 4 high-sev DOM-XSS alerts in upstream code we
do not own. Switch to advanced setup with a committed config whose
paths-ignore mirrors the .gitattributes linguist-vendored block, so the
scanner treats vendored upstream as out-of-scope while still fully
scanning our own nodes and sidebar JS.

Languages match the prior default-setup matrix (actions,
javascript-typescript, python).

NOTE: requires disabling "default setup" once in repo Settings before
this workflow can run (GitHub rejects advanced runs while default setup
is enabled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): scope vendored fork exclusions to v2_0_2

* fix(forks): preserve final LTX keyframe snap bucket

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:15:41 +02:00
malkuthro 24857d5e9c Release v0.4.4 (#263)
* Release v0.4.4: funding metadata

* docs: add GitHub footer to v0.4.4 notes
v0.4.4
2026-06-23 23:14:44 +02:00
malkuthro 29052ed369 ci: update actions for Node 24 runtime (#262) 2026-06-23 22:29:34 +02:00
b9a896041b feat(sidebar): add GitHub discovery footer nudge (#261)
Adds a discreet line directly below the update-available footer in the Kforge Labs sidebar: "Help creators find us on GitHub". Gated on the same showUpdateFooter flag so it shows in the live panel but stays out of the read-only A/B compare columns. Ko-fi/funding deliberately stays out of the sidebar (repo-only, tracked as a follow-up).

Co-authored-by: Cristian Predut <cristian.predut@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 21:06:24 +02:00
malkuthro b6b95d8a85 docs: add Ko-fi funding link (#260) 2026-06-23 00:29:00 +02:00
malkuthro 575cfb7f63 Release v0.4.3: publish latest updates (#256) v0.4.3 2026-06-20 11:42:04 +02:00
ba39cb3658 Claude/goofy faraday d23e98 (#255)
* refactor(nodes): drop keyframe_batch alias from Easy Image Batch

The keyframe_batch input was a deprecated alias of keyframes_insert,
added in 0.4.1 during PR #246 review to keep pre-rename workflows
loading. The old name only ever existed in unreleased dev builds before
the rename, so no published workflow depends on it and the alias only
cluttered the node with a second IMAGE input.

Remove the input, its parameter, and the alias-routing fallback; the
node now exposes a single keyframes_insert insert input. Tests and the
behavior spec updated; CHANGELOG notes the removal under [Unreleased].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(policy): make backward compatibility opt-in for Koolook nodes

Invert the default: backward compatibility is no longer a standing
constraint for Koolook-created nodes (the root k_*.py custom nodes).
By default, make the cleanest change — rename/drop/reorder IDs, inputs,
and outputs freely, no deprecation aliases or _v2 suffixes. Compatibility
with saved workflows is only preserved when the maintainer explicitly
says "check backward compatibility".

Forks are explicitly excluded: anything under forks/ (including the
Koolook-original nodes exposed via SKIP_VERSION_SUFFIX) keeps full
back-compat discipline by default, no request needed. The
__koolook_vX_Y_Z collision suffix always applies.

Scoped across the binding instruction files and skills: CLAUDE.md,
copilot-instructions, node-versioning.md, versioning.md, and the
preflight-release / add-external-fork / tdd skills. The preflight
workflows check is reframed as a drift detector for Koolook IDs and a
hard back-compat gate for fork IDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: clarify keyframe_batch alias removal

* fix: retain keyframe_batch compatibility alias

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:16:38 +02:00
472960b919 docs(setups): add publish -> run loop architecture overview (#254)
* docs(setups): add publish -> run loop architecture overview

Entry-point doc that ties the existing published-setup docs together:
the loop in plain terms (Mermaid), a technical map of frontend JS ->
Koolook aiohttp routes/handlers -> ComfyUI engine, and the contract /
schema requirements. All file paths and symbols verified against source.

Diagrams use Mermaid per docs/maintainers/conventions.md. Linked from
the maintainer docs index as the "read this first" entry for the group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(setups): fix publish-run loop references

* docs: codify review-fix push rule

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:04:12 +02:00
malkuthro 543998e026 Release v0.4.2 (#253)
Bump 0.4.1 -> 0.4.2. Ships #251 (Comfy Registry node-list parsing fix) and records #252 (tag-driven automated release pipeline). First release cut via release.yml.
v0.4.2
2026-06-19 09:01:56 +02:00
malkuthro cb19c375c9 feat(ci): tag-triggered release automation (publish + GitHub Release) (#252)
Push a semver tag (vX.Y.Z) at the squash-merge commit on main to publish to the Comfy Registry and create the matching GitHub Release from the CHANGELOG (only on a successful publish). Pre-publish guard refuses any tag that is not on main (merge-base --is-ancestor); per-job least-privilege permissions. publish.yml kept as a manual fallback. Docs synced: releasing.md + release_checklist.md.
2026-06-19 08:49:40 +02:00
malkuthro 3e2b541a61 fix(registry): expose nodes to scanners (#251) 2026-06-19 08:39:44 +02:00
malkuthro bda4df576a Release v0.4.1 (#250)
* Release v0.4.0: update footer notice

* Release v0.4.1
v0.4.1
2026-06-19 01:38:40 +02:00
malkuthro ae7abba73b docs: refresh shipped help and tooltips (#249)
* docs: refresh shipped help and tooltips

* docs: add snapshot transfer guide section

* docs: refresh guide screenshots
2026-06-19 01:01:30 +02:00
88dbfedf86 feat(nodes): Easy Image Batch keyframes_insert + insert-over-source +… (#246)
* feat(nodes): Easy Image Batch keyframes_insert + insert-over-source + clean batch

- Rename optional input keyframe_batch -> keyframes_insert (node ID unchanged).
- Insert-over-source mode: when keyframes_insert AND source_batch are both
  connected, composite the inserts over the source cut window (instead of the
  placeholder background). keyframes_insert alone keeps the offset/reconstruct
  behaviour over placeholder.
- No image source connected -> clean placeholder batch sized by new
  width/height fallback widgets, instead of raising. width/height are appended
  LAST so older saved workflows keep their positional widgets_values alignment
  (fixes a mid-list insertion that shifted placeholder_color into source_frames).
- alpha_batch marks only inserted/placed positions; source background is not
  counted as placed.
- New pure planner plan_source_base_fill (torch-free) alongside
  plan_offset_placements; both unit-tested in CI.
- Docs: behavior matrix spec, interactive HTML demo, and a generated
  "all versions" ComfyUI demo workflow under docs/designs/.

Not yet implemented (specced as DRAFT in docs/designs, pending maintainer
approval + tie-break decision): image1-4 "super-overwrite" layer across all
modes, and removal of the Mode 2 unconnected-slot source_batch fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(deps): raise aiohttp [test] floor to >=3.14.1

The constraints lock already pins aiohttp 3.14.1 (the CVE-2026-54273..54280
fix line), but pyproject's [test] extra still declared aiohttp>=3.9.
Dependabot keys off pyproject, not the lock, so the open declared range kept
eight range-based alerts open even though the installed version is patched.
Raising the floor to >=3.14.1 aligns the declared range with the lock and
clears the alerts once this reaches the default branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): Easy Image Batch image1-4 super-overwrite + source passthrough

Make the four manual slots the top compositing layer in every mode (select
and insert): a connected imageN overwrites output frame imageN_frame on top
of the sequence/inserts and the background. Higher slot wins a collision
(image4 > image1); a slot beats a sequence pick / insert at the same position.
Extracted as the torch-free plan_slot_overwrites planner + a shared
_apply_slot_overwrites helper used by both code paths.

Two model fixes ride along, per the finalised behaviour matrix:
- An unconnected slot now contributes nothing -- imageN_frame is only a
  placement target for a wired image, never a source_batch pick. The old
  unwired-slot fallback (which surfaced seemingly-random frames) is removed.
- source_batch + an empty frame list (no insert) now passes the source cut
  window straight through instead of a placeholder batch, matching the
  insert-mode empty-list passthrough.

Tests: new plan_slot_overwrites planner cases + fake-tensor behaviour tests
covering slot-beats-pick/insert, higher-slot-wins, passthrough-not-placed, and
the removed phantom fallback. Docstring, tooltips, user guide, design spec
(promoted DRAFT -> canonical), and CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(nodes): assert selected_image_batch = picks/inserts + slots; drop matrix HTML demo

selected_image_batch already packs every placed position -- both the
source_frames picks / inserts and the manual-slot overwrites, with slots
winning on overlap. Add explicit select- and insert-mode tests asserting the
composition and ascending order, locking the behaviour in.

Also delete docs/designs/easy-image-batch-matrix.html (stale interactive demo
built against an earlier draft) and drop its companion reference from the
design spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): Easy Image Batch passthrough inpaint mask + frame-range syntax

Two refinements to the Easy Image Batch passthrough/list behaviour:

- Short-source passthrough is now an extend/inpaint mask. When source_batch is
  connected and the frame list is empty, the covered source frames are kept
  content (alpha 0.0) and the uncovered tail becomes the selection
  (selected_image_batch / selected_frames) with alpha 1.0 -- "keep the first N,
  generate the rest". Full coverage -> no gap, empty selection. The selection
  is the gap here (inverse of select mode); insert mode is unchanged.
- The source_frames field accepts inclusive ranges: "1-5, 7, 9, 14-17" expands
  to 1,2,3,4,5,7,9,14,15,16,17, in both select and insert, via a shared
  torch-free parse_frame_tokens (descending/non-integer tokens warn).

Tests cover the gap selection (short/full/cut-start-offset/slot-in-gap), the
parser (ranges, bad tokens), and range syntax end-to-end. Docstring, tooltip,
user guide, design spec, and CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(designs): remove draft-era Easy Image Batch demo workflow + builder

Delete easy-image-batch-demo-setup.json and build_easy_image_batch_demo.py
(the leftover draft-era demo, sibling to the already-removed matrix HTML) and
drop the companion reference from the behavior spec. A purpose-built demo will
replace them later; the spec matrix + the node remain the canonical reference.
Also drop the now-stale "(proposed)" from the spec title (it is canonical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(user-guide): remove stale v0.1.5 Easy Image Batch screenshot

The screenshot predates the node's keyframes_insert / super-overwrite /
passthrough rework and is unreferenced. Removing it to be re-created fresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: point Easy Image Batch docs at the bundled Select-and-Rebuild demo

Note the runnable demo workflow ("Easy Image Batch - Select and Rebuild",
bundled into the Kforge Labs Workflows sidebar folder via #248) in both the
user guide and the design spec, replacing the generated demo removed earlier.
Referenced by path (not a clickable link) since the workflow lands via #248.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(nodes): address PR review — keyframe_batch alias + input guards

- Restore keyframe_batch as a deprecated alias of keyframes_insert so
  pre-rename workflows (origin/main exposes the input) keep loading; an explicit
  keyframes_insert wins. Follows docs/maintainers/node-versioning.md
  (alias-then-deprecate); removal deferred to a release boundary.
- Warn instead of silently returning a clean batch when source_frames is
  non-empty but no image source is connected.
- Cap frame-list range expansion at 8192 frames/token to guard against typos
  like 1-99999999.
- Docs: reframe the bundled demo-workflow note as forthcoming (the JSON lands
  via #248, not on this branch); document the alias, cap, and no-source guard in
  the user guide / design spec / CHANGELOG.

Tests: alias routing + precedence, no-source warning, range cap + boundary
(46 node tests). Fresh-eyes review: no correctness/compat issues (the IMAGE
alias input adds no widget-position shift).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: point Easy Image Batch docs at the now-merged Select-and-Rebuild demo

#248 merged to main and is now in this branch (via the main merge), so the
demo workflow exists at docs/workflows/kforge-labs-workflows/. Repoint the user
guide and design spec at it directly (present tense, real folder link) instead
of the earlier "coming soon / pending" framing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:51:16 +02:00
malkuthro 4383c2fc0b feat(sidebar): bundle Kforge demo workflows (#248) 2026-06-19 00:24:08 +02:00
6f158c9020 fix(nodes): Easy_LoadVideo empty-branch degrade (bundles #243 publish-tab cleanup) (#244)
* docs(setups): add setup runner quickstart runbook

Standalone reference for opening the published-setup mockup frontend
(setup runner simulator): the `<comfy-address>/koolook/setup_runner_simulator.html`
URL pattern, the live-registry vs setup-file (inspect-only) load paths,
and demo mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sidebar): close throwaway workflow tabs left by publish capture

Publishing a setup captures ComfyUI's API-format prompt by loading the
graph onto the canvas (graphToPrompt) and reloading the prior canvas.
In current Comfy each loadGraphData spawns an "Unsaved Workflow" tab, so
every publish left two stray tabs behind.

captureWorkflowApiPrompt now snapshots the open workflows before capture
and, afterward, re-activates the user's original tab and closes whatever
the capture opened, via app.extensionManager.workflow. All of it is
guarded and best-effort: the workflow-store API is Comfy-version
specific, so if it is absent capture still succeeds and behavior falls
back to today (no crash, no broken publish).

Covered by a source-pattern test. Needs live verification against the
1.45 frontend (dev-sync) to confirm the closeWorkflow API shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(nodes): Easy_LoadVideo degrades to empty on an unselected folder branch

When an upstream mode switch routes a path into Easy_LoadVideo, both the
EXR sequence loader and the video loader execute eagerly. On the EXR
branch the video loader was handed a folder (no filename) and raised
ValueError, crashing the whole prompt -- even though that branch is
discarded downstream. Lenient sequence loaders just ignore a wrong-type
path; the video loader was the only one that hard-failed.

_compose_input_video_path now returns EMPTY_BRANCH_SENTINEL instead of
raising when input_path resolves to an existing directory. load_video
turns that into a correctly-shaped empty result (IMAGE zeros,
frame_count 0, empty AUDIO, empty video_info) matching VHS_LoadVideoPath
returns, so the dead branch no longer crashes the prompt. VALIDATE_INPUTS
and IS_CHANGED handle the sentinel too. A path that does not exist is
still a genuine mistake and keeps raising.

torch is imported lazily in the empty-result builder so the path logic
stays importable/testable without torch.

Covered by unit tests for the sentinel (absolute + relative dirs), the
still-raises case, and a load_video integration test that asserts the
empty result and that the loader never runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(setups): expose prompt as an always-on field in the publish frontend

The publish surface modelled `prompt` as a 4th source mode (mutually
exclusive with EXR/QT/Img) and hid it (visible:false), so a published
setup couldn't offer "pick a source AND describe the shot". This makes
prompt a standalone, always-on input.

- published_surface.js: prompt becomes visible + standalone + multiline,
  rendered independently of the source switch; the author's prompt-widget
  text is surfaced as a `placeholder` hint (submitted default is empty so
  an untouched hint is never sent), with a `help` line. The switch
  excludes standalone fields from its source options.
- setup_runner_simulator.js: renders always-on standalone inputs alongside
  the selected source (textarea + placeholder + help), includes them in the
  run payload, and updates the demo setup.
- setup_runner_simulator.html: .field-help styling.
- contract doc: documents the always-on prompt behaviour.
- launch.json: portability fix (hardcoded C:\Python313 -> python).

Contract test updated + a new test asserting prompt is always-on, carries
the placeholder, and is excluded from the switch source options (runs on
CI where Node is available). Behaviour verified in the runner simulator
(demo mode) in a browser: textarea + hint, EXR/QT/Img-only switch, prompt
stays visible across source changes, payload includes the typed prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): mirror always-on prompt field in the Python publish inference

The publish surface is inferred server-side in Python (koolook_setups.py
_infer_app_surface, called by publishSetup), which overrides the client.
The earlier change only updated the JS (published_surface.js), so
re-publishing still produced the old surface with a hidden prompt.

Mirror the JS here: prompt is a standalone, always-on input (visible +
multiline) with the author's prompt-widget text as a placeholder hint and
an empty submitted default; it is excluded from the source switch options.
Keeps koolook_setups.py and published_surface.js in sync.

Registry test updated to the new shape. Server + publish-contract suites
green (115); full non-sidebar suite green (530).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): keep the runner simulator polling through long renders

The poller used a fixed 30s total budget, so a multi-minute render (e.g.
428s) timed out mid-run with "Timed out waiting for run" even though
ComfyUI was still rendering and the run later landed as succeeded.

timeoutMs is now a "no-progress" window: while ComfyUI keeps reporting the
run as queued/running, the deadline is pushed forward, so a long-but-active
render never spuriously times out; only a stalled/vanished run gives up.
Default raised 30s -> 120s. The run status line shows elapsed seconds while
active so a long wait visibly ticks instead of looking frozen.

Happy path (queued -> running -> succeeded) unchanged; verified in the
runner simulator (demo mode) in a browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): serve simulator assets with no-store so dev-syncs take effect

The runner-simulator HTML/JS route returned no cache headers, so browsers
cached the assets and kept running stale JS after a dev-sync -- e.g. the
old 30s poller persisted client-side even though the server was serving
the long-render fix, which looked like "the fix didn't land". Serve these
dev-synced assets with Cache-Control: no-store so a normal reload always
picks up the current code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(setups): add live run/poll smoke check for the published-setup API

tools/check_setup_run.py queues a published setup through the same routes
the runner simulator uses and polls the run endpoint with the same
deadline-extension logic, printing the status timeline. Lets us self-verify
end-to-end run tracking against a live ComfyUI (e.g. "does a long render
reach succeeded?") without driving the browser UI.

Used it to confirm the run/poll server flow is correct (terminal status
detected in ~2s; run-000001 reported succeeded), isolating the simulator's
earlier "Timed out" to stale browser cache rather than a server bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): Koolook_NextVersion auto-detects the next output version

Adds filesystem-aware version auto-detection so a re-run lands on a fresh
version instead of failing with "Output file already exists and overwrite
is disabled" (the real EasyAIPipeline failure seen on the RMGB setup).

- koolook_versioning.next_version_token(directory, name, ...): scans for
  existing <name>_vNNN files AND sequence-folders (handles name_v002.png,
  name_v002.0001.exr, name_v003/), returns highest+1 zero-padded; missing/
  empty dir -> start (v001); exact base-name match so other shots never
  bump this one; empty name matches bare vNNN; configurable prefix/padding.
- Koolook_NextVersion node (Koolook/Publish): folder + name -> next version
  token. Wire once into both the EXR and video version inputs so branches
  stay in sync. IS_CHANGED returns NaN to re-scan every run.

Tested: helper edge cases + node registration/run. Preflight clean (20
node IDs). Full non-sidebar suite green (539).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): 'auto' version keyword for EasyAIPipeline + Easy_VideoCombine

No new node or wiring needed: type 'auto' (or 'next') in the version field
and the node scans its own output folder for existing <name>_vNNN entries
and writes the next free version -- fixing "Output file already exists and
overwrite is disabled" without manual version bumps.

- koolook_versioning.is_auto_version(): detects the auto/next keyword.
- EasyAIPipeline (build_pipeline_outputs): on auto, scans the version-folder
  parent (base, or base/shot/method when subfolders are on) for vNNN dirs and
  picks the next; version computed after path segments so the scan dir is known.
- Easy_VideoCombine: on auto, resolves the token after the output dir + name
  root are composed, scanning that dir for <name>_vNNN outputs.
- Non-auto/typed/wired tokens and disable_versioning are unchanged.

Tests: is_auto_version keywords + EasyAIPipeline auto (v003 after v001/v002,
v001 first run). Full non-sidebar suite green (542), preflight clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(nodes): drop redundant Koolook_NextVersion node

The 'auto' version keyword on EasyAIPipeline + Easy_VideoCombine covers
auto-detection without any new node or wiring, so the standalone
Koolook_NextVersion node is redundant. Remove it (and its node test) to keep
the node list clean; the shared next_version_token helper stays (the 'auto'
keyword uses it).

Node-ID count back to 19; full non-sidebar suite green (541), preflight clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tools): satisfy security gate for setup smoke check

* fix(pr244): address verified review findings

* fix(video): fail if relative auto-version scan root is unavailable

---------

Co-authored-by: Cristian Predut <cristian.predut@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
2026-06-19 00:10:59 +02:00
malkuthro e165c96fcc fix(ci): enforce dependency audit gate (#247) 2026-06-18 19:40:31 +02:00
malkuthro 30d51cf365 fix(loop): harden Koolook_LoopStatus auto-queue (#241)
Auto server_url detection (widget defaults to 'auto'); the connected index wire is authoritative for frame-index detection (a stale numeric label no longer overrides it); fold in #207 (string-bool auto_queue_next + detected index-node logging); review hardening (IPv6 / bare --listen, Bandit B104, blank server_url, debug diagnostics).
2026-06-18 18:16:08 +02:00
malkuthroandClaude Opus 4.8 df34ddcf1a feat(nodes): Easy Image Batch offset mode via keyframe_batch input (#242)
Add the inverse of the node's select behaviour. When the new optional
keyframe_batch IMAGE input is connected, the node scatters a packed
sequence back onto the source_frames positions (i-th frame -> i-th listed
number, ascending), placeholder fill in between, at total_frames length.
This closes the select -> process -> reconstruct loop: feed a processed
selected_image_batch plus the same selected_frames list back in to rebuild
the original spacing.

The switch is wire-driven (source_batch and image1-4 are ignored while
keyframe_batch is connected, with a console note); count mismatches and
outside-cut positions are reported in the end-of-run summary. Placement
logic is a pure, torch-free planner (plan_offset_placements) so it is
unit-tested in CI, where torch is unavailable. Fully backward-compatible:
unconnected keyframe_batch preserves the existing select path exactly.

Tests: 10 new torch-free tests; full suite 630 passed, 1 skipped; preflight_release green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:45:10 +02:00
malkuthroandClaude Opus 4.8 ae2480eee9 feat(setups): publish success feedback + published-setup discoverability (#227) (#239)
* feat(setups): publish success feedback + published-setup discoverability (#227)

Post-publish feedback (issue #227 Part 1):
- POST /koolook/api/setups now returns the on-disk storagePath
- new POST /koolook/api/setups/reveal opens the published-setups folder
  (shared _open_dir_in_file_manager helper; the snapshot reveal route was
  refactored onto it, behaviour-identical)
- the publish modal renders a success card reusing the snapshot Save dialog's
  "Saved to ... Open folder" language (Open folder / Copy path / Close)
  instead of closing silently; a draft publish still gets a confirmation

Published discoverability (reuses the tag system, no new schema):
- publishing auto-tags the source workflow `published` -> a "published" badge
  in the Workflows tree + the Tags-section "published" pool
- a new "P" toggle in the sidebar Tools row prunes the Workflows tree to
  published-only, folders force-expanded, so setups show in their origin
  structure; toggling off restores the full tree
- workflowHasTag() shared helper (unit-tested)

Deferred, still open on #227: separate destination/library setting (Part 2),
relocating the storage folder beside the snapshot library, and clearing the
filter when entering Compare mode.

Tests: +server (storagePath, reveal route happy + 404), +client (reveal
client, buildPublishSuccessView view-model), +sidebar (workflowHasTag).
Full suite: 610 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(setups): store published setups beside the snapshot library (#227)

- the published-setup registry path now derives from the configured snapshot
  library (`_configured_dir().parent / koolook-published-setups/setups.json`)
  instead of a fixed user-dir folder, so it follows libraryPath /
  KFORGELABS_PRESETS. The default config resolves to the same path as before
  (target == legacy), so existing default installs are unchanged.
- non-destructive one-time migration: copy a legacy user-dir registry to the
  new location when the new one is absent, leaving the original in place.
- sidebar: clear the "P" published filter when entering Compare mode so it
  doesn't render both compare columns filtered with no toggle to undo it.

Tests: +tests/server/test_published_setups_relocation.py (path beside library;
migration copy / no-op-when-present / no-op-when-no-legacy). Full suite: 614
passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(tests): remove unused pathlib.Path import (ruff F401)

The relocation test only uses the `tmp_path` fixture, not `Path` directly, so
`from pathlib import Path` was unused and failed the Lint & Smoke (ruff) gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(setups): address PR #239 review — migration masking + tag persist

- HIGH: a failed legacy->new migration no longer masks real setups with the
  bundled sample. The default registry keeps the legacy file as its primary
  storage when migration could not produce the new file, so stranded data
  stays visible. (+2 tests: keeps-legacy-on-copy-failure / uses-new-on-success)
- MEDIUM/LOW: the publish flow now awaits the `published`-tag persist so the
  success card reflects a committed tag (persist failures already surface via
  persistMutation's critical toast), and warns when the source workflow could
  not be tagged (vanished mid-publish) rather than on a harmless republish.

Full suite: 616 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(setups): harden migration against partial/corrupt relocation target

Addresses the remaining HIGH from the PR #239 consolidated re-review: a
zero-byte/partial/corrupt setups.json at the relocated path could shadow the
intact legacy registry (existence was treated as proof of migration).

- migration now copies via temp file + atomic os.replace (the same pattern
  FileSetupStorage.save_setups uses), so an interrupted copy can never leave
  a partial setups.json at the target; failed-copy temp files are cleaned up.
- primary selection is corruption-aware: when the relocated file exists but
  does not parse as a setups document while the legacy file is intact, the
  registry keeps serving the legacy file and logs a loud diagnostic. The
  unreadable file is left untouched (never overwritten) as evidence. A valid
  relocated file stays authoritative; if both are corrupt, the relocated path
  stays primary so storage diagnostics point at the current location.

Tests (+4): corrupt-target-prefers-intact-legacy (file left untouched),
valid-target-stays-authoritative, both-corrupt-keeps-target, and
interrupted-copy-leaves-no-partial-target-and-no-tmp-litter.

Full suite: 620 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:54:12 +02:00
malkuthro 91cee1daae docs(setups): add external frontend kit (#236)
External frontend kit handoff for published-setup consumers; refreshes full-circle/status docs and neutralizes stale current-PR apiPrompt wording per review #4485848881.
2026-06-12 16:43:51 +02:00
malkuthroandClaude Fable 5 c6a155a033 fix(web): generation-agnostic Comfy draft-quota guard (#237)
* fix(web): generation-agnostic Comfy draft-quota guard

"Failed to save workflow draft" toasts returned on ComfyUI frontend
1.44+ because draft storage moved to new localStorage key names
(per-workspace V1 keys, then a V2 index+payload scheme) that the old
exact-name guard embedded in ltx_director.js never matched - and the
1.44 V2 layer latches a session-wide storage-unavailable flag after one
failed save+evict cycle, re-toasting on every edit until reload.

Move the guard to its own global extension, web/koolook_draft_guard.js:

- match draft keys by prefix (Comfy.Workflow.Draft) across all known
  generations, with unknown future keys as last-resort evictions
- rescue quota failures inside localStorage.setItem (evict the oldest
  draft across generations, retry) before Comfy's own catch sees the
  error, so the save succeeds and the latch never trips
- boot prune: drop the suffixed-V1 dead weight the frontend's V1->V2
  migration never deletes, scope corrupt-key cleanup to the offending
  key, drop oversized entries, enforce a total draft budget
- ship the guard with dev-sync-audio so a scoped sync cannot strand a
  dev install guard-less

Behavior is covered by tests/js/test_draft_guard.mjs (12 scenarios,
wired into CI next to the existing node smoke test); static integration
contracts live in tests/sidebar/test_draft_guard.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): address deep-review findings on the draft guard

- latch the "still full" hint toast once per session so a genuinely
  full origin cannot re-toast on repeated failing autosave attempts;
  regression assertion added to the exhaustion scenario
- document the bounded-eviction termination contract and the
  post-rethrow storage-unavailable latch at the wrapper, plus the
  budget's V1-blob-only (1.5M) -> all-generations (2M) scope widening
  (also now stated in the CHANGELOG entry)
- version-robust wording for the suffixed-V1 cleanup rationale: the
  shipped 1.44 migration leaves the keys behind (verified against the
  installed 1.44.19 dist - no removeItem exists there), later
  frontends clean up on successful migration, and an interrupted
  migration can leak the keys on any version
- comment the intentional best-effort swallow in the toast helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:29:25 +02:00
malkuthroandClaude Fable 5 5da590120c fix(tests): canonicalize expected writer filepath on Windows (#238)
* fix(tests): canonicalize expected writer filepath on Windows

build_pipeline_outputs canonicalizes output paths to forward slashes on
every platform, but the execution-map writer test built its expectation
from str(tmp_path), which yields backslashes on Windows — failing there
while ubuntu CI stayed green. Normalize the expected prefix the same way
test_easy_ai_pipeline.py already does. No-op on POSIX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: run pytest job on a windows-latest matrix lane

The suite is now Windows-clean (previous commit), but only ubuntu ran in
CI - the path-separator regression this PR fixes was invisible until a
local Windows run. Add windows-latest to the pytest job so that class of
break gates PRs. fail-fast off so a Windows-only failure still reports
alongside the green ubuntu lane.

Safe rename: Protect-main requires only "Lint & Smoke" and "Security
Gate", so Pytest -> "Pytest (os)" breaks no required check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:41:42 +02:00
malkuthro 1732030175 fix(setups): resolve switch-selected run results (#235)
* fix(setups): resolve switch-selected run results

* fix(setups): harden switch result reporting

* fix(setups): reject stale execution map writers
2026-06-09 00:27:45 +02:00
malkuthro c3c34f0171 Capture API prompt when publishing setups (#233)
* feat(sidebar): capture API prompt when publishing setups

* feat(setups): preview and load canonical setup records

* fix(setups): close publish review gaps
2026-06-08 20:28:35 +02:00
malkuthro 85206ab321 feat(setups): add external runner simulator (#231)
* feat(setups): add external runner simulator

* feat(setups): render app-surface simulator form

* docs(setups): capture publish-to-run frontend plan

* docs(setups): preserve sidebar inventory breadcrumbs

* fix(setups): address runner simulator review findings

* fix(setups): validate simulator run payload shape
2026-06-08 08:30:10 +02:00
malkuthro 16c1b42760 Add published setup execution API (#230)
* feat(setups): add published setup execution API

* fix(setups): expose app surface run results

* test(setups): harden execution API failures
2026-06-07 23:08:12 +02:00
malkuthro 83b170bdae feat(setups): add publish contract nodes (#228)
* feat(setups): infer app contract from named fields

* feat(setups): add publish contract nodes

* feat(setups): split publish result contract

* fix(setups): resolve publish output reroutes in preview

* fix(setups): validate app surface on read

* fix(setups): harden app contract validation
2026-06-07 22:02:16 +02:00
malkuthro 7cffe0702e fix(setups): publish simple widget-only workflows (#223)
* fix(setups): publish simple widget-only workflows

* fix(setups): read subgraph and named widget defaults

* fix(setups): allow optional publish descriptions

* fix(setups): reject incomplete widget-only values
2026-06-07 21:50:54 +02:00
malkuthro 3b35e6a195 feat(setups): infer setup surface from workflow groups (#222)
* feat(setups): infer setup surface from workflow groups

* fix(setups): validate inferred setup surfaces on read

* fix(setups): reject boolean setup geometry
2026-06-07 14:45:16 +02:00
malkuthro b4314f3b3d feat(sidebar): preserve workflow groups in saved selections (#220) 2026-06-07 12:41:14 +02:00
malkuthro 98c204b119 feat(setups): convert visual workflows to API prompts (#217)
* feat(setups): convert visual workflows to api prompts

* fix(setups): reject corrupt visual graph links
2026-06-07 11:36:35 +02:00
malkuthro 16023662e6 feat(setups): publish sidebar workflows (#216) 2026-06-07 11:07:52 +02:00
malkuthro 1cf2fd31a2 feat(setups): add published setup catalog API (#214)
* feat(setups): add published setup catalog API

* fix(setups): report registry storage diagnostics
2026-06-06 16:12:21 +02:00
malkuthro df1d66402a fix(deps): relock test aiohttp (#215)
* fix(deps): relock test aiohttp

* docs: clarify review approval default
2026-06-06 15:50:30 +02:00
malkuthro 8c8546c17c Add queue-driven loop status controller (#205)
* Add queue-driven loop status node

* Document ComfyUI server API testing

* Harden loop queue controller

* Guard loop controller HTTP requests
2026-06-03 12:34:15 +02:00
malkuthroandClaude Opus 4.8 38d1f8425d feat(snapshot): Compare Phase 2 — bidirectional path-preserving copy + folder merge + diff filter (#197) (#202)
* feat(snapshot): Compare Phase 2 — bidirectional path-preserving copy + folder merge + diff filter (#197)

Makes the read-only A/B Compare view (live kit A ↔ loaded snapshot B) fully
actionable, in both directions, with orientation signage and a diff browser.

Copy. Right-click a node/workflow on the read-only SOURCE side → "Copy to
<target>". Path-preserving: items land at the SAME folder path they came
from (new copyWorkflowIntoStore auto-creates/merges dirs, deep-clones the
graph, applies a skip-identical / keep-both collision policy; tags + module
ride along). Right-click a folder → "Copy folder (with contents)" bulk-copies
everything under it (the Workflows root copies the whole tree — merging two
snapshots) with an added/kept-both/already-there summary. Archived versions
are excluded from the diff and from bulk copy.

Both directions. B→A writes the live kit via persistMutation (Save round-
trips it). A→B writes back into the snapshot FILE on disk (writePreset),
targeting the exact file/dir it was loaded from (autosave dir threaded
through onChoose). The A→B path is serialized + epoch-guarded: it re-clones
the latest snapshot inside the queued task and only swaps the in-memory
snapshot in after a successful write while the same Compare session is open
(no resurrecting a closed view, no concurrent-copy lost update).

Orientation + diff browser. Each column shows a SOURCE/TARGET footer stripe;
the swap flips which side is the editable target and moves the read-only
guard with it. The bottom green/red legend doubles as filters (new/modified)
that collapse the SOURCE panel to just those items and back. The highlight,
counts, and filter all follow the source side and mean "in the source, not
the target" (the copy candidates).

The two panels keep independent folder-expansion maps so a copy's re-render
never collapses the other side; copies open the destination folder (not the
Archive child) so you can see where they landed.

Non-destructive throughout: no applySnapshot on the Compare path; the read-
only side only writes through the explicit copy affordance; graphs are deep-
cloned so neither store aliases the other.

Tests: copy-engine behaviour (path create/merge, skip/keep-both, deep-clone,
bulk folder, archived exclusion) + structural invariants (read-only guard,
bidirectional additive copy, path-preserving, serialized/epoch-guarded
write-back, source-following diff, panel-isolated expansion, folder pull-in).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(snapshot): guard the Compare copy engine against prototype pollution (#197, #203)

copyWorkflowIntoStore writes destination keys from user-controlled workflow
names / directory segments (a loaded snapshot can be hand-crafted), so reject
__proto__ / constructor / prototype via the same isSafeObjectKey() guard the
other name-keyed store mutators adopted in #203. copyFolderIntoStore inherits
the guard through it. Added after rebasing onto main so the new merge surface
matches the hardened sinks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(snapshot): Compare A→B is an in-memory working copy with an explicit Save (#197)

A→B no longer writes to disk per copy. The loaded snapshot (B) is treated as
an in-memory working copy: copying into it mutates `compareSnapshot` and marks
it dirty — no writePreset, so a rotating autosave (which "Load latest" can
hand you) is never a write target. The user Saves explicitly via a footer
button: writePreset to a NAMED snapshot in the default library dir, pre-filled
with the shadowed/loaded name (compareDefaultSaveName strips the `_autosave`
suffix to reach the named parent). Exiting Compare with unsaved edits prompts
to discard first.

Each column footer now has two lines: line 1 = SOURCE/TARGET + name; line 2 =
save behavior — "Live — changes auto-save" for your kit, and for the snapshot
either "Read-only file", "edits need Save", or "● Not saved" + a Save button
(shown whenever the snapshot is dirty, so it survives a swap). The word "live"
disappears from the UI; the footer carries the meaning.

Removes the per-copy persistSnapshotEdit + compareEpoch + snapshotWriteChain
machinery (no longer needed — there's a single explicit Save). B→A is
unchanged (auto-persists into the live kit). Non-destructive guarantees hold:
no applySnapshot; sources are clones; the snapshot panel stays read-only apart
from the pull-in. Fresh-eyes review: no Critical/Important findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(snapshot): Compare bottom = two separate stripes; clean autosave name (#197)

Per maintainer feedback, split the bottom into two distinct stripes instead
of cramming save state into the SOURCE/TARGET footers:
  - Stripe 1: the SOURCE/TARGET footer under each column — orientation only.
  - Stripe 2: a dedicated, full-width SAVE stripe with the snapshot's save
    state, instructions, the dirty indicator, and the Save button.

Also fix the long autosave name: an autosave-loaded target showed the raw
`Foo_autosave/pre_load_...` path in the header and footer. compareSnapName()
now derives a clean name (the named parent + " · autosave"); the Save button
reads "Save as file…" and still pre-fills the named parent so A->B merges
land in a named file, never the rotating autosave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(snapshot): guard Compare snapshot Save against silent overwrite (#197)

PR #202 review (data-safety): saveCompareSnapshot wrote `writePreset(name, …)`
with no existence check and no overwrite confirm. Since the Save name is
pre-filled with the snapshot's own name and the server route ends in an atomic
os.replace, accepting the default could silently overwrite a DIFFERENT
same-named snapshot file. Now mirrors the live Save flow (modals.js
promptForName): sanitizeName the input, presetExists() → abort on null
(library unreachable) → confirm on true (Overwrite?) → write only on false.
Adds a `compareSaving` in-flight guard so rapid Save clicks can't fire
overlapping writePreset POSTs.

Also dedup: copyWorkflowIntoStore's collision check now reuses the existing
`normalizedStoresEqual` instead of the byte-identical local `graphsEqual`
helper (removed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:26:26 +02:00
malkuthro 7f8c09ad29 feat(sidebar): clean up workflow archives (#201) 2026-06-02 22:25:09 +02:00
05f94cbd19 feat(scripts): add make_silent_wav.py for no-audio LTX graphs (#196)
Generates a silent WAV (stereo, 44.1 kHz, 16-bit by default) matching the
AUDIO format ComfyUI/LTX-2.3 expect, so a required AUDIO input can be
satisfied via the stock LoadAudio node when no real audio is available —
the same waveform the LTX Director produces internally.

stdlib only (wave module); CLI flags for duration/rate/channels.

Co-authored-by: Cristian Predut <cristian.predut@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:36:18 +02:00
malkuthroandClaude Opus 4.8 f7e0666cc0 fix(sidebar): guard name-keyed store mutators against prototype pollution (#203)
CodeQL js/prototype-polluting-assignment (alert #1, medium) flagged
`dir.workflows[wfName].archived = true` in archiveWorkflow: a "__proto__"
name resolves to Object.prototype, passes the truthy existence check, and
the assignment pollutes the prototype process-wide. The same shape exists
across every exported mutator that uses an untrusted directory/workflow
name as an object key.

Add a shared isSafeObjectKey() denylist guard (__proto__, constructor,
prototype) with an early `return false` at all 12 keyed-mutation sinks:
add/rename/delete/move Directory, save/archive/unarchive/rename/delete/move
Workflow, and add/removeTag. Read-only accessors and clearArchive (which
derives names from own entries) are left untouched — reads don't pollute.

Supersedes the Copilot autofix draft #167 (extends it from a partial set
to every sink). Validated with `node --check`; repo has no JS test runner.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:33:27 +02:00
malkuthro 67e9d55e98 docs(nodes): add user guides and tooltips (#200)
* docs(nodes): add user guides and tooltips

* chore: clean up node preflight nits
2026-06-02 20:42:56 +02:00
malkuthroandClaude Opus 4.8 447465ecd8 refactor(nodes): remove superseded LTX Director extra nodes + harden registry (#198) (#199)
* refactor(nodes): remove superseded LTX Director extra nodes + harden registry (#198)

Remove the two experimental nodes layered on the WhatDreamsCost fork — KoolookAudioTranscriptTimeline (k_audio_timeline.py) and KoolookTimelineEditor (k_timeline_editor.py) — plus the transcribe_audio_timeline.py CLI and its test. The audio-transcript to Director-timing job now lives inside the fork via the Director's audio_transcript_json input, so the standalone nodes are redundant. Also drops the orphaned [audio]/faster-whisper extra and updates packaging, dev-sync, web, and docs references.

Harden the node registry against the #183 failure: __init__.py now imports each node group independently and installs the /koolook/presets/* routes regardless of node-import results, so one missing or broken module degrades to 'that node absent' instead of zeroing every node and 404'ing the sidebar disk routes. A new test keeps every root k_*.py listed in dev-sync's RUNTIME_PATHS.

Closes #198.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(nodes): ship loader gates via dev-sync-audio; generalize RUNTIME_PATHS guard (#198)

Address the K-Flame review on #199:

MEDIUM — the #198 refactor made koolook_versioning.py a single registration gate, but dev-sync-audio shipped the new __init__.py without it (and the k_*.py-glob guard test couldn't see it). Add koolook_versioning.py AND koolook_install_guard.py (whose absolute-import fallback raises uncaught) to AUDIO_PATHS so a scoped sync can never leave the loader pointing at a module the target lacks.

Generalize the guard test: parse __init__.py's actual load-time import targets (relative imports + _merge_node_group calls) and assert each is in RUNTIME_PATHS, instead of globbing k_*.py which missed the non-k_ gates. Add an AUDIO_PATHS gate-coverage test.

LOW — CHANGELOG now records the [audio]/faster-whisper + transcribe_audio_timeline.py removal as an external-contract break.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:47:25 +02:00
8058e17456 fix(snapshot): paired drift-corruption fix — duplicate-install guard + boot-time mismatch (#161, #162) (#186)
* fix(snapshot): prevent drift corruption from duplicate installs + boot-time mismatch (#161, #162)

Closes #162 and #161 as the paired fix the issues describe — both touch
the same data-loss surface and verify against the same dev-sync rig.

installs at import time, log critical, register nothing in the
alphabetical loser; client-side `window` sentinel as belt-and-suspenders;
align .env.example / dev-iteration-loop / sync_to_dev / README at the
Registry-derived `koolook/` path.

compare against live /userdata + picks, set a drift flag on mismatch;
gate periodic + pre-load autosave routing to `_unsaved_autosave/` while
drifted; promote status pill to "drifted" precedence over "saved" with a
recovery tooltip; auto-clear on the next markStateSaved (Save / Load).

26 new tests (14 install guard, 4 sidebar sentinel, 8 boot drift) —
316 total passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snapshot): apply review fixes — canonical fingerprint + early loser exit + drift timestamp

Review-driven follow-ups on PR #186:

1. **Symmetrize file-side fingerprint (MEDIUM).** `detectBootDrift`
   now uses a new `_canonicalStateFingerprint()` that normalizes the
   workflows tree through `normalizeWorkflowsStore` AND deep-sorts keys
   via `sortJsonValue`. Without this, hand-edited / cross-machine /
   pre-v0.3 files would false-positive as drifted on first boot
   (e.g. `archived: "false"` string vs `false` bool, missing
   `directories: {}`, key insertion-order asymmetry from the
   `{...wf, archived, module, tags}` spread). `_computeFingerprint`
   stays unchanged so the localStorage save baseline contract holds
   for existing users. New regression test verifies a shape-only diff
   does NOT trigger drift.

2. **Gate fork imports on `_is_winning_install` (LOW).** Non-winning
   duplicate installs now short-circuit before the heavy torch / PIL /
   diffusers / aiohttp imports, exiting in ~5 LoC instead of paying
   full import cost just to discard the mappings.

3. **Drift timestamp uses `driftDetectedAt`, not stale
   `lastNamedSaveAt` (LOW).** The localStorage save baseline could be
   from a prior session whose live state was already corrupt —
   surfacing it as "Last named save" in the drift tooltip would
   mislead. Replaced with the session-noticed-at timestamp from
   `_bootDriftDiagnostics`.

352 tests passing (316 prior + 26 from PR #186 + 1 new normalize-
symmetry case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install-guard): fail safe on unreadable / non-UTF-8 siblings (#162)

Re-review follow-up. The import-time duplicate-install scan could crash
the entire plugin load (fail-silent — the alphabetical winner vanished
too, not just the loser) on two unguarded paths:

- detect_duplicate_koolook_installs: the per-entry is_dir()/is_file()
  over custom_nodes/ siblings was unguarded, so a non-traversable
  sibling (restrictive perms, broken mount, a special dir) raised
  PermissionError up into __init__.py. No duplicate even required.
  Now caught per-entry and skipped.
- read_pyproject_version: read_text(encoding="utf-8") raises
  UnicodeDecodeError (a ValueError, NOT OSError) on a non-UTF-8 /
  binary pyproject.toml, escaping the `except OSError`. Now degrades
  to "?" via `except (OSError, ValueError)`.
- __init__.py: wrapped the guard call site as a final backstop — any
  unforeseen guard failure now degrades to register-normally rather
  than aborting the import.

Both paths reproduced before the fix; the first surfaced while running
the suite from a /tmp checkout whose parent held a root-owned dir.

3 regression tests added (non-traversable sibling skipped; non-UTF-8
pyproject -> "?"; report survives a non-UTF-8 sibling). Full suite:
452 passed (449 + 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: kflame99 <261674183+kflame99@users.noreply.github.com>
2026-06-02 18:44:57 +02:00
malkuthro b5b46d7fcc feat(sidebar): Snapshot Compare — read-only A/B side-by-side with diff tint (#181) (#183)
Phase 1 of #181: read-only side-by-side Compare. A/B toggle renders the existing sidebar twice — live working copy (A) + a loaded snapshot (B), B labeled with its own name + diff-tinted (green=only in comparison, red=same name/graph differs ignoring savedAt). Non-destructive: read-only capture guard on B, onChoose chooser never calls applySnapshot. Phase 2 (pull-in + A<->B swap) tracked in #197.
2026-06-02 08:22:15 +02:00
malkuthroandClaude Opus 4.7 b706a94030 chore(testenv): lock + audit test deps; pre-authorize auto-bootstrap (#182)
Make the Python test-env bootstrap safe to auto-run so contributors and
agents no longer have to approve a fresh-worktree install every session.

* `constraints-test.txt` — pinned resolve of `.[test]` extras + their full
  transitive closure (39 entries); committed lockfile becomes the
  dependency-change review surface.
* `scripts/bootstrap_test_env.{sh,ps1}` — install via the lock when
  present, then gate with `pip-audit` (a known CVE fails the bootstrap).
  New flags: `--force`/`-Force`, `--relock`/`-Relock`,
  `--no-audit`/`-NoAudit`. `--relock` without `--force` now warns
  instead of silently no-op'ing.
* `tests/scripts/test_bootstrap_constraints.py` — guards the lock stays
  in sync with the `[test]` extras declared in pyproject.toml.
* `CLAUDE.md` — documents that the bootstrap is pre-authorized to run
  automatically and supersedes the global `/warmup` MISSING_VENV
  hard-stop; dependency *changes* still land as a reviewed diff.
* `.gitattributes` — normalize `constraints-test.txt` to LF so the
  PowerShell `--relock` path doesn't flip the file to CRLF.

CI still installs its own minimal `pytest`+`aiohttp` subset directly
(not yet on the lock/audit path) — noted in the CHANGELOG; CI hardening
left as a fast-follow.

Verified: `--force` rebuild from the lock → pip-audit clean → 235 tests
pass (231 + 4 new guards). bash `-n` syntax check OK; arg-error path
exits 2; `--relock` without `--force` warns + exits 0.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:26:59 +02:00
malkuthro 3a0f835814 fix(audio-loop): read active director card values (#195) 2026-05-31 21:25:25 +02:00
malkuthro 63f5c1db7b fix(audio-loop): capture card metadata and Director pins (#193)
* fix(audio-loop): capture card metadata and Director pins

* fix(audio-loop): address card metadata review

* fix(audio-loop): remove duplicate card import
2026-05-31 16:44:46 +02:00
malkuthro 3294e6e7ae fix(video): robust load path handoff (#192)
* fix(video): make load path handoff robust

* fix(video): defer validation for linked load paths

* fix(video): avoid duplicate extension on counter strip

* revert(video): leave combine naming unchanged

* docs(video): clarify load path validation tradeoffs
2026-05-31 01:24:32 +02:00
malkuthro 201cdf3001 Track audio loop card setup metadata (#191)
* feat(audio): surface director flavor and prompt mode

* Track audio loop card setup metadata

* Fix audio card review issues

* Harden audio card metadata capture
2026-05-31 00:02:45 +02:00