* fix(ci): skills sync publishes past a version the registry holds unpublished
* fix(ci): skills sync fails unless a republished skill is the registry's latest
* fix(ci): skills sync republishes one patch past a hidden version and checks it as owner
* test(ci): skills sync fails the job on an error and stops after a clean sync
Two mutants survived review: dropping the line that sets the exit code (the job would print its ::error:: lines and still pass) and dropping the clean-sync return. One test runs the script with a failing fake clawhub on PATH and expects exit 1; another pins that a clean sync makes no further call.
* test(ci): the exit-code test can only reach its fake clawhub
The child's PATH was the fake's folder plus the inherited PATH, so on a machine where the fake cannot execute, a real logged-in clawhub further down would run sync for real. PATH is now the fake's folder alone.
* fix(ci): skills sync skips only hyperframes-creative's hidden 1.0.13
Republishing one patch past any taken version would publish past every future hidden version with no one looking. Only the acknowledged case (hyperframes-creative, 1.0.13 taken) is published at 1.0.14; any other taken version fails the sync as before.
* test(ci): the skip allowlist matches the skill, not just the version
A taken 1.0.13 on any other skill must fail the sync too; without this case, accepting 1.0.13 for every skill passed all tests.
* refactor(ci): skills sync picks the republish version in its own helper
The allowlist check took republishTaken to cyclomatic 5, which the Fallow audit scores at its 30.0 CRAP threshold; deciding the version in versionPastSkippable keeps each function under it. Behaviour unchanged.
* test(producer): unit tests never reach the network
* test(producer): the unit lane refuses sockets to other hosts, not only fetch
* test(producer): refuse outside hosts at the lookup, not by parsing socket calls
* test(producer): keep dns.lookup promisify-compatible under the no-network guard
* fix(core): namespace SVG ids in runtime-mounted compositions
* fix(core): keep SVG selectors and captured ids working after loaded scenes rename them
* chore(core): generated artifacts carry a plain do-not-edit banner
* test(catalog): record the font subsets the new scene-script installer requests
* test(catalog): keep earlier mirror recordings, as re-records only add
* fix(catalog): 3D previews load their scripts under the docs host's script policy
* fix(catalog): preserve script text through HTML parsing
* docs(catalog): describe the shared script bundle
* fix(engine): a render fails when a composition script throws before its timeline binds
* fix(engine): an unrelated page error no longer fails a render whose timeline registers late
* fix(engine): only the composition's own scripts fail a render, local or distributed
* style(engine): format the script failure change
* fix(engine): a third-party browser rejection no longer fails a render
* refactor(engine): one page URL for navigation and error attribution
* fix(engine): a rejection after a hash change or in an iframe no longer fails a render
* fix(engine): a widget's error no longer fails a render once its script is inlined
* fix(core): name injected and inlined scripts without building code from library input
* fix(engine): a third-party embed's eval or inserted script no longer fails a render
* fix(producer): a widget's inline handler or a sub-composition no longer misreports a render
* refactor(engine): read each script response once and keep comments within 120 characters
* fix(engine): a scene that throws after registering its timeline no longer cuts a render short
* fix(engine): a composition script failure names the error and its real cause
* fix(engine): a failed VFX chain fails the render at once instead of shipping a missing layer
* fix(core): a VFX layer that is empty on one frame renders empty instead of failing or going stale
* fix(core): a VFX layer that grows from zero size renders instead of staying missing
* fix(core): a VFX layer hidden or late while empty renders once it has a size
* test(producer): cover a VFX layer that is 0×0 under a host that starts later
* test(producer): pin both VFX 0×0 marks, with a GSAP-shaped wipe and a real late host
* test(producer): a late VFX host with a GSAP wipe catches a walk that skips hidden hosts
* 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(core): bundled local scripts keep their own place, defer and async
The bundler joined every local <script src> into one inline script at the first one's position. That dropped defer and async (a deferred main.js ran before a deferred CDN gsap), moved later files ahead of CDN scripts between them, and let one file's error stop the rest. Each local script is now inlined where it stands with its attributes; a deferred or async one keeps its tag with its file as a data: URL. Inline-script merging also no longer folds in nomodule scripts, which dropped the attribute.
* fix: preview scripts run after the runtime when the bundler fails, and inlined files are escaped
- When the bundler returns nothing or throws, the preview places the runtime
where the bundled page does (end of head) instead of after the composition's
scripts or not at all; core's placement moves to insertRuntimeTag.
- Every HTML-spec JavaScript MIME type counts as a classic script for merging
and the web-font gate.
- Inlined local files and merged runs are escaped with escapeInlineScriptSource.
* fix: deferred local scripts run late without data: URLs, and authored runtime reads survive
- Local defer/async scripts are inlined in their own tag with the attribute
kept; the font-gate runner (and its old-runtime fallback) runs an inline
classic defer script after the classic scripts, in document order, so a page
policy that allows inline scripts but not data: still runs them.
- stripEmbeddedRuntimeScripts matches the bootstrap attribute and runtime file
names on the tag, not runtime words in an authored script's text.
- The preview applies its runtime on every path, so an adapter page without
one still gets it; the inline escaper writes <!-- as \x3C!--, valid in a
unicode regex.
* fix: a deferred inline script waits for the deferred CDN script before it to load
* fix: an authored script that mentions the runtime marker no longer stops the runtime being added
* fix: only inlined deferred files run late, head ones included, and renders skip a CDN runtime link
* fix: deferred head files run as before, and one selector owns the deferred-file rule
* fix: a local file's legacy <!-- comment no longer breaks its merged run, and escapes keep case
* fix: a local file inlined on its own keeps working with a legacy <!-- comment
* test(core): a local non-JavaScript file keeps its text as written
* docs(skills): resolve cross-skill contradictions from the doctrine audit
Section 1 of #5025, each direction taken from the code or the shared
contract:
- async timeline build is supported (runtime rebinds an async-registered
timeline; lint only flags registering before the build finishes), so
animation and keyframes now say register after the build completes
- data-track-index is a Studio row, display only; layering is z-index
- the final gate is `check`, which reruns lint
- music-to-video keeps render approval as the one autonomous question,
per the brief contract
- faceless-explainer and product-launch-video pre-install registry blocks
before dispatch, like pr-to-video
- talking-head-recut layout files cover landscape and portrait; 4:5 is
derived
- embedded-captions Cinematic authors cinematic.json; make-cinematic.cjs
derives plan.json
- audio carve has no static mode; remove the stale static and dynamic text
Left open: the figma usage beacon and the skills-update consent rule.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore(skills): refresh manifest; treat cinematic.json and safe-zones.json as project files
Both are written into the user's project by the embedded-captions workflow,
so the comment-citation check should not look for them in the repo.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* 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
* feat(registry): eight ad templates show their video preview in the catalog
* refactor(scripts): the preview backfill reuses the shared entrypoint guard
* ci: a test-only change under studio or player needs no captures
Tests are not behaviour a user sees, like the markdown the check already skips.
A bench-harness PR (#4953) was asked for Before and After screenshots.
* ci: pin a mixed source and test change, and say tests are not counted
* fix(ci): let markdown-only changes under studio and player skip the captures check
* docs: map the Studio edit path, the edit accuracy bench and test setup for agents
* docs: name the Studio captures rule and the local comment checks
* docs: point the Studio gate notes at the scripts that own the rules
* docs: correct the Studio save paths and add the producer stage map
* docs: name where producer sizes workers and which captures encode inline
* feat(registry): add cinemagraph — a still photograph with one living region
* feat(registry): add ai-answer-rank — a mock AI answer that re-ranks to put your brand in it
A 1080x1080, 9s block for answer-engine and AI-visibility marketing. A prompt
types into a chat card, the assistant answers with a ranked list of three picks,
then re-ranks: the promoted brand slides into its slot (brandRank 1-3), the picks
below step down, the third fades out of the list, and the brand row lands with
the one accent highlight and a drawn check. Monochrome placeholder UI; prompt,
lead-in, pick names, brand, rank, accent, ink and ground are variables.
* feat(registry): add caret swap text effect
* feat(registry): add pendulum-swing component
Add a motion-primitive component that hangs content from a fixed pivot and
lets it settle with a damped oscillation, like a hanging sign coming to rest
on its hook.
The swing rides a single unitless custom property (--hf-swing-angle) that the
timeline eases 1 -> 0, so only rotation animates and it stays sub-pixel under
the seek-by-frame capture engine. The demo drives it with a damped-oscillation
ease (a cosine inside an exponential decay, normalised to land exactly upright)
so the sign passes through center several times, each pass smaller, before it
rests.
Four enum variables: pivot (top-center/left/right), amplitude
(gentle/standard/wide), direction (right/left), tone (ink/paper/accent, each a
bg/fg pair to keep AA contrast on any frame). The snippet is recipe-only and
does not own its timeline; its demo carries the same variable declaration.
Ran npx hyperframes lint/validate on both files, regenerated registry.json via
scripts/generate-registry-items.ts and the docs via
scripts/generate-catalog-pages.ts.
* fix(registry): drop hand-added cinemagraph index entry; catalog-publish adds it
* style(registry): format pendulum-swing with oxfmt
* fix(registry): keep pendulum-swing demo caption clear of the swinging sign
* fix(registry): ship the ad-template editing contract for ai-answer-rank
---------
Co-authored-by: Carlos Miranda Levy <carlosmirandalevy@gmail.com>
Co-authored-by: tugrawork-creator <319556794+tugrawork-creator@users.noreply.github.com>
Co-authored-by: Akshar Patel <aksharcommit@gmail.com>
Co-authored-by: rottenpen <12029924+rottenpen@users.noreply.github.com>
* fix(lint): fail skill lint on unresolved doc cross-references
scripts/lint-skills.ts now checks every *.md under skills/, .claude/skills/
and .agents/skills/ for cross-references that do not resolve: inline links
[text](target), reference definitions [id]: target, and backticked paths
that are explicitly relative (./ or ../) and end in .md or .html. Targets
resolve against the referencing file's directory and must exist; a #anchor
on a Markdown target must match a GitHub-style heading slug of that file,
and same-file #anchor links are checked against the file's own headings.
Fenced code blocks, URI-scheme targets, absolute paths and placeholder
targets ({ } * $ < >) are skipped. Bare backticked paths without ./ or ../
are a documented blind spot: on the current tree they are mostly skill-root
shorthand, another skill's file, or a runtime artifact, and checking them
produced ~200 hits that are not wrong.
The former top-level main now runs only when the script is the entry point,
so importing the exported checkers from the test file no longer exits the
test runner when the tree is red.
On the current tree the rule reports 34 violations across 16 files, all of
the shape described in #3919 (../ one level too shallow for the referencing
file, or a file that does not exist). The docs are corrected separately.
Refs #3919
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(lint): trim the cross-reference rule's comments and inline its reporter
* docs(lint): cite placeholders, not invented paths, in cross-reference comments
* refactor(lint): keep the skill lint entry point under the complexity limit
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(release): use portable Git paths in release guards
* fix(release): drop the comment on a path git never compares
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(studio): resizing an element GSAP only positions saves its CSS width and height
* fix(studio): a resize never saves GSAP's scale mask as the element's CSS
* fix(studio): keep the resize size writer inside the file and comment budgets
* fix(studio): a GSAP-positioned resize saves its CSS size before the GSAP write renders
* fix(studio): undoing a crop leaves a GSAP-animated layer where its file puts it
* test(studio): an undo on a GSAP-parsed layer in a two-script file reloads the preview
* fix(studio): a move keeps an authored xPercent centring set in the saved file
* fix(studio): a drag on a centred element no longer flashes off-centre on release
* fix(studio): a font pick saves the edit and its @font-face in one write
* fix(studio): a focused combobox, switch or plain-text editor counts as an unsaved edit
* test(scripts): pin that a slash after i-- before a quoted slash is division
* fix(studio): an inspector edit saved while a preview reload loads stays on screen
* fix(studio-server): a font save accepts exactly one @font-face rule and nothing after it
* fix(studio-server): a font name or path with braces in quotes still saves its @font-face
* fix(studio-server): a CR or form feed in a quoted font name cannot end the @font-face rule
* test(studio): bank the 99 edit accuracy cases the GSAP fixes now pass
* feat(cli): one owner for init media placeholders, audio stays on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): init keeps a video's sound on the clip; silent files scaffold muted
Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com>
Co-Authored-By: kapelame <kapelame@users.noreply.github.com>
* feat(registry): example templates keep the a-roll's sound on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): video audio rules recommend keeping sound on the video
Update video_missing_muted and video_audio_double_source message and
fixHint text to recommend the new default: keep audio on the video with
data-has-audio="true" (not muted). Tests verify the new wording is in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): a video is either audible (data-has-audio) or muted, never both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): follow the compiler's audio rule in the audio-track toggle
Confirm before background removal drops a video's sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): hyperframes-core teaches sound on the video by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): workflows and scaffold guidance keep a video's sound on the clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: footage with sound keeps it on the video clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): init from-file a-roll is timed, muted strip is attribute-exact
The from-file a-roll video gets data-start="0" so a scaffold lints clean, and
the orphaned audio-only slot comment is dropped. setVideoAudio now walks the
tag attribute by attribute, so a class value containing the word muted
survives. The ffprobe-missing warning says the video is assumed to have sound
and how to switch to muted, and DEFAULT_META notes the failure surfaces at
render until lint support lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep has-audio in step with the muted toggle, test cutout confirm
Muting a video now clears has-audio and unmuting sets it to true, in both the
Design and flat media panels, matching the Has audio track toggle. Audio
elements are unchanged. Adds coverage for both panels plus the cutout
window.confirm guard: declined leaves src alone, accepted swaps src and mutes,
a silent video never prompts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false
An audio element or an undefined hasAudio no longer picks up a muted attribute;
only an explicit false on a video writes muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: video_missing_muted rule text, silent-footage notes, consistent audible examples
lint.mdx and cli.mdx describe the current video_missing_muted rule instead of
the retired unmuted-video wording. The editing recipes say how to adapt for
silent footage and require the outgoing clip's sound to end at a J-cut. The
Remotion media example shows the audible mapping consistently, data-attributes
states that has-audio is required unless muted, and the hairline frame note no
longer claims the video is muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): encode init media filenames and probe --audio duration
Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded.
--audio now passes the probed track length as the composition duration in both init paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): audio-state-aware media rules and time-aware double-source check
video_missing_muted honors any explicit data-has-audio.
video_audio_double_source only considers audible videos and only fires
when the same-src clips overlap in time.
duplicate_audio_track now covers audible videos.
audio_file_without_element no longer claims silence when an audible video
carries the sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): audio-stream check for remotion ports, followable J-cut recipe
Remotion Video maps to data-has-audio only when the file has an audio stream.
The J/L-cut recipe now shows both shots with muted pictures and separate audio.
embedded-captions removes the audio.mp3 scratch file it extracts.
skills-manifest.json regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(lint): trim comments to satisfy the comment ratchet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): atomic muted edits, cutout keeps sound on a linked audio
Muted and Has audio track commit one batched write (one persist, one
undo entry) and remove data-has-audio instead of writing an empty
marker. Unmuting a video probes its src: audible -> true, silent ->
false, probe failed -> absent. Remove background on an audible video
inserts a linked sibling <audio> with the original sound in the same
write, replacing the drop-the-sound confirm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(producer): skip hidden videos when auto-adding video audio
A video hidden in Studio is dropped by the mixer, so its src never reached
existingAudioSrcs and the render re-added it at full volume. The video parser
now records the shared hidden predicate and the auto-detect step skips hidden
videos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep useDomEditSession under the 600-line cap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com>
Co-authored-by: kapelame <kapelame@users.noreply.github.com>
* ci(studio): measure drag smoothness on a quiet runner, with a blank-page control
* ci(studio): gates read the base the run measured, not the base branch tip
* style(ci): format the base-file test
* fix(scripts): count comments after regex literals in the comment ratchet
* fix(scripts): keep comments after JSX tags, ++/-- and wrapped division in the ratchet
* feat(studio): the code editor follows studio's light and dark theme
Replaces One Dark with a Studio editor theme whose colours are theme.css variables: eight syntax role tokens (One Light and One Dark hues held at 4.5:1), editor surfaces from Studio's roles, all measured by the contrast ratchet.
* test(studio): the code editor theme takes every colour from a theme.css token
* fix(studio): code stays at 4.5:1 when a selection or search match sits on the active line
Syntax tokens are tuned for the selection over the active line, which the ratchet now measures; matches are outlined rather than stacking another wash; the search panel's pressed button and special characters follow the theme; unused tooltip rules go.
* fix(studio): code matches carry no background, and types read apart from constants
Search and selection matches are outlined only, so CodeMirror's lime match wash and stacked washes are gone; type and constant tokens split by hue in both themes.
* fix(studio): the current search match reads apart by width, not colour alone
A 2px accent outline on the current match and a muted 1px outline on other copies of the selected word.
* feat(studio): graphite colour system with a light theme that passes contrast
Studio's tokens take the Graphite values: a lifted dark, a light set behind
data-theme="paper", the player dark in both. Every text role clears 4.5:1 and
every icon and ring 3:1 on the fills it sits on, held by the contrast ratchet.
* fix(studio): every studio surface follows the light theme and passes contrast
The dock no longer inherits dockview's forced dark scheme, popovers sit on the
raised surface, stock white, red and amber text map to roles, the keyframe
diamonds and track headers use roles, and the contrast ratchet measures every
pair in both themes (150 pairs).
* fix(studio): stock white, red and amber stay colours; studio text and fills use roles
Round-two review: white, red and amber are colour content as well as text, so
they keep Tailwind's values and hosts keep their palettes. Text, toasts, the
feedback card, the FX popover, destructive buttons, category labels and the dock
take Graphite roles instead; the contrast gate rejects an unparseable colour.
* fix(studio): every light-theme ink, icon, dot and focus ring holds its contrast
Stock white, red, amber and file-type hex left in Studio chrome now use roles or a light-dark pair; the file tree icons carry a contrast test.
* chore(studio): drop two comments the names already carry
* fix(studio): red marks, teal outlines and colour curves hold contrast in both themes
Danger graphics use the danger ink, file tree outlines use the accent ink, the colour-curve well stays dark like the scopes, and six timeline token pins come back.
* fix(studio): invalid readouts, the curve focus ring and editor dots stay distinct in both themes
The invalid readout border is dashed as well as red, the curve graph's ring sits inside its dark well, the radius dots and ease handle centre follow the theme.
* refactor(studio): ease and radius editors use colour classes; the dashed invalid border has a test
Simplify pass: the ease editor's inline accent styles become stroke and fill classes, the radius dot class is computed once, the ratio check is one expression, and the alias table is named for what it holds.
* fix: readers never see an empty or half-written project file
* test: pin the create routes and block install to whole-file writes
* fix(sdk): a write through a link to a missing file creates that file
* fix: saves through a linked folder reach the real file, atomic writes handle long names
* test(sdk): check for the stray file, not the folder listing
* fix(core): creates work on Windows FAT drives, and links through folders resolve as the system does
* fix: agree the helper move in the deletion check, and match temp paths without a path regex
* test: check POSIX file modes only off Windows
* feat(catalog): shelve 3D objects and cursors in their own catalog sections
Carries the generator grouping rules from #4310 and #4308.
* feat(registry): add a starfield ambient overlay
From #4313.
* feat(registry): add a falling snow ambient overlay
From #4331.
* feat(registry): add a torn-paper-rip transition
From #4321.
* feat(registry): add a paper-collage-reveal transition
From #4322.
* feat(registry): add a curtain-reveal transition
From #4323.
* feat(registry): add 3D objects: product turntable, glass hero, exploded stack, extruded logo
From #4310.
* feat(registry): add eight cursor animations and the dart cursor
From #4308.
* fix(catalog): find items by everyday editor words and accept a bare search word
From #4311.
* chore(registry): remove the simulated-cursor component
The Cursors section's new pointer animations replace it (from #4308).
* fix(registry): take cursor-zoom-follow's screenshot from runtime variables only
The declared default is empty, so page markup never becomes an image URL.
Clears the code-scanning alert raised on #4308.
* docs(cli): show a catalog search example that finds items
"smoke" matches nothing in the catalog; "crossfade" finds fade-through.
* chore(ci): allow the simulated-cursor removal in the deletion guard
* fix(ci): skip tracked files missing from disk in the player CDN pin check
Catalog regeneration removes a deleted item's pages before the publish PR untracks them,
so the check crashed on every catalog deletion PR.
* chore(cli): drop comments that narrate catalog search history
* fix(registry): drag-drop keeps the family arrow and shows the drop slot only while hovering
* fix(registry): agent cursor labels its steps in an accent tag, no glow or sparkles, in 4.8 s
* fix(registry): annotate cursor draws a tapered marker stroke and clears the circled value
* fix(registry): show real file thumbnails and end the context menu demo sooner
* fix(registry): give marquee select real photos, a Finder-blue marquee and a tighter loop
* fix(registry): centre the text-select toolbar on the selection and tighten its timing
* fix(registry): selection cursors default to the macOS system blue
* style(registry): format the agent and annotate cursor sources
* fix(registry): dart-cursor stops covering the choice it makes and shows the export finishing
* fix(registry): frame the product turntable fully and light it with two studio softboxes
* fix(registry): touch-indicator taps like a real phone and the post it taps gets liked
* fix(registry): press-ripple confirms with a check and small click rings at the cursor
* fix(registry): exploded stack separates at once and marks each layer in blue as it is labelled
* fix(registry): extruded logo settles facing the camera, set in Geist Bold with a neutral rim
* fix(registry): magnetic cursor docks its toolbar on the selection and ends after the press
* style(registry): format the exploded stack source
* fix(registry): glyph trail becomes a tutorial cursor path with numbered steps on a settings screen
* fix(registry): make glass-hero a clear lens that shows the headline through it
* fix(registry): snow reads as real snowfall with bokeh flakes over a winter title card
* fix(registry): oversized cursor is a 3.5x arrow that clicks a real editor toggle on
* fix(registry): starfield reads as a drifting night sky over a real title card
* style(registry): format the glass hero source
* fix(registry): make the paper tear show a white fibrous edge and lift the torn sheet away
* fix(registry): curtain reveal draws velvet folds that gather onto a title card
* fix(registry): make every zoom-follow click change the screen and read at zoom
* fix(registry): paper collage pieces stay opaque, tear with deckled grain and lift away on an arc
* refactor(registry): drop dead code and correct stale comments in reworked items
* fix(registry): agent cursor scrubs cleanly, docs match press-ripple, fictional demo names
* docs(registry): catalog notes say oversized-cursor exits and how it differs from press-ripple
* docs(registry): plain lowercase comments in oversized-cursor and touch-indicator
* docs(registry): oversized-cursor names CLICK_AT as the click cue
Ctrl+C during a render left its whole work dir of captured frames (and a
staging dir) in the renders folder. Puppeteer's default SIGINT handler kills
Chrome and exits the process before the render's own cancel path can remove
them. The engine keeps that default unless the host owns Ctrl+C; the CLI's
render cancellation scope claims it while a render runs, so the render aborts
and cleans up after itself.
A render killed outright (SIGKILL, crash) cannot clean up, so each work dir and
staging dir now records its owner process (pid, hostname, boot, and on Linux
the pid namespace), and the next render into the same folder removes
the ones whose owner has exited.
Cancelling a render while its browsers start no longer leaves anything
running that kept a batch render alive after Ctrl+C: the BeginFrame warm-up
loop, the probe browser and file server, or a capture browser whose launch
finished after the cancel (now closed before the worker settles, since
Puppeteer's exit hook skips every other live browser).
* fix(docs): restore visible example video cards
* fix(docs): use catalog player for example films
* fix(docs): fit player posters before playback
* fix(docs): preserve the original examples grid
* fix(producer): gif renders survive rgb frames among transparent ones
* test(producer): run the gif mixed-frame encode test in the ffmpeg lane
* fix(producer): gif encode test passes the argv, reachability and comment checks
* fix(producer): transparent gifs clear every frame so an opaque frame never lingers
* fix(producer): only gifs that turn translucent after an opaque frame get whole cleared frames
* fix(core): plain text keeps its own gif control block; gif encode failures share one shape
* fix(release): name Release once in a changelog entry's tags
* fix(release): the release tag appears once in any case; four past entries lose the duplicate
* fix(producer): renders use the font named in a var() fallback
When a font-family starts with var(--x, A, B) and --x is not defined, the
render now reads A, B the way the browser does. A system font at the head of
that fallback gets Inter placed in front of it, the same rule a direct
system-font stack already follows. The rewrite stays inside the var()
fallback, so a defined --x still wins. A defined property whose value is
itself a var() is followed too. The distributed plan check reads the same
answer.
Fonts named only in such a fallback are best-effort: they are embedded when
they resolve, and one that does not resolve is logged instead of failing a
distributed render. Fonts named directly, or through a defined property,
still fail closed as before.
CSS-wide keywords such as inherit are no longer treated as font names to
fetch.
Compiled the way a distributed render compiles them, 28 registry items now
embed a font they name this way, and none newly fails.
* refactor(producer): fold var() fallback resolution into one branch
* fix(producer): fonts behind a var() chain or empty fallback add no new render failures
A family reached through a chain of defined var()s is optional like a
fallback-only one: fetched fail-closed, skipped only when no source serves
it. An empty var() fallback makes the declaration invalid, so the browser
inherits and the plan check no longer rejects the families after it.
* test(producer): a family named after a leading var() stays required
* fix(producer): a transient error on an optional font warns and renders the fallback
* chore(catalog): record the font requests var() fallbacks now make
* fix(producer): required fonts fetch before optional ones can spend the time budget
* fix(cli): commands exit without waiting on update checks or telemetry
The update and skills checks run in a detached child that refreshes the
caches for the next run; the parent only reads the caches. finalizeCli no
longer awaits the telemetry flush: the exit handler flushSync() delivers
what is queued. The events beacon, feedback and figma error paths stop
awaiting flush() too.
* fix(cli): retry a failed background check at most once an hour
Before starting the background worker, the CLI stamps a per-check attempt
time and skips a check it attempted within the hour, so a dead network no
longer starts a worker on every command. The stale-pin notice writes onto a
fresh read, detached spawns set windowsHide, and a test ties the worker path
to its build entry.
* fix(cli): a failed-check stamp dated in the future no longer silences update checks
* feat(plugins): ship portable agent bundles and installation guides
* fix(plugins): align new manifests with current release
* test(plugins): split caption fixture setup and cleanup
* test(plugins): pass render fixture path as an argument
* test(cli): CLI tests run with a home folder of their own, never the user's
* style(cli): format the test home check
* test(cli): the skills test lane also runs with a home folder of its own
* style(cli): format the test home check
* ci: the test reachability map follows the skills lane's new command
* fix(studio): reload the preview only when a file it loaded changes
* test(studio): read the owner id from the new change-owner shape
* refactor(studio): drop a comment the parser name already says
* fix(studio): keep reloading until the preview is built, and count what its document names
* style(studio): format the reference pattern and name the kept reload
* fix(studio-server): scan preview references in linear time and refresh all thumbnails on a merged reload
* fix(studio): keep a waiting film change when a file the preview never loaded arrives behind it
* test(studio): name the preview ownership test for what it checks
* chore(ci): allow the watchIgnore removals and trim the new comments
* fix(studio-server): reload the preview when a folder holding a file it loaded moves in or out
* perf(studio-server): check a changed folder in one lookup and cover every reference form
* fix(studio-server): record read folders for a project at the filesystem root
* fix(studio-server): reload the preview when the file a symlinked asset points at changes
* fix(studio-server): resolve symlinks on every preview read and key projects by their real folder
* fix(studio-server): record every link a preview read passes through, resolved as the system does
* fix(studio): an automation lane edit is refused on a locked clip and reports how it saved
Lane drags, Delete and paste save through the timeline's gated clip or group
save instead of the property panel's commit, so the host's canEdit applies and
the commit resolves saved, refused or failed. A landed save refreshes the lane
store and the panel's selection, as the panel's commit did.
* fix(studio): a refused automation save puts the preview and the lane back
A save that does not land (recording, no id, a failed write) reverts the live
preview it was given, the lane drops a draft whose save came back refused or
failed, the no-id and no-project failures toast like the others, and the panel
refresh reads the current selection. The panel commit's unused coalesce option
goes, and the lane value reuses automationAttrValue.
* fix(studio): a failed save restores the preview it was given, not an older one
Each timeline save claims the preview's before-value when it starts, so a
gesture begun while it is in flight records its own and a later failed save
cannot put back a value from before an earlier save landed.
* chore(repo): allow the automation gesture key files this branch removes
* fix(studio): a refused automation save leaves the lane store and paste mark as saved
Reverting a live value re-syncs the lane store from the restored preview, so
an overlapping save that landed first cannot leave the store holding a later
value that failed. A paste whose save is refused or fails gives back the
selection and paste chain it replaced.
* fix(studio): a failed save settles on what the file holds and a refused paste marks nothing
A save that does not land settles the preview and store on the file's value
(the claimed before-value only when the file cannot be read), and leaves a
gesture begun meanwhile alone apart from correcting its before-value, so two
overlapping failed saves cannot leave either on a value that never landed. A
paste selects and marks its span only once it lands and only over the
selection it was pasted at.
* fix(studio): a failed save reads the file only after writes queued to it land
The settle read goes through the same per-path queue as the writes, so a save
that fails while another is queued settles the preview, the store and any
newer gesture's before-value on what that queued save wrote.
* fix(studio): an older failed save never settles over a newer edit on the same lane
Live-preview bookkeeping moves to createLiveLanes, one owner for both timeline
saves: each lane key carries a generation that every live write and save start
advances, and a save settles the preview and store (on its landed value, or on
the file after a failure) only while it is still the newest. A paste marks its
span only if it is still the latest paste.
* test(studio): a newer live drag keeps its preview when an older save recovers
* fix(studio): every finished save records what the file holds, even under a newer edit
A save's settle now always writes the file's value (landed, or read back after
a failure) into the lane store and a live gesture's before-value, and moves the
preview only while nothing newer has touched the lane. The clip store is set
from that value rather than copied from the preview. A paste marks its span
only while its clip is still the selected one.
* fix(studio): an older automation save never overrides a newer gesture on its lane
A failed save's recovery read could settle after a newer save started, and the
save's own optimistic preview patch and failure unwind ran over a newer drag.
Every preview write a save makes now goes through the lane's ownership check,
only the newest save records its value, and a refused drag hands the lane back
to the newest pending save.
* fix(studio): timeline attribute saves land in the order they start
An attribute save read the file before joining its write queue, so an older
save with a slow read could land after a newer one, overwrite it on disk and
leave the lane showing the newer value. The save now joins the queue first and
reads, patches and writes inside it. A save moves the preview only while it is
the newest save and no drag is live, which replaces the per-lane generation.
* refactor(studio): name the in-queue history write for the queue it needs
* test(studio): pin save start order and a newer release's preview mid-flight
* fix(studio): a drag keeps its points when an older save lands under it
An older save that landed mid-drag wrote the lane store, the lane dropped its
draft, and the release committed the older points. The lane now keeps its draft
while a gesture is live. A save that cannot read the file back settles on the
last value read or saved on its lane, never on an unsaved one, and an older save
that lands records its value there even when a newer save is pending.
* fix(studio): a lane's first failed save still puts its drag back
With the claimed-value fallback gone, a first save on a lane that failed and
could not read the file back settled on nothing, leaving the drag playing. A
save begun with no other save pending now records the value under its drag as
verified, since that is what the file last settled on.
* fix(studio): only a before-value read with no save pending counts as saved
The value under a drag was trusted when its save started with nothing pending,
but it was read when the drag began, possibly over a paste that later failed.
A lane now marks a before-value clean only when no save was pending as the drag
began. A group lane's preview carries its store mirror, as its live writes do.
* fix(studio): a lane's saved value never crosses into another project
The lane bookkeeping outlives a project switch and its keys had no project, so
a value saved in one project could settle into another project's clip with the
same key when a save there failed and could not read the file back. Lane keys
now carry the open project, and a save moves the preview or store only while
the project it was made in is still open.
* fix(studio): a failed save reads its file back from its own project
A save that failed after a project switch read its recovery from the newly
opened project and recorded that project's value as its own. Each save now
captures its project when it starts and reads back from it.
* fix(studio): a failed save never brings back a value an undo removed
A lane kept its last verified value after its saves settled, so after an undo a
save that failed and could not read the file back settled on the removed value.
The value now lives only while a save on the lane is in flight, and each save
records what its own in-queue file read found. The group preview's store mirror
is no longer needed and is gone.
* test(studio): pass the file-read callback in the direct attribute-save tests
* fix(studio): a drag whose before-value a save settled counts as clean
When a settle rewrites a live drag's before-value to a verified value, that
value is known saved, so the drag's own save can fall back on it even when its
read and read-back both fail. Adds a test for a failed save reading back from
its own project while another save is in flight across a switch.
* fix(studio): a save started in one composition never lands in another of the same project
Live lanes were scoped to the project only, so a save held across a switch
to another composition of that project settled its value into the new
composition's preview (clip and group lanes) and store (group lane).
Lanes are now scoped to the project and the composition open when the save
starts. createLiveLanes moves to its own module for the 600-line cap.
* fix(studio): an undo during or after a lane gesture stays undone when the next save fails
A live gesture records the lane's value before it starts, and a later save
that cannot read the file settles on that value. An undo mid-drag, or after a
gesture that ended without saving, left that value stale, so the save brought
back what the undo removed. Every undo and redo now re-reads each open lane's
value from the files it restored.
* fix(studio): undoing group automation clears it from the members' lanes at once
The group-info cache only noticed a change when its MutationObserver callback
ran, so the soft restore's store sync, which reads it synchronously, mirrored
the pre-undo group automation onto the members. A read now counts mutations
the observer has not delivered yet.
* fix(studio): an undo reaches every lane and pending save it rewrote, in any composition
The undo re-read only covered lanes in the open composition and live gestures.
History is project-wide, so undoing from another composition left the
rewritten composition's lanes on the pre-undo value, and a save released while
the undo was in flight kept that value as its fallback. The re-read now covers
every lane of the project on a restored path, including saves still pending,
and an undone file it cannot read makes the value untrusted instead of dropped.
A missing element reads as unreadable, the same as a save's read-back.
* fix(studio): a cancelled fade, stretch or preset audition leaves no stale lane value behind
A cancel put the gesture's start back through a live write, which left the
lane's before-value in place until the next save trusted it, even after the
property panel, the source editor or an agent had changed the file. The fade
cancel, the edge-stretch cancel and leaving a preset audition now end through
the lanes' revert, so a before-value exists only during a live gesture.
* test(studio): pin that a group write is read back fresh in the same task
* style(studio): drop two comments the names already carry
* fix(studio): a failed group save reads its own file back; fade cancels work without revert
- The group save's failure read-back used the file of whatever composition was
loaded when the write failed. It now reads the file captured when the save
started, as the clip save does.
- A fade cancel writes its start back live before it reverts, like the other
cancel sites, so a host without the optional revert callback still puts the
fade back.
- The edge-stretch cancel comment says what the code now does.
- The save test harness passes the revert callbacks, as the app does; without
them the cancelled-gesture tests exercised a host that lacks them.
* refactor(studio): trim three long comments on live automation saves
* fix(cli): --yes in an unattended run no longer overrides a no saved by another process
* fix(cli): every settings writer takes the lock, and --yes needs a real terminal to reverse a no
* fix(cli): the thin-results download offer also needs a real terminal
* fix(cli): one owner-checked settings lock, and an unreadable settings file never counts as a yes
* fix(cli): settings lock removals go through a reaper, and a non-boolean answer counts as a no
* fix(cli): a settings lock that cannot be released no longer fails a write that landed
* fix(cli): a leftover settings lock is never taken over, and doctor names it
* fix(cli): a background update that cannot be recorded is not launched again every run
* fix(cli): every on-device install re-reads the saved answer first, and an unsaved yes installs nothing
* fix(cli): the thin-results offer stops when a yes cannot be saved
* fix(cli): only the consent-checked step installs the on-device runtime, and a leftover lock warns once
* fix(cli): the leftover-lock warning follows the attended-terminal rule
* fix(registry): carousel cards fall back to their sample image inside the assets folder
When an image variable resolves to nothing, the 25 carousel blocks loaded the
sample card by bare file name, which 404s beside the block and draws a white
card. The fallback now keeps assets/carousel-images/, the folder every card
image is installed into. A guard test fails on any block with a bare fallback.
* test(registry): the carousel fallback guard covers every block that uses it
The guard now picks each block that reads an image variable and fails when its
fallback no longer matches, instead of silently skipping it, and ignores stray
files in the blocks folder.
* fix(media-use): tests never write into the person's media library
* fix(media-use): guard the media home only in this repo's test runs
* test(media-use): probe the media home from a plain child process
* fix(media-use): keep miss logging best-effort when home is unresolvable
* test(media-use): miss logging survives an unresolvable media home
* docs(media-use): describe the media library without naming an app
* chore(media-use): refresh the skills manifest hash
* ci: stop running CodeQL on merge queue groups, where its upload races the merge
The code scanning rule does not apply to merge queue groups, and the queue deletes
a group ref when it merges, so the upload failed on about half the groups.
Pull requests and pushes to main keep scanning.
* ci: catch merge_group in any trigger form and say the upload race is intermittent
* feat(studio-server): an undo taken from Studio is not mistaken for an outside edit
* feat(studio): undo and redo step through the project history, whoever made the change
* feat(studio): the dev server keeps each project history, in the same folder as preview
* fix(studio-server): an undo that deletes a file reads as Studio's, and dev histories close
* fix(studio-server): a claim keeps an agent's earlier write the agent's and takes back Studio's own
* chore: allow the removal of Studio's browser edit history files
* test(studio): mock the project history in the runtime smoke
* fix(studio): step past a jammed undo and say why a step fails
* fix(studio-server): an agent turn cut by a claim commits its part before the claim
* fix(studio-server): a claim's cut ends a held drag and takes only the cut files out of a turn
* fix(studio-server): a held drag ends when someone else changed a file it already holds
* docs(cli): explain how catalog search works and which model it downloads
* docs(cli): name the model catalog search was chosen against
* docs(cli): keep only live rules in the word-search header comment
* docs(cli): correct the hosted vector size in the local vector build comment