* feat(cli): capture accepts a local HTML file again
Adds an explicit --local-file mode. The file's directory (or --local-root) is served
on a loopback port that only Chrome may reach outside the capture proxy; all other
traffic keeps the public-address policy. The URL argument stays public HTTP(S) only.
* fix(cli): local capture output names the file, not the loopback server
In --local-file mode, meta.json, AGENTS.md, extracted files and the returned result
used the temporary 127.0.0.1 origin. One rewriter maps that origin to file:// URLs
under the root, and meta.json records the file as its source. Root and served paths
now resolve with the same native realpath, which Windows needs.
* fix(cli): local capture reads its files from disk and resolves real paths natively
The asset downloader read the local page server with Node's fetch, which an environment proxy
(NODE_USE_ENV_PROXY with HTTP_PROXY) intercepts, so local images and fonts were dropped. It now
reads the file from disk under the server's own checks.
Root and served paths both use the native realpath. The JS realpath kept Windows 8.3 short names
while fs/promises expanded them, so every file answered 404 on Windows.
The default root is the directory the file was named in, so a symlinked entry can no longer widen
it to wherever the link points.
The local-file docs move below the capture policy text so their heading does not swallow it.
* refactor(cli): capture settles a watchdog outcome in one place
* fix(cli): escape every RegExp metacharacter in the local origin rewriter
---------
Co-authored-by: Jerrai <jerrai@heygen.com>
Chrome's profile lockfile is held for a moment after the browser exits, so removing the
capture profile threw EBUSY and a finished capture exited 1. The profile and staging
removals now go through a core helper that retries transient locks and, for cleanup of
finished work, warns instead of failing.
* feat(cli): capture can route its traffic through an opt-in upstream proxy
HYPERFRAMES_CAPTURE_PROXY=http://host:port sends the capture browser proxy's
forwarded requests and CONNECT tunnels, and every publicRequest download,
to that proxy by hostname instead of dialing a checked public IPv4. Unset,
behaviour is unchanged.
* fix(cli): keep capture comment share flat and close the new CodeQL findings
Test fixtures record what they receive instead of echoing request data, and
capture --video strips line breaks from a failed download's message before
logging it.
* fix(cli): proxied HTTP downloads send no URL fragment, and a malformed proxy credential is refused
The absolute-form request to the upstream used the URL's href, so a fragment such as
sprite.svg#icon went on the request line, which the pinned path never sends and strict proxies
refuse. A malformed percent-escape in the proxy credentials threw a bare URIError instead of the
usual "not a valid proxy URL" error. The test servers label their echoes text/plain.
* fix(cli): strip line breaks from capture --video errors in the form CodeQL recognizes
* fix(cli): capture --video logs errors through a recognized newline sanitizer
---------
Co-authored-by: Jerrai <jerrai@heygen.com>
* fix(cli): reap only orphaned browsers HyperFrames launched
Orphan cleanup on preview and render killed every PPID-1 process of the
current user whose args named chrome-headless-shell or a Puppeteer temp
profile, which included detached browsers started by other tools.
Every HyperFrames Chrome launch now passes an inert
--hyperframes-owned-browser switch, and the reaper only kills PPID-1
processes that carry it as a whole argument.
Fixes#6203
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(core): serve the browser marker from its own subpath
The GCP image's BeginFrame contract step imports the engine's
browserManager straight from source before core's generated runtime
exists, so pulling the marker from the core root entry broke it.
A dependency-free @hyperframes/core/browser-marker subpath keeps that
import chain as light as it was.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): expect the browser marker in the capture fallback launches
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs(cli): say orphan cleanup runs before preview and render
The cleanup only stops browsers HyperFrames launched, so the opt-out is
no longer needed to protect other tools' browsers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): snapshot rejects a negative --at and builds its sheet from its own frames
* fix(cli): snapshot --at refuses an empty value and trailing junk too
* test(cli): the contact sheet test's fake image reads orientation like the sheet now does
* fix(cli): capture images declared only through srcset
* chore: prepare conflict resolution against current main
* fix(capture): retain srcset coverage with validated GIF redirects
* fix(cli): preserve stylesheet order during website capture
* chore: prepare conflict resolution against current main
* fix(cli): retain captured stylesheet order with current main
* chore: prepare conflict resolution against current main
* fix(capture): retain cascade coverage with validated redirects
* 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(cli): preserve video frames when scenes reuse element IDs
* chore(core): keep media identity comments within the ratchet
* test(core): update page-build goldens for distinct media ids across repeated scenes
* fix(cli): a check that runs out of time says which step it was in and how long each took
When check hits its 120 s deadline, the error now names the step it was in
(loading the page, waiting for it to be ready, checking samples, the overlap
pass, finding crops) and the seconds each step took, including the video
copies and Chrome start that happen before the deadline starts.
* fix(cli): drop two comments the names already say
Carry the runtime's blur through the render pipeline and the Studio render
panel.
- engine: installPageMotionBlur sets the page-side off switch and the render's
device scale before any author script runs; CaptureOptions.pageMotionBlur
lets a caller stand the page blur down.
- producer: a composition that asks for data-motion-blur on the root the
runtime would pick forces screenshot capture, because html-in-canvas needs
a real paint; the probe session carries the request's pageMotionBlur like
every other session.
- studio-server and Studio: the render panel sends previewMotionBlur, the
render route turns "off" into pageMotionBlur once, and the Studio dev server
and the CLI server both receive the final value.
- mover scan: a large element that is see-through while it moves (a sweep, a
light leak) is no mover, including a large image or canvas with alpha; the
scan skips elements moved by a 3D individual transform property.
- cli snapshot injects video frames before the seek settles, matching the
engine's order.
Folded review fixes: R1, R2, R3 (scan half), R7, R16, R18, T3 (render
adapters).
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* refactor(skills): fold six domain skills into hyperframes and media-use guides
hyperframes-core, -animation, -keyframes, -creative and -studio move to
skills/hyperframes/{core,animation,keyframes,creative,studio}/, and
hyperframes-audio moves to skills/media-use/mix/. Each moved SKILL.md becomes
a frontmatter-free GUIDE.md so installers that scan recursively for SKILL.md
no longer see them as skills; the published list goes from 21 to 15.
Relative links, code paths and skill-name mentions are rewritten to the new
locations, the catalog surfaces, marketplace core-skills list, fallback core
set, module-copy targets and skills-manifest.json follow, and
`hyperframes skills update` now removes the retired skills (RETIRED_SKILLS)
when they are still installed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): slim the core entries and give media-use a HeyGen and a free path
The four entries every run opens are rewritten to what an agent cannot infer:
hyperframes 21.8 KB to 7.7 KB, hyperframes-cli 24.8 KB to 6.9 KB,
hyperframes-registry 9.9 KB to 4.3 KB, media-use 9.7 KB to 4.9 KB. Detail moved
whole into references (routing close calls and creator edits, CLI pin upgrades,
catalog search tiers, CLI conventions and feedback reports).
- hyperframes: state table, routing, the render contract, a terminal work loop
(check, snapshot, preview before render), the desktop-app hand-off, and a
table of the folded guides.
- media-use: one table of HeyGen versus free local tools per need; HeyGen
sign-in, credits and the CLI's video-agent, avatar, clone, lipsync, translate
and clipping commands; every free path works with no account.
- scripts/check-core-skills.mjs holds entries and descriptions to budget and
checks every taught `npx hyperframes` command and path; runs in lint.
- Core entries are exempt from the formatter: its table padding cost ~1.5 KB
per entry on every run.
- Content tests follow the text to its new home.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): plugin runs skip skills update; media-use explains HeyGen sign-in and TTS setup
The smoke eval showed a presenter request never reaching media-use and the
free voiceover failing on a missing kokoro-onnx; plugin runs also ran the
skills update the plugin rules forbid.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(skills): drop a committed Python bytecode file and ignore bytecode
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): align the entry with the Studio skills and keep the workflows' measured facts
The entry drops routing and the intent interview and carries the contract, the
editable-project rules and the work loop. Studio's format, runtimes, motion,
look and captions references come over for terminal use, frame presets move to
assets/, media-use gains Studio's mixing and local-command references, and
film.md and overlays.md keep what the workflow skills measured.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills)!: retire the workflow skills and folded guides; publish seven knowledge skills
The skill pack now publishes exactly seven skills - hyperframes,
hyperframes-cli, hyperframes-registry, media-use, figma, slideshow and
remotion-to-hyperframes - carrying knowledge, not process. The eight
workflow skills, the folded domain guides and the routing/interview
references are deleted; their measured facts already live in the
hyperframes references (format, runtimes, motion, look, captions, film,
overlays) and media-use's mixing reference.
- bake-basemap.mjs and locate.mjs move to skills/hyperframes/scripts/
(bake-basemap gains --help); extract-audio-data.py moves to
media-use/audio/scripts/ for the caption motion reference.
- skills update: the eight workflows join RETIRED_SKILLS so existing
installs prune them (by lock attribution only, since their names are
generic); naming a retired skill reports where its knowledge went and
succeeds instead of failing as unknown.
- Capture prompts point at the hyperframes skill's references/film.md;
the live-map warning names the new bake-basemap path.
- Content tests that pinned knowledge are retargeted to the kept
references; tests of deleted workflows, scripts and process go.
- Catalog surfaces, templates and docs describe the seven skills.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): resolve the local.md captions link and allow capture's output files in citations
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): regenerate the skills manifest from tracked files only
The committed media-use hash included an ignored __pycache__ bytecode file present
in the local checkout, so CI's clean checkout hashed differently.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(docs): restore generated catalog pages and docs navigation to main
Catalog PRs commit item sources only; CI regenerates the pages from the edited
registry sources. docs/docs.json is catalog navigation, so its Workflows group
rename is left for a non-catalog PR.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* build(cli): raise the CLI size budget for the skill references it bundles
The CLI bundles skills/hyperframes, which now carries the folded domain guides and
frame presets (155 KB to 1.08 MB in git). Budgets are the CI-measured size plus 5%:
packed 7526634, unpacked 22593213.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: keep repo-sweep guards meaningful after the workflow skills left
The last shell ffprobe caller and most root index.html examples lived in retired
skills. The shell detector is now pinned on a literal command line, and the
structure-rule sweep needs more than 5 roots (11 remain) rather than 30.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): slim figma, slideshow and remotion to the Studio shape; restore the storyboard board
slideshow 33.5 KB to 4.9, figma 18.2 to 6.2, remotion-to-hyperframes 11.0 to
4.9, each keeping the facts only it holds (the figma motion helpers, verify gate,
storyboard decoding and usage beacons; present and hand-off; the Remotion lint,
SSIM check and API map). The board page format from the storyboard recipe comes
back as hyperframes/references/storyboard.md: the desktop app's Storyboard tab
reads boards/storyboard.html by its frame, title and time classes. The entry and
the init template now say to snapshot the moments an edit changed before
replying, which the terminal eval found skipped in 22 of 38 runs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): manifest without local bytecode; Studio's edit prompt names kept files
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): render when the person asks for the file or for no stops, not only after approval
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): keep the voiceover carve in media-use; presets stage their own fonts
Review follow-ups on #5651:
- carve.mjs and its tests move to skills/media-use/audio/scripts; the audio
docs, mixing.md and two Studio comments point there.
- code-editorial's FRAME.md says to copy its bundled WOFF2s and add the
@font-face blocks, instead of promising the retired build-frame.mjs.
- media-use states the host rule before the HeyGen sign-in steps.
- storyboard-format marks transition_in as a note the agent builds itself.
- Two stale citations (creator-editing-recipes.md, sub-compositions.md).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(skills): regenerate the manifest without local bytecode
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): the CLI render step follows step 4's rule, not approval alone
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ci): run the core-skills check from lint again after the main merge
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): films that tell a story start from three storylines; maps use real imagery
Opus 5.5 arm D read motion.md and applied it, but its launch film was a
feature list (pain, three feature cards, proof, logo): film.md left story
to the model. And the map case never opened film.md, so it drew Natural
Earth outlines instead of baking satellite tiles.
- references/story.md: the generic half of Studio's moment-film story and
craft (one thing, three storylines with world and device, the hook,
beats, worlds, copy, brand, tells of a generated film). The person picks
a storyline, or the agent takes its recommendation when asked for the
file or for no stops.
- The entry's work loop names the storyline step for films over ~15 s; the
references table routes stories to story.md and maps to film.md.
- film.md points at story.md and gives maps and real places a section.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): story.md follows the entry: no voiceover unless asked
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): bake-basemap starts from a globe and stops waiting out its idle timeout
The Opus 5.5 map run spent 13 of its 38 minutes on parallel bakes that
stalled and 15 more recording 300 frames, after rewriting the script because
it could only start on a flat map.
- PROJECTION=globe with a seeded starfield and atmosphere, and FROM/PAN to
turn the globe to CENTER before the dive.
- Each frame polls for loaded tiles instead of waiting for an "idle" event
that had often already fired: 30 frames went from 198-219 s to 10-13 s
locally with byte-identical frames.
- Progress prints seconds a frame and minutes left; film.md shows the globe
call and says to run one bake at a time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): render in the foreground; wait on a detached render's log, never pgrep -f
Both Opus 5.5 explainer runs started a detached render, then waited with
`until ! pgrep -f "hyperframes render"`, which matches its own loop: each
render took 88-117 s and the wait sat for the tool's full 10 minutes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): route each kind of film; storylines carry look and sound; add review and sound
The entry routes by request (launch, explainer, music, data, vertical, 3D,
character) to a self-contained file under references/types/, and a film
over ~15 s now gets music and sounds on its visible actions unless the
person asked for silence. Each storyline names its look and its sound, so
picking one picks both; STORYBOARD.md frames carry `reads` and `sound`.
review.md is the whole-film pass: the check warnings, the review sheets,
seven questions to look for, P0/P1/P2 notes and REVIEW.md. motion.md gains
eases by role, six title entrances, a reading-time rule and nine measured
transitions; look.md a design plan before the first scene, with presets
kept for decks and named picks. story.md adds the second default that
generated design falls into, with what to do instead.
The blank template and format.md no longer teach the fade-and-slide-up
entrance on a dark canvas.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(cli): check warns on unread lines, dead stretches, small type, empty frame 0 and silence
A coding agent cannot watch its film, so check now measures what a viewer
notices over time. A new `craft` section holds warnings only (exit 0
unless --strict), sampled on the dense 8 fps grid the overlap pass
already seeks, skipped for --range checks:
- line_unread: a reading-size line (>= 30 px tall at 1080) on screen
under 0.4 s for 1-2 words, else under 3/4 of 0.5 s + 0.3 s a word;
the film's last line needs 2 s. Short lines are re-measured between
samples to ~0.01 s before judging. Captions-track and
data-layout-ignore text are exempt.
- dead_stretch: more than 3 s where the keepsMoving liveness signature
does not change, outside the final 3 s and any data-hold element;
quiet when sweep_static fires or a whole-composition keepsMoving
assertion owns the limit.
- text_below_floor: text under 2.6% of the short side (28 px on 1080)
held 0.5 s, one finding per file listing every element.
- first_frame_empty: frame 0 paints only the background.
- film_silent: over 15 s with no unmuted audio or audible video.
Each message names the fix and the skill reference to read. keepsMoving
now also leaves the film's last 3 s (a still end card) out of its
static runs, so the two agree.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(cli): snapshot --review captures a beat sheet, seam strips and a phone sheet
snapshot --review picks the frames that show a film over time instead of
evenly spaced ones, in the same single browser session:
- review-beats.jpg: one settled frame per scene at its start + 0.3 s,
plus one per timed data-beat="name" element, labelled time and id.
Scenes are the composition's top-level timed clips and sub-composition
hosts, read like `timeline` reads rows (captions and audio excluded)
and timed by the runtime's start resolver over the live page.
- review-seam-NN.jpg: frames at -0.2, -0.1, 0, +0.1, +0.2 s around
every cut (each scene start, and any scene end no scene starts at).
- review-phone.jpg: the beat frames at 360 px wide.
--composition scopes the review to one sub-composition; with fewer than
two scenes the beats spread evenly and a note says so. snapshot also
gains --json: one envelope with every frame's path and time and, under
--review, the scenes, beats and seams behind the sheets; progress moves
to stderr.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): craft warnings stay out of --strict; a hold gets a real next action, not a caret
Craft findings (unread lines, dead stretches, small type, an empty first
frame, silence) are judgment calls a film may break on purpose, so
--strict keeps gating defects only. The check reference no longer
suggests a blinking caret to keep a still frame moving.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): review spreads beats when every scene starts together; map credits skip the type floor
A map film whose basemap and overlay both start at 0 s got one beat
frame; the even spread now keys on distinct scene starts. film.md tells
agents to mark the imagery credit data-layout-ignore and back it, since
the new type floor flagged it on the first real run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): review frames each scene at its middle too, not only 0.3 s in
On the first real explainer, 0.3 s into a scene showed a bare colour
field before its title entered. Each scene longer than about a second
and a half now adds a frame at its middle, so the sheet shows what the
scene builds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): a film is built from a numeric SPEC.md, with defaults banned by name and a look test
Studied in full: flxrnc's Motion Lab (vague vs directed prompt), 0xMovez's
course, EianLu's riso skill, agentchengfeng's style samples and the two
Opus 5.5 guides. Their common point: adjectives are where the default look
lives, so the agent now writes SPEC.md before any scene (GOAL, OUTPUT,
TIME, LOOK, MOTION, SHOTS, BANNED, QA), with a number for every adjective.
spec.md bans the defaults by name, unless the brand or reference uses
them (a dark void with small things floating, glow and particles, the
cream-serif-mono-label look, fade-and-slide-up, corner brackets), and
grows that list after the first render. Each storyline gets a 3 s test of
its hero shot before the pick; a look is written as the rules of one
medium. Composition numbers: the hero fills 40% of the frame, one hero
move per shot, the shot's information in within 2.5 s then a held poster.
motion.md adds springs by damping ratio, shorter exits, overlap, stagger
and the beat grid; review.md reviews as a harsh director and grows BANNED;
vertical.md takes the riso skill's measured safe band; explainer.md the
shapes of information.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): story before look; every object reads as itself; posters only for info cards
The first spec-driven C02 swapped the dark default for a flat Swiss
poster, and the story went with it: two black bars nobody read as mirrors,
slogans instead of cause and effect, a storyline picked for its picture.
The spec now serves the story: a storyline is picked for the story a
stranger would follow, the look test asks first whether a stranger
understands what they see, and SPEC.md gains an OBJECTS line giving each
thing the material and details that make it read without a label. The
2.5 s held-poster rule is limited to information cards; in a story film
every shot causes the next. Explainers start from a person or situation
the viewer knows. Review names every object as a stranger would and
treats one that needs explaining as a P0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): storylines are three films in words, each with a device and a form
The three storylines are pitched as text and the person picks from them;
nothing is built for a storyline before the pick, so the 3 s hero-shot
look test is gone. Ideas pair a device with a form, five or more first,
keeping the three that differ most, and the one stop states the rest in
an assumption line that names every paid item beside its free option.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci: list main's new embedded-captions hf-cli test among the retired workflow files
The merge with main removed it with the rest of the retired skill, and the
deletion guard rejects any removal it has not been told about.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(core): keep the audio preset comment within core's size budget
The comment's new citation to media-use's mixing reference made the published
package 14 bytes larger than its budget, which sits at main's exact size.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): craft keeps its own 8 fps grid; comments back within the checks
Review on #5651 found that the merge moved craft onto the overlap pass's
times, which main caps at 180, so a brief line in a long film fell between
samples and line_unread missed it. Craft now builds its own grid at 8 fps up to
600 samples, as before the merge, and a 60 s film with a line shown for 0.2 s
pins it.
The Comments check passes again: REVIEW.md and STORYLINES.md join the files a
skill writes into a project, the comment lines this branch added come back
under each file's share at the merge-base, and the audio preset comment drops
a word the check reads as history.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): a finished capture exits even when a site's DNS never answers
Capture resolved every page request with the system lookup and gave up after
10 s, but a system lookup cannot be cancelled: the abandoned one kept the
process alive until the OS resolver timed out, so a capture that had printed
its result could run past the app's 5-minute cap and lose its folder.
Resolve through c-ares instead, as hostAnswers does, and cancel it at the
same 10 s deadline.
* test(cli): a capture lookup that answers must exit right after too
Dropping the timer cleanup kept every successful lookup alive 10 s and no test noticed.
* fix(cli): capture falls back to the system lookup and exits once its result is out
c-ares answers first. When it has no answer, the system lookup (hosts file, OS-only DNS)
runs in a child process killed at the 10 s deadline, cached per host, because Node waits
for an in-process system lookup even in process.exit. Capture then asks the CLI to exit
100 ms after printing its result, as render does.
* fix(cli): capture reads the hosts file first and kills its lookup child on exit
The hosts file answers before DNS, as the system lookup does (overrides and 0.0.0.0
blocks keep working). A host whose system answer is cached skips c-ares, a caller that
joins a running lookup keeps its own deadline, and a pending lookup child is killed when
the CLI exits.
* fix(cli): capture's forced exit waits until a slow reader has the whole result
The root exit now waits for stdout and stderr to hand over what is already
written before it calls process.exit, so a large JSON result on a slow pipe
arrives complete. A hostname listed several times in the hosts file now has
every address checked, not only the first.
* fix(cli): keep process exit in the CLI root and let test fixtures exit
The output wait is now a helper that only waits; cli.ts still owns the
process.exit. The process-ownership check treats test fixtures like tests,
since each one runs as its own process.
* chore(cli): run the process-ownership check before every commit
A process.exit outside a process root used to pass the local hooks and fail
only in CI's Preflight.
* fix(cli): snapshots, compare and check wait for late images the way render does
* fix(cli): check runs its browser pass inside a macOS sandbox that does not name itself
Every CLI Chrome launch has used a pipe since the warm-check change. A pipe
launch that a seatbelt kills fails with only "Protocol error
(Target.setDiscoverTargets): Target closed". The one-process fallback keyed
on Chrome's bootstrap_check_in stderr, which a pipe launch failure never
carries, so it never fired. On macOS a pipe launch that fails at the
protocol level now relaunches as one process over the pipe.
* fix(cli): retry only a pipe launch whose connection closed, and keep puppeteer lazy
* fix(cli): the browser the CLI launches exits with it, even on kill -9
* fix(build): the root build runs on Windows
* perf(cli): a repeat check runs in a process that already has the CLI and Chrome up
* fix(cli): a warm check delivers all its output before it exits
* fix(cli): the warm check only talks to a server that proves it is the user's
The warm socket now lives in a private, owner-verified directory (XDG_RUNTIME_DIR,
~/.cache or LOCALAPPDATA) next to a 0600 token. Client and server prove the token to
each other over fresh nonces before any request is sent, the request carries an
HMAC of the environment instead of the environment itself, and frames are capped.
* fix(cli): a warm check prints its stdout even after something went to stderr
The client stopped writing frames once any frame had been written, so a check that
printed a notice to stderr first lost its JSON report.
* test(cli): list the warm-check client fixture as a fallow entry instead of an inline ignore
* fix(cli): a check started while a warm one is busy runs cold at once on Windows
The warm server stopped listening when it accepted a client. On Windows the pipe name stays alive through the served connection, so a second client's connect saw a busy pipe and waited 30 s for a free instance before running cold. It now keeps listening while serving (accept already declines anyone else) and stops when the served check finishes.
* test(cli): the orphan test reads the check's own Chrome and watchdog instead of racing a poll
On Windows each process listing is a PowerShell call of about a second, so a check that finishes quickly could start and close Chrome between two polls, and the test failed its own 'Chrome was seen' precondition. A fixture now records the Chrome tree and the watchdog from inside the check when its first page opens.
* perf(cli): commands run without esbuild, puppeteer-core or sharp until they need them
A host that bundles the CLI can now leave these packages out: each command
loads them only where it uses them, and sharp installs once on first use when
it is missing. The build check fails on any static import of them again.
* fix(cli): use a host's sharp from NODE_PATH and let hosts turn first-use installs off
* fix(cli): capture fails when sharp is missing, and one install serves every caller
A missing sharp now stops the asset download up front instead of counting every
image as unavailable. Concurrent first uses share one install, a failed install
is not retried in the same run, and preview thumbnails report why. Hosts can turn
off installs for a list of packages, e.g. HYPERFRAMES_NO_OPTIONAL_INSTALL=sharp.
* fix(cli): snapshot --describe still works without sharp, resizing only when it is installed
* style(cli): format the sharp loader and its tests
* build(cli): sharp is an optional dependency, so a host can ship the CLI without it
* build(studio-server): keep sharp out of the bundle now that it is optional
* test: queue runs on slow windows runners stop failing four timing tests
* test(cli): the keepalive process test budgets for a cold start on a slow runner
* fix(cli): wait for consent navigation before cataloging assets
A consent click can reload the page (some CMPs reload once consent is
stored). The post-click DOM reads then raced the navigation: the overlay
sweep and scrollHeight probe ran against a torn-down or still-parsing
document, scroll screenshots degraded to a handful of shots, and the
asset catalog observed an empty body (0 assets).
Add waitForPageReady: wait for network idle, then poll document.readyState
so a committed-but-still-parsing reload is also waited out — network-idle
alone stops counting a request once its response headers arrive, and
waitForNavigation deadlocks when armed after the commit. The overlay
sweep now runs on the settled document instead of the pre-reload one,
and the same settle guards the asset catalog boundary.
Fixes#4923
* fix(cli): bound consent-click settle by wall clock and skip it on banner-free pages
waitForPageReady now shares one deadline (timeoutMs, capped by the
remaining capture budget) across its network-idle waits and readyState
poll, uses waitForNetworkIdle concurrency: 2 (the networkidle2 semantic
navigation already uses), races each readyState evaluate against the
deadline, and rethrows degradable evaluate timeouts so the stage can
degrade. captureScrollScreenshots only pays the settle when the consent
sweep clicked a control or performance.timeOrigin shows the document was
swapped; the catalog boundary call degrades to a budget-bounded
readyState check (networkIdle: after-load).
* fix(cli): drop added comments to keep comment share at merge-base
* fix(cli): adapt consent-settle tests to ScreenshotCaptureResult and keep comment share flat
* fix(cli): tolerate missing waitForNetworkIdle in consent settle
waitForPageReady called page.waitForNetworkIdle unconditionally; a
Page-shaped double without it throws a synchronous TypeError that
captureScrollScreenshots' catch-all reports as internal-error with
zero screenshots, breaking the partial-budget contract upstream's
coreExtractionPhase.test.ts exercises with a thin mock page. The idle
wait is best-effort, so make the hook optional and swallow its
failures; also only keep the readyState poll alive while the page
affirmatively reports "loading" so a non-boolean result cannot burn
the settle deadline.
* perf(cli): check reads a solid text backdrop from styles instead of a screenshot
* perf(cli): check starts Chrome while lint and bundling run
* perf(cli): the CLI reuses compiled bytecode between runs
* test(cli): browser shutdown tests stop their Chrome when a case fails
* perf(cli): check and inspect skip paint waits and screenshots nothing reads
* perf(cli): check runs its dense overlap pass in one page task so no frame is painted between seeks
* perf(cli): check waits for late images and fetches instead of a fixed 1.5 s after load
* fix(cli): check gives an event it can only place within the dense pass that pass's time range
* perf(cli): check waits for image decodes before a screenshot instead of a fixed 120 ms
* perf(cli): check screenshots only the text contrast measures and skips samples with none
* perf(cli): seeks check fonts on the settle's second frame instead of waiting a third
* docs(cli): the contrast audit header says how a clipped screenshot is read
* docs(cli): cut the font-wait and contrast-audit comments to short blocks
* fix(cli): a page-load timeout names the script or stylesheet the page was waiting on
* test(cli): wait for Chrome's own request event before reading the tracker
* fix(cli): keep signed-URL secrets out of page-load timeout messages
* fix(cli): check and snapshot wait for a slow CDN script as long as render does
check, snapshot, layout, motion-shot and validate gave the page 10 s to reach
DOMContentLoaded while render gives it the engine's page-navigation timeout
(60 s), so a parser-blocking script from a slow CDN failed the diagnostics
with "Navigation timeout of 10000 ms exceeded" on projects that render fine.
The separate 10 s budgets are gone: every caller takes the engine's value
(and its env override), raised only by a command's own longer --timeout.
The cold-Chrome allowance for sending the first request is unchanged.
* fix(cli): check and snapshot read only the page-load budget from the engine
resolvePageNavigationTimeout() owns the page-navigation env read for render
and the diagnostics, so check and snapshot no longer resolve the whole render
config, which printed render's Windows streaming-encode warning. validate's
comment and timeout hint now say render shares the budget.
The seatbelt launch runs Chrome as one process, where there is no GPU process
to composite. Capture's flags left GPU compositing on, so every screenshot
failed and heavier pages stalled until the protocol timeout. The one-process
launch now also disables GPU compositing, as software capture already does.
* fix(cli): check's dense geometry seeks stop waiting for video frames they never read
* test(cli): race the dense seek against the barrier ask instead of a timer
* fix(cli): keep the geometry seek's comment share at its base
* fix(cli): dense geometry seeks still wait for frame sources, only video frames are skipped
* fix(cli): page loads stop timing out while a busy machine's Chrome warms up
* fix(cli): keep the page-load helper comment-free and its files in the bench filter
* fix(cli): bound checks of non-finishing compositions
* fix(cli): account for cold startup in deadline witness
* fix(cli): wait for profile owners before test cleanup
* test(cli): separate portable deadline and Linux process witnesses
* test(cli): preserve per-launch browser profile ownership
* fix(cli): start check deadline after browser readiness
* fix(core): a sub-composition timeline with a non-function duration no longer aborts the runtime
* fix(core): every registered-timeline duration read survives a partial or throwing entry
* fix(core): a runtime that fails to start reports its real error instead of a half-built page
* fix(core): a timeline post that throws after start-up is named too, and a later good post clears it
* fix(cli): check skips a custom clock whose time() returns null instead of reading it as stuck at 0
* fix(core): a recorded runtime start-up error stays set instead of being cleared by a later post
* fix(core): the runtime failure message says what to fix
* fix(core): the runtime failure is recorded even when the thrown value cannot be printed
* chore(core): format and regenerate the position-edits bundle for the startup error recorder
* fix(cli): keyframes and validate close the browser when the runtime fails to start
* fix(cli): snapshot, check, layout and thumbnails honour software WebGPU like render
* test(cli): stub the local WebGPU helper in the studio server tests
* fix(engine): webgpu compositions are recognised after the bundled runtime script
* fix(engine): a hung GPU probe falls back to software in 15 s instead of 120 s
* fix(engine): the GPU probe keeps the configured launch timeout and bounds only its page
* build(parsers): declare the html-attribute-spans subpath in its descriptor
* build(gcp-cloud-run): the BeginFrame contract stage copies parsers, which the engine now imports
* test(cli): the screenshot launch pin follows the shared local WebGPU helper
* fix(engine): the GPU probe's wait for Chrome's first page falls inside its 15 s limit
* fix(cli): enforce capture screenshot limits and report interruptions
* refactor(cli): split screenshot step out of capture animation phase
Keeps the screenshot limit outside the mirrored core-extraction argument
list and moves scroll screenshots into their own step, so both stay under
the audit's duplication and complexity gates with no behaviour change.
* fix(cli): a project .env cannot set the feedback email in another letter case
* fix(cli): capture loads a project .env through the same rules as the CLI
* fix(core): composition scripts start after web fonts are ready
A scene script that measures text (offsetHeight, getBoundingClientRect) ran while
the page parsed, often before its web font loaded, so it laid the scene out with
fallback metrics. Renders of the same film differed run to run, and preview could
differ from export.
The compilers now give every body script a type the browser does not run
(text/hf-after-fonts, or +module), through one helper in scriptRuns.ts called
at the end of the bundler, the producer compile and the sub-composition preview.
The Studio motion script is emitted the same way so it still runs after them.
The runtime entry owns the wait: on DOMContentLoaded it forces a layout, waits for
document.fonts.ready or FONT_WAIT_TIMEOUT_MS (5 s), then re-creates each script
in its own place in parser order (classic in order, awaiting src loads; then
module and defer scripts in insertion order), boots as before, and calls the
DOMContentLoaded and load listeners the scripts added. On timeout the scripts
run anyway and runtime_font_wait_timeout names the families still loading.
Scene swaps and the composition loader use the same wait.
* fix(core): a page whose runtime predates the font gate still runs its scripts
A bundle can be served with a runtime loaded from HYPERFRAME_RUNTIME_URL. If that
runtime is older than the font gate, nothing would run the deferred composition
scripts and the film would be blank.
Whenever the marking helper defers scripts, it also puts one small inline script
first in the head. At DOMContentLoaded it runs the deferred scripts in parser
order itself, without waiting for fonts and with a console warning, unless a
runtime has claimed them. The gate's runtime claims them when it loads, before
DOMContentLoaded, so the scripts run exactly once either way.
* fix(studio-server): motion paths animate in Studio preview when gsap loads in the body
The MotionPathPlugin tag Studio inserts after a body gsap script now carries
that script's type, so the runtime runs it right after gsap once fonts are
ready, instead of before gsap exists.
* fix(cli): layout, motion-shot and validate wait for the composition scripts to run
The three commands waited on the timeline registry, which exists as soon as the
runtime file loads, or on a flat sleep. They now wait for the runtime's
render-ready flag through one shared helper, keeping each command's timeout and
proceeding as before when it passes.
* fix(core): a script that wraps addEventListener keeps its wrapper after its scripts run
While composition scripts run, the runtime patches addEventListener to hold
load events that already passed. Putting the original back now happens only if
the patch is still in place, so a library wrapper installed meanwhile survives.
* docs: comments on script timing match scripts that wait for web fonts
The early stub's flush and the player's iframe-load handler described page
scripts as done by DOMContentLoaded and the runtime as ready before load.
Composition scripts now wait for web fonts, so both can come later. Comment-only.
* test(core): pin script order after an external script and the scene swap's font wait
An inline composition script that follows an external one sees its global, and
a scene swap runs the new scene's script only once web fonts are ready.
* docs(producer): the early stub comment names what drains late font-gated work
* fix(studio): motion paths add without a flash when gsap loads after web fonts
Studio preloads MotionPathPlugin on iframe load. A body gsap now runs after web fonts, often after that load, so the preload retries once the runtime reports ready.
* test(cli): pin the runtime wait before each command first samples the page
* fix(studio): the motion path preload stops waiting once the preview runtime has booted
A composition with no global gsap posts ready before the preload runs; the retry listener then never detached and kept each retired preview document alive. It now waits only while the runtime has not booted, and the tests pin the cleanup and the ready filter.
* fix(studio): a preview frame that never boots is not kept alive by the motion path preload
Only the latest preview waits for the ready message of its runtime; a newer load drops the older wait, so a crashed or missing composition frame can be collected.
* style: comments added by the font gate move into names