* fix(core): write only authored hue curves, so a single hue curve passes lint
* fix(cli): apply over a grading saved with empty hue curves instead of refusing it
* fix(core): read grading properties from computed style, and stop advising an inline start
* fix(cli): preserve validation during legacy hue repair
* fix(cli): preview grading writes without apply
* fix(cli): group sampled check findings by element and rule
* fix(cli): preserve unique elements when grouping audit samples
* fix(cli): retain geometry sample times until report grouping
* fix(cli): keep grouped finding evidence time paired with geometry
* fix(auth): accept host-managed HeyGen access tokens
* fix(auth): share environment header validation
* docs(auth): condense credential resolver guidance
* fix(auth): report the account a browser login just signed in
Identify the account from the tokens the login issued, not a fresh credential
lookup that an environment token or API key outranks.
* docs(auth): list the host access token in the CLI credential order
* fix(cli): name a rejected host access token on publish
Publish told a host-token user to log in again, which cannot help while the token outranks the login.
* docs(auth): show the media credential order main resolves
* fix(cli): warn at logout that a host access token still signs commands
One map of environment credential variables, owned by the resolver, now feeds the logout warning and both publish messages.
* docs(auth): scope the media credential order to the audio scripts
* refactor(cli): read every env credential name from the resolver's map
* fix(studio): recover previews after missed file changes
* fix(studio): keep queued reloads with the current owner
* fix(studio): keep a queued outside change when switching compositions
* test(cli): the project watcher closes its parent folder's watch too
* fix(studio): keep the project-folder change out of the shared signature check
* fix(studio): keep the edited file's draft and reload every session after a reconnect
A reconnect or replaced folder is a project-wide change: every open SDK session reloads, a failed save is recorded against the file the draft belongs to, and two queued changes to different files merge into one project-wide reload.
* fix(studio): refresh the preview cache when the .hyperframes folder is replaced
* fix(cli): report the files of a folder moved into the project
Replacing a populated folder such as .hyperframes/ now reaches the signature cache through its files, so the shared signature rule stays as on main and Studio's own writes inside .hyperframes/ still invalidate nothing.
* refactor(studio): name the merged file-change payload and share the test sender
* fix(studio): keep a failed draft's recovery copy until that file itself saves
* style(studio): rewrap the merged payload line after the rename
* fix(studio): deliver a held change's full reload when its block clears
A clean save, Use external and Keep Studio now all deliver the merged scope a blocked change owes, and a moved-in .hyperframes folder reports its manifests so a dropped one still clears the cached signature.
* fix(studio): keep restored drafts out of the reload scope and clear the lint gates
* fix(studio): retain held reload scope before React renders
* fix(media-use): use a host app's music tools; fail loudly without the heygen CLI
* fix(media-use): name the heygen CLI only for music and sound effects; keep the sfx test offline
* fix(skills): workflow skills use a host app's own music tools first
* fix(skills): say how a host app's music track reaches the narrated workflows
* fix(skills): set a host app's music track after the last audio pass
* fix(skills): a host-music film with no narration still fetches its sound effects
* fix(media-use): keep the stub-failure test off the machine's heygen CLI; sync the skills manifest
* fix(skills): audio passes keep a host app's music and sounds; silent needs no sound cues
* fix(skills): host audio keeps string frames, stops on a broken audio_meta.json, survives a re-run
* fix(skills): report a host audio entry with no path and a broken sidecar instead of crashing
* fix(skills): keep host audio written while an audio pass runs
* test(cli): serve the telemetry capture while the resolve child waits on it
* test(cli): fail the telemetry capture when the resolve child exits non-zero
* style(skills): wrap the host audio comment to the 120-column limit
* docs(skills): ask agents inside a host app to report its friction as feedback
* feat(cli): send missing features and host-app friction as ratingless feedback
* fix(cli): keep the rating error, refuse blank reports and document ratingless feedback
* docs(skills): leave names and figures out of missing-feature reports
* docs(skills): keep anything private out of missing-feature reports
* fix(engine): automatic worker count never exceeds what the V8 heap can feed
A 24 GB Mac exporting a 43 s portrait project with 22 videos picked 5 capture workers against a 4192 MB heap that feeds about 4, and the render died at 62% with JavaScript heap out of memory. The heap budget was only advisory; it now caps the automatic count (bound 'heap'). An explicit --workers N is untouched.
* test(engine): heap-cap test restores its mocks even when an assertion fails
Review findings on #5149: the os and v8 mocks were restored only if every assertion passed, and two comments still called the heap cap advisory.
* docs(engine): heap budget comments say it caps auto sizing
* fix(cli): load telemetry without the core and engine barrels, and stop the history test racing
* fix(cli): load history from its leaf subpath, and fail the history test fast on a crashed CLI
* fix(check): see color, filter and media time under seek, and name data-no-timeline for stills
* fix(check): warn instead of fail when only audio advances, and judge text overlap on glyphs
* fix(check): judge glyph overlap by the glyph box, and see capitalized words and one-side borders
* fix(check): capitalize past leading punctuation, test outline color, trim a comment
* fix(check): capitalize words the way Chromium does, so digit-led words stay lowercase
* feat(render): report encoding and assembling progress
* fix(render): close every progress stage and fail a render whose progress callback throws
* fix(render): send a repeated progress update once
* fix(render): keep Docker terminal output as before and drop the unneeded callback ending
* fix(render): keep one failure stage code per stage whatever its frame counts
* fix(core): the preview loads the next scene's images before it starts
* fix(cli): check stops reporting image loads that its own scrubbing cancelled
* fix: preload only a coming clip's own images, and judge aborted images by whether they still decode
* fix(cli): count an aborted image as broken only when its decode fails
* style(cli): keep the aborted-image comment within the line limit
* test(cli): pin that a reload still pending at the cap is not reported
* test(cli): run the cap test on whatever timers are pending, and always restore real timers
* feat(registry): gooey split block with metaball goo and directional motion blur
A dot splits into a three-dot menu through a gooey neck, spins into a
swirl mark under shutter motion blur, breaks back into three dots and
merges into one. Timing, spacing and eases are measured off a reference
recording frame by frame.
The motion-blur snippet gains an opt-in `sharp: 0` option that hides the
crisp frame-time copy while an element moves, leaving only the shutter
average. The default is unchanged; the inlined copies carry the same text.
* feat(registry): grow gooey-split blades out of swelling dots
* refactor(registry): tidy gooey-split pose code and document sharp 0 limits
* fix(registry): hide a sharp 0 motion-blur target by marker, not its visibility
Hiding through the element's own visibility fought timelines that
write it (autoAlpha): a target faded in after attach stayed hidden at
rest. A marker attribute and one !important rule now do the hiding, the
default path writes nothing, and a declarative sharp other than 0 or 1
is refused like any other bad option.
* fix(registry): smear a motion-blur target that was hidden when blur attached
Copies froze the target's computed visibility at attach, so a target
entering with autoAlpha never smeared (and vanished entirely under
sharp 0). The group now carries the target's current visibility each
frame, read with the sharp 0 marker lifted, and a copy keeps a
descendant's visibility only where it differs from its parent's.
* test(registry): pin a copy keeping a descendant's own visibility
* fix(registry): keep a child's own hiding in the smear of a target hidden at attach
* refactor(registry): restore a lifted visibility in one call and lift any hidden form
* fix(registry): restore a lifted visibility even when copying styles throws
* fix(registry): let motion-blur copies hide with a clip that has ended
The smear group set an explicit visibility on every moving frame, and
copies used to freeze an explicit visible, so an ancestor clip hiding
after the last update could not hide them: copies from a 0-4 s
sub-composition stayed on screen at 16.4 s. The group now sets a
visibility only where the element's differs from its parent's.
Also regenerates skills-manifest.json for the motion-blur skill doc.
* feat(cli): stream heard words on the transcribe --json progress stream
* test(cli): share the streaming transcribe helper
* fix(cli): keep install and download stoppable and whisper errors short while streaming
* fix(cli): let parakeet-mlx detect the language and name the Parakeet install under auto
* docs(cli): say what --language does under each engine
* docs(cli): whisper transcribes as the --language, it does not filter
* chore(cli): drop a comment the install-hint condition already says
* feat(cli): catch up on what the desktop app did since a project was handed over
The desktop app records each chat turn in .hyperframes/app-history.jsonl.
`hyperframes catch-up [dir]` prints the turns since the hand-off or the last
catch-up (what the person asked, what Framey did, which files changed) plus
the project files changed since, by the app or by hand, then marks them seen.
`hyperframes open` and Studio's Edit with Framey mark the hand-off moment.
Any command run on a project with unseen turns ends with a line naming
catch-up, so an agent back in the terminal hears about it even when it
forgets. /hyperframes and /hyperframes-cli tell the agent to catch up as
soon as the person writes again.
* feat(cli): hand a Grok terminal session over to the desktop app
Grok gives every tool command its session as GROK_SESSION_ID (grok 1.0.4),
so `hyperframes open` and Edit with Framey now name a Grok session in
.hyperframes/agent-handoff.json, after Claude Code and Codex. The desktop
app reads its turns from Grok's own session files.
* feat(cli): tell a new project's agent to catch up after the desktop app
Scaffolded projects' CLAUDE.md and AGENTS.md now say to run
`npx hyperframes catch-up` before the next change once the project was
opened in the desktop app. Claude Code, Codex and Grok read these files on
their own, so a resumed session catches up even when it never loaded a
HyperFrames skill: a resumed Claude Code session that had not, asked to
change the title, ran catch-up first.
catch-up also prints each reply on one line.
* fix(cli): read and write .hyperframes records without following links, mark seen after showing
Review (Jerrai, Rames):
- One helper (projectRecords.ts) now reads and writes every record in
.hyperframes/. A linked folder is refused. A read opens once with
O_NOFOLLOW and checks the open file is a plain file, which clears the
CodeQL race. A write goes to a temp file renamed over the record, so a
link is replaced, never written through. agent-handoff.json, the seen
record and the history all go through it.
- catch-up marks a turn seen only after showing it, and only up to the
newest turn shown, so a turn written just after the read shows next
time. Changed files have their own "checked" time.
- Turns show only once the project was handed over from this machine, so
a history file shipped in a cloned repo is never shown as the person's
words. The end-of-command check skips reading when the history is
older than the seen record.
- The skills mention catch-up only when the CLI printed a line naming it,
so an older CLI never meets an unknown command.
- The notice test uses basename, which fixes Windows.
* feat(cli): add the catch-up line to an older project's instructions on hand-off
A project scaffolded before catch-up existed has no line telling its agent
to catch up, and a resumed session that never loaded a HyperFrames skill
then edits without knowing what happened in the app. When `hyperframes open`
or Edit with Framey hands such a project over, its scaffolded CLAUDE.md and
AGENTS.md (they start "# HyperFrames Composition Project") gain the
template's line, in the template's place. A file of the person's own, a
link, a missing file, or one that has the line is left alone. The file is
rewritten through a temp file with its mode kept. A test keeps the line the
same as the template's.
* fix(cli): keep catch-up's cursor in the person's home, never reset it, read without links on Windows
Review (Rames, Jerrai):
- The seen record moved from the project to ~/.hyperframes/catch-up/,
keyed by the project's real path. A seen file a cloned repo commits
can no longer make its history read as the person's words.
- A second hand-off (`open` again, or another Edit with Framey click)
keeps the cursor, so turns the app recorded meanwhile still show.
- Reads lstat the path, open it once, and refuse a file whose dev/ino
differs from what lstat saw. That holds where O_NOFOLLOW doesn't
exist, fixing the Windows test. The note writer reads the same way,
which clears the new CodeQL alert.
- Writes use core's replaceFileAtomically, which retries a rename that
Windows briefly refuses.
- catch-up names every changed file, and the cursor is the newest `at`
shown, not the last line's.
- `open --json` carries `catchUp` after a hand-off.
- The bin scripts are back to mode 644.
* fix(cli): open before checking for CodeQL, never show a turn dated ahead
Review (Rames, Jerrai):
- readPlainFile opens the file first, then requires lstat to find the same
plain file (dev/ino) at the path, and reads only through the fd. That is
just as safe, and it clears CodeQL's check-then-use alert.
- catch-up skips turns dated after the moment it reads. So a cloned history
can't show a far-future turn as the person's words or park the cursor
past real ones.
- The seen key uses core's realpath (realpathSync.native), so letter case
can't split a project in two. ~/.hyperframes/catch-up is made 0700, and
a hand-off that can't note itself says so.
- Tests stub HOME with vi.stubEnv and restore it.
* docs(cli): map the pinned Sherpa runtime loading contract
* fix(cli): use a healthy pinned Sherpa copy beside the CLI
* chore(cli): keep runtime loader comments within the ratchet
* docs(cli): wrap the runtime contract palette declarations
* chore(cli): keep the Sherpa contract note out of the repo
* fix(studio): undo takes back the first edit of a film whose media is still being copied
* fix(studio): the first undo of a film repaints the preview in place
* fix(studio): background media copies keep the media ledger and survive a history prune
* fix(studio): a busy project history never makes writes wait for its lock again
* refactor(studio): name the restore stamping instead of commenting it
* perf(studio): undo stamps only a restore side that has no stamps
* fix(studio): closing a project history finishes its media copy first
* fix(studio): media copies stay resumable until recorded and skip a replaced folder
* fix(studio): the history lock wait comes back after a successful open
* feat(core): the renderer's font pipeline is importable from @hyperframes/core/fonts/embed
* style(core): format the font pipeline and state the bundled-subset invariant
* test(producer): repin the test lane map after the font tests moved to core
* fix(core): images build fonts once, CLI bundles one pipeline, font tests pass on Windows
* test(core): font retry tests get a budget no slow machine can exhaust
* test(core): the Retry-After budget test fails when the budget is ignored
* 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
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.
- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
then Canary) and points to the download when neither is installed. When run by Claude Code
or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
`hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate
Review of #4951 at 58934a34.
- One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says
`handoff`, and Studio's button reads it, so the gate cannot be half-flipped.
- While gated nothing opens and nothing is written: `openInDesktop` returns
`handoff-unavailable` (so `hyperframes open` prints the download line and the POST route
answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release.
- The POST answers only Studio's own same-origin request (loopback Host, matching Origin,
Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to
server/desktopRoutes.ts with their own tests.
- `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad
directory, and tells a missing app from one macOS could not open (`open-failed`). A project
that cannot be written still opens, with nothing handed over.
- Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft
and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`).
- The load smoke mocks the route.
* feat(cli,studio): hand off on macos now, and open the app on windows and linux
The released Mac app (b254) takes a handed-over folder and its conversation, so
HANDOFF_READY turns on for macOS. Windows and Linux stay on the download until the
desktop change that reads a folder from the command line ships.
- `hyperframes open` finds the app off macOS: the per-user install on Windows, and
on Linux the AppImage the app records in launcher.json; it starts it with the folder
- download links follow the machine: the DMG on macOS, the AppImage on Linux, and
nothing on Windows, which has no public download yet
- the Studio button shows wherever there is a download or an installed app; the card
no longer says "for macOS"
- every render prints the app line except batch rows: music-to-video delivers a draft
- the success line names the app that took the folder (Canary included)
* fix(studio): the edit-with-framey button asks its route through studioApiFetch
* fix(cli,studio): name the app the hyperframes desktop app, apart from the studio preview
* fix(cli,studio): fewer comment lines, so no file's comment share rises
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.
- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
then Canary) and points to the download when neither is installed. When run by Claude Code
or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
`hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate
Review of #4951 at 58934a34.
- One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says
`handoff`, and Studio's button reads it, so the gate cannot be half-flipped.
- While gated nothing opens and nothing is written: `openInDesktop` returns
`handoff-unavailable` (so `hyperframes open` prints the download line and the POST route
answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release.
- The POST answers only Studio's own same-origin request (loopback Host, matching Origin,
Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to
server/desktopRoutes.ts with their own tests.
- `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad
directory, and tells a missing app from one macOS could not open (`open-failed`). A project
that cannot be written still opens, with nothing handed over.
- Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft
and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`).
- The load smoke mocks the route.
* fix(studio): the edit-with-framey button asks its route through studioApiFetch
* 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
* fix(media-use): a slow download no longer leaves an empty asset that undo brings back
* docs(media-use): tell the person an OAuth sign-in makes voice and avatar video free
* docs(media-use): the OAuth free usage line names the monthly allowance
* docs(media-use): the OAuth line says a free allowance
* feat(cli): feedback says whether a person or an agent wrote it, with the email an app attaches
* refactor(cli): split the .env loader so each part stays simple
* fix(cli): a broken .env loader fails loudly; only a missing .env is ignored