* fix(cli): init and skills update only touch HyperFrames skills; --skip-skills works
* fix(cli): init never touches installed skills and no command removes one
* fix(cli): shorten the init skills comment
* fix(cli): shorten the init skills comment
* fix(cli): the stale-skills notice reaches agents that run without a terminal
* test(cli): a run without a terminal now checks skills in the background
* fix(cli): the stale-skills notice also prints on --json runs
* perf(engine): remote sources of a site keep their probe results for the whole render
* test(engine): probe a content-addressed URL once per render
* style(engine): format the audio probe split
* fix(sdk): a delete is kept hidden when any script lookup could reach it, not only an id
* fix(sdk): a lookup argument that is not one closed literal keeps the delete hidden
A `<canvas layoutsubtree class="hf-vfx-src">` nested inside another capture
canvas blocks the renderer main thread in drawElementImage, so `check` and
`snapshot` time out with no output. Refuse either direction of the nest with a
reportVfxError and leave the chain unregistered.
Also scope the two host lookups to immediate children (`:scope >`) so a nested
host's src/out canvas is never bound to the outer host, and read the
`data-vfx-ref-visible` flag in resolveCaptureSource so a canvas reached as both
a host's own source and as a ref gets one consistent `visible`/keepBitmap.
Refs #4405
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The prepare guard used `test -d .git`, which is false in a linked
worktree where .git is a file, so lefthook install was silently skipped.
Use `test -e .git` so clones and linked worktrees both install hooks
while non-git directories remain a no-op.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): recognize painted gradient children on older Chrome
* fix(cli): recognize ancestor image text masks
* fix(cli): report clipped ancestor text on older Chrome
* fix(registry): centre mk-callout-highlight with flex instead of a script-time measurement
The text box becomes a flex item of a full-frame wrapper (#mk-ch-center),
so its position is laid out by the browser rather than read from a
getBoundingClientRect() taken at script time. That measurement depends on
which font faces are loaded when the script runs; the fonts gate covers only
faces that static markup already uses, so script-built text measures
against the fallback face (#5112). A non-null CONFIG.x / CONFIG.y becomes a
margin offset on that axis. The GSAP y entrance stays on the box, which
carries no CSS transform.
Also drops the scene-exit visibility tail (the runtime hides the clip at
its window end; the 0.02 s tail never reached a frame at 30 fps) and
removes data-layout-allow-occlusion from #mk-ch-box: nothing beneath the
block is text, and the attribute made the contrast audit skip the block's
own words whenever a later full-frame host answered the hit-test probe.
Renders are bit-identical to the measured version when the face is loaded
(nine-scene project at v0.8.135, 1380/1380 frames). CONFIG and the sweep
mechanic are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(registry): keep only the flex centring in mk-callout-highlight
Address review: restore the scene-exit visibility tail and the layout
occlusion attribute, which belong to family-wide conventions and are out
of scope here, and note the x + maxWidth clip edge in CONFIG.
Refs #5112
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The media_src_kind_mismatch rule classified .mp3 and .jpeg but not .m4a or
.avif, so an <img> pointed at an audio file or a <video> at a still slipped
through for those two formats. Add .avif to IMAGE_SRC_EXT and .m4a to
AUDIO_SRC_EXT.
.ogg stays out: its container can carry Theora video as well as Vorbis or
Opus, so the extension does not name the kind. .m4a is the opposite case --
it is audio-only, and video in the same MP4 container is .m4v, which
VIDEO_SRC_EXT already lists. The .ogg case keeps its existing coverage and
gains an explicit unknown-extension case alongside it.
Refs #4382
Co-authored-by: Yi-111-a <Yi-111-a@users.noreply.github.com>
* docs: align render, fps and transcribe docs with the CLI
- docs/guides/rendering.mdx: default render quality is `looks` (CRF 16),
not `standard`.
- docs/packages/cli.mdx, render --help (render.ts): default --output is the
timestamped `renders/<project>_<YYYY-MM-DD>_<HH-MM-SS>.<ext>`.
- skills/hyperframes-cli/references/preview-render.md: local --fps accepts
1-240 or a rational and defaults to the root data-fps, else 30; only
lambda and cloudrun are limited to 24, 30, 60.
- skills/hyperframes-cli/references/lint-validate-inspect.md: snapshot
--range is documented as START,END like every other reference.
- skills/media-use/references/audio.md, skills/media-use/audio/references/
transcribe.md, skills/hyperframes/references/captions.md: name the entry
point for each transcription default (transcribe.mjs prefers Parakeet;
`hyperframes transcribe` uses Parakeet when installed, else the small.en
whisper model).
- transcribe --model help text and docs/packages/cli.mdx: list `small` and
`medium`, which the CLI already accepts.
- skills-manifest.json: regenerated hashes for the three skills touched.
— Jerrai
* docs: lambda and cloud run also accept 25 fps
Distributed renders accept 24, 25, 30 and 60 fps (DISTRIBUTED_FPS in
packages/core/src/distributedFps.ts).
- skills/hyperframes-cli/references/preview-render.md: the --fps note
lists 24, 25, 30, 60 for lambda and cloudrun.
- skills/hyperframes-cli/references/lambda.md, cloudrun.md: --fps lists
include 25.
- packages/cli/src/docs/rendering.md (`hyperframes docs rendering`): --fps
said 24, 30 or 60 and --quality defaulted to standard; local render takes
1-240 or a rational, default root data-fps else 30, and the default
quality is looks.
- skills-manifest.json: regenerated hyperframes-cli hash.
— Jerrai
* chore(skills): regenerate skills manifest after formatting
The format hook rewrote skill markdown after the manifest hook had hashed
it, leaving the hyperframes-cli hash stale.
— Jerrai
duplicate_media_discovery_risk, self_closing_media_tag and
placeholder_media_url were defined in media.ts but no test in
media.test.ts mentioned any of their codes.
Adds a trigger case per rule (asserting code and severity) plus
negative cases that pin the boundary each rule actually keys on:
one fingerprint field differing for duplicate discovery, an
explicitly closed tag, and a real src.
Each new test was checked to fail against a disabled rule.
The serial guard in captureFrameSurface, which segmented capture reaches, threw on any frame under the
size floor. It now draws the frame once more and keeps a byte-identical redraw, as the streaming drain
guard does, so a flat frame no longer fails a segmented render.
Fixes#5685
Note that the desktop hand-off in `open` is macOS-only for now, that
`timeline apply --plan` prints only a count without --json, and what
`history undo --who` reverts.
Refs #5386
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): preview fetches only the clips due next, and drops passed clips still downloading
A fast-cut film started a fetch per clip inside the look-ahead and let passed clips finish.
* fix(core): a passed clip stays released for the rest of the play
It was refetched each pass inside the look-back window.
The isolation CSS re-showed every descendant of a mover, so a child the
author hid with visibility:hidden was cropped and smeared into the trail
(240 green px in the repro; 0 in the sharp frame). The hide rule now skips
the mover subtree and the mover alone is made visible, so its descendants
inherit and keep their own visibility.
Found by review of #6106.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(studio): motion blur panel with Off / Subtle / Strong
One three-way control in the right dock. Subtle is the default blur
(240 deg shutter, samples sized to the smear); Strong is 720 deg and 64
samples. Writes the root data-motion-blur attributes through the SDK so
renders match and undo works; reads live attributes from the preview.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(studio): motion blur panel edits the file's own root
The edits were worked out against the preview DOM but written to the file,
so a stale preview (undo inside the hold, a slow reload) could save a level
other than the one picked. They are now computed inside the mutation from
the candidate's root. While locked to a sub-composition no level is shown.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(studio): shorten the motion blur tab so the dock strip fits
Five tabs overflowed the default dock width by 10px and clipped a label
(Studio: load smoke). The tab is now titled Blur.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
- skills/hyperframes-cli/references/upgrade-info-misc.md: the "Remaining
harness usage" section (heading plus three paragraphs) appeared twice in
a row; removed the second, byte-identical copy (8 lines).
- skills-manifest.json: regenerated the hyperframes-cli content hash, as the
manifest check requires after a skill file changes.
No wording changed.
— Jerrai
* perf(aws-lambda): a render reads a video's audio from a small file stored beside it
A video's audio is interleaved with its pictures, so mixing it from the cloud read the whole video (2.5 GB for a 20-minute 4K film) at the Lambda's network speed. lambda sites create now stores each large video's audio packets as a separate object keyed by the video's hash; the audio task reads that when it exists.
* chore(producer): shorten the audio sidecar header comment
* fix(aws-lambda): no audio sidecar for a video whose audio starts late or that has several tracks
* test(producer): format the audio sidecar tests and end their ffprobe options with --
* fix(cli): snapshot waits for the image layers a seek reveals before capturing
x
* style(cli): shorten the late-image test comment
* style(cli): trim three comments so the comment share of the capture file goes down
* fix(engine): a composition whose script throws is skipped with a warning, not waited on and refused
A script that threw before registering its timeline kept the capture waiting the full 45 s readiness timeout, then the render was refused. After a page error with timelines still missing the wait now ends after a short grace, each such composition gets an empty paused timeline of its authored length, and a script_error_isolated warning names the element. The render goes on without that animation.
* test(engine): memo test expects a script failure, format the browser test
* style(engine): shorten a doc comment to the line limit
* fix(engine): isolate only nested compositions that threw, never load failures or a root
* fix(engine): a thrown composition script fails fast and names the composition, not a load error
* fix(engine): the page-error grace starts at the first error, not at the start of the wait
* fix(engine): a nested throw is skipped only while another nested timeline is live
* feat(cli): check writes a progress line to stderr per sample, overlap batch and craft frame
* fix(cli): the check progress lines are written by the CLI, not by the pipeline tests call
* fix(sdk): a stored delete a script still looks up is hidden when overrides replay, not removed
* fix(sdk): shorten the script lookup comment
* fix(sdk): a replayed delete a script looks up leaves the model and serializes back hidden
* fix(sdk): shorten a comment line
* fix(sdk): a kept delete is marked hidden so the audio mixer drops it
* feat: check names the filter fallback, the render log names the tier, Studio stops opting out
hyperframes check reports motion_blur_filter_fallback (info) with the reason capture could not
run and the way back to it. The engine's per-session log line gains tier=, and its pixel test
covers the fallback over a WebGL page. Studio no longer sends previewMotionBlur: every Chrome
can preview blur now, through the fallback where it lacks html-in-canvas; the server still
accepts false from an older Studio.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(core): blur picks the cheaper filter tier when every mover only translates
The scan now notes whether any mover turns or scales (the linear part of its transform chain
changes) and whether it already forms a stacking context while it moves. When every mover only
translates, the filter tier draws as well as capture and costs about what no blur costs: on an
8 s bench shot a render took 7.0 s against 9.9 s with capture and 7.3 s with no blur. Anything
that turns or scales, or moves without a transform, stays on capture.
data-motion-blur-tier="capture" or "filter" forces a tier. The report says which one drew
and why (translation-only, tier-attribute); hyperframes check stays quiet for those two, which
are choices, not fallbacks. The pixel tests run the shared assertions through both tiers.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs(motion-blur): the two tiers, the tier attribute, and what changed in limits
The guide described blur turning off for WebGL, a root transform and similar. It now says the
filter tier draws instead, how the tier is chosen and forced, what each does with a turn or a
zoom, the measured cost of each, and which findings check emits. The skill note drops the
off-for-WebGL advice and the 1.7x figure.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(core): blur defaults match a 240 degree shutter and at least 32 samples
The shutter opens 240 degrees by default (was 180) and each mover gets at least 32 taps (was
sized to the smear alone, down to 2), growing with the smear as before. These are the settings
the shutter blur Jake built uses; on the card whip the default render is now 51 dB from his
(34.6 dB at the old defaults). The guide's attribute table says so.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* chore(core): trim tier.ts comments to the ratchet
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* chore: allow deleting the Studio previewMotionBlur render test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(producer): a chunk with mixed capture modes retries whole in screenshot mode
* fix(producer): format the mixed capture mode change
* fix(engine): the BeginFrame start-up check waits for the first frame up to its deadline
* style(engine): format the probe tests
* fix(engine): drop a comment from the probe loop
* fix(engine): a render no longer decodes the videos it replaces with extracted frames
* fix(engine): shorten the render-frame video hold comment
* fix(engine): keep the render-frame video hold comment under the line limit
* fix(engine): hold only the videos whose render id is replaced by extracted frames
Two scenes can share a video id. The render id tells them apart, so a live video is no longer held.
* perf(studio): an idle editor stops waking the GPU four times a second
The overlay frame loop's safety-net poll requested one animation frame every
250 ms for as long as the editor stayed open. Each request restarts the
display's vsync for a few frames, so a paused, untouched editor kept the GPU
process waking ~65 times a second. The poll now doubles its wait after each
idle frame up to 4 s, resets to 250 ms on any wake, and stops while the page
is hidden (visibilitychange already wakes it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(studio): keep the overlay loop's comment share at or under its base
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(core): capture blur falls back to per-element filters where capture cannot run
html-in-canvas cannot capture a WebGL canvas, an iframe or a transform on the root, and some
browsers lack it. Blur used to turn off for the whole composition. Now the runtime hands the
composition to a filter tier: an SVG filter on each mover, 32 offset copies averaged along its
sampled path, softened along the motion so long smears do not step. It filters the live DOM,
so it works over a WebGL canvas, in any Chrome, and leaves glows and shadows on the element.
Turn and scale only soften; capture draws those truthfully, so it stays the first choice and
a scene re-init tries it again.
Capture turning itself off for a defect or three failed captures hands over; the renderer's
explicit off (HDR, distributed, engine blur) stays off. The report names the tier. Core's
unpacked size budget goes to 3,965,000 for the tier.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* test(engine): the backface pages now take the filter tier
The two backface tests still expected blur off; with the filter tier a
backface page reports tier filter. Fails on CI without this.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* perf(aws-lambda): warm the audio function while the plan runs
The audio task runs on the x86 render function, which is cold when the plan ran on the arm64 video function. A fire-and-forget invoke at the start of a render now wakes it, so the audio no longer outlasts the chunks.
* perf(aws-lambda): warm the render function only on a stack with a video function
An x86-only stack has no cold function to warm, so it takes the old path with no extra step. The CDK stack has no video function and is unchanged.
* perf(aws-lambda): wrap a template comment
* style(aws-lambda): format the warm-up template test