1161 Commits
Author SHA1 Message Date
Miguel Ángel 6cbe3fbe90 chore: release v0.8.23 (#3586) 2026-09-01 13:58:14 -04:00
WaterrrForever 8eea3c913f fix(capture): let vision captioning authenticate the way a server can (#3561)
* fix(capture): let vision captioning authenticate the way a server can

Three defects in one phase, all of which end with a capture that reports
"Captioned N/N images" and then "0 images captioned with Gemini" — a
successful-looking run that hands the agent nothing to see by.

1. Credential. The captioner only accepted an API key. A server deployment
   holds a service account, not a key, and a rejected key is indistinguishable
   from an unset one here: every request returns empty text and no error. Vertex
   is now a first-class provider, ranked above the bare key and below an explicit
   OPENROUTER_API_KEY opt-in, configured by HYPERFRAMES_VERTEX_SERVICE_ACCOUNT +
   HYPERFRAMES_VERTEX_PROJECT_ID (region via HYPERFRAMES_VERTEX_LOCATION). It
   carries its own model default because the Gemini API's flash-lite preview id
   is not resolvable on Vertex.

2. Empty captions. Thinking tokens are drawn from maxOutputTokens, so a model
   left free to think can spend the whole budget and return no text — a
   successful request with no caption. thinkingBudget is pinned to 0; a one-line
   factual caption needs no reasoning.

3. Native abort. Rasterizing a batch of SVGs concurrently drove up to SVG_BATCH
   simultaneous librsvg renders through libvips and corrupted the heap:
   `free(): unaligned chunk detected in tcache 2` (SIGABRT) during this phase,
   twice in fourteen days, losing the whole capture each time. A native abort
   cannot be caught, so the concurrency is removed rather than handled —
   rasterization is serialized and libvips' worker pool is bounded, while the
   vision requests, which are the slow leg, stay parallel. Throughput barely
   moves: 225 captions across three real captures, 0 failures, 13-25s each.

* test(capture): pin the rasterization loop to one render at a time

The serialization fix shipped without a regression test on the grounds that native
heap corruption is not unit-testable. The corruption is not, but the property that
prevents it is: `sharp` is mocked to record how many renders are in flight, and a
six-SVG batch must never reach two. A deliberately slow caption stub makes
overlapping renders the faster path, so a future refactor that "optimises" the loop
back to `Promise.all` fails here instead of aborting in production.

Also covered: `sharp.concurrency(1)` is applied — serializing the loop while leaving
libvips' pool at the host core count still fans one render across every core — and an
unrasterizable SVG is skipped without breaking serialization for its siblings.

Verified as a real guard: reverting only contentExtractor.ts to origin/main fails 7 of
the 22 cases in this file.


* fix(capture): tell the truth in the asset-descriptions header when Vertex captioned

The provider gate in `contentExtractor` accepts Vertex when a project and a
service account are both set -- which is the configuration a server
deployment actually has. The header written next to the captions still
tested only for an API key, so a capture whose captions Vertex had just
generated was labelled "GEMINI_API_KEY not set -- descriptions below are
catalog-derived".

That header is not cosmetic: it travels into the context the template
editor reads, telling it to distrust captions that are real.

Mirror the same two variables here, and name every provider in the fallback
text instead of only the API key.

* fix(capture): hand libvips' worker pool back after the renders

`sharp.concurrency(1)` is process-global and was set once, for the whole life of
the process. The bound is right for the rasterize loop -- a native abort in
libvips cannot be caught, so the renders must not overlap -- but its scope was
every later sharp caller in the process, none of which asked for captioning, all
of them pinned to one thread from then on.

Now the host's value is read first and restored in a `finally` around the
rasterize loop, so a skipped SVG cannot cost the process its threads either. The
vision requests below are network work and gain nothing from a pinned pool.

The mock had to grow the getter half of sharp's API -- `concurrency()` with no
argument reports the current value -- since save-and-restore is untestable
without it. Verified as a real guard: dropping only the restore fails both new
cases.

Raised by Rames Jusso in review of #3561 and concurred by Magi.
2026-09-01 23:34:59 +08:00
Xuanru Li 45cc343525 fix(cli): flag caption-zone by DOM box overlap (#3580)
A card centered at y=.860 can still cover the painted V2A pill. Intersect the element's getBoundingClientRect with the keepout instead of testing whether its center sits inside the band.
2026-09-01 04:41:57 +00:00
Miguel ÁngelandJames 38e356fba4 chore: release v0.8.22 (#3575)
* chore: release v0.8.22

* docs: include encoder retry in v0.8.22 notes

---------

Co-authored-by: James <james.russo@heygen.com>
2026-08-31 22:54:13 -04:00
heygengenesis[bot]andmiguel.sierra 9097d539b1 fix(cli): hide Windows child process consoles (#3529)
Rebase #3529 onto current main. Preserve all 16 issue-scoped Studio server, lint, and CLI child-process windowsHide options, including main's PowerShell null guards and stderr suppression in orphanCleanup.

Regression tests continue to assert windowsHide at each scoped spawn site. #3476 and #3430 remain out of scope.

Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
2026-08-31 18:11:20 -04:00
miga-heygen 73aa71c9ec fix(cli): guard PowerShell process queries against exited PIDs (#3571)
## Summary

`processIdentity` and `processParentPid` call `Get-CimInstance Win32_Process` to look up process metadata on Windows. When the target process has already exited, `Get-CimInstance` returns null and calling `.CreationDate.ToFileTimeUtc()` or `.ParentProcessId` on it throws `InvokeMethodOnNull`. The try/catch handles it, but PowerShell writes the error to stderr, which pollutes the test runner's output and causes spurious exit code 1 on Windows CI.

Two fixes per call site:
- Null-check the CimInstance before accessing properties (`$p = ...; if ($p) { $p.Property }`)
- `-ErrorAction SilentlyContinue` + `stdio: ["pipe", "pipe", "ignore"]` to suppress any residual stderr

Fixes the recurring `Tests on windows-latest` flake on main.

## Test plan

- [x] All 196 CLI test files pass locally
- [ ] Windows CI should no longer exit 1 from PowerShell stderr noise

— Miga
2026-08-31 17:15:39 -04:00
Miguel Ángel f3099dcb27 chore: release v0.8.21 (#3570) 2026-08-31 15:16:22 -04:00
Miguel Ángel 724796e2f0 chore: release v0.8.20 (#3555) 2026-08-30 00:31:08 -04:00
Miguel Ángel 0fd70b1d21 chore: release v0.8.19 (#3551) 2026-08-29 13:58:33 -04:00
Miguel Ángel 5cc2f1bef5 chore: release v0.8.18 2026-08-29 15:38:26 +00:00
Miguel Ángel d99eeef0b2 fix(cli): honor authored playback rate in snapshots (#3536) 2026-08-29 02:48:33 +00:00
Miguel Ángel e4dabf830c fix(cli): prevent keyframe shots overwriting sources (#3534) 2026-08-29 02:43:37 +00:00
Miguel Ángel b28747df0f fix(cli): respect timeline-free static compositions (#3533) 2026-08-29 02:43:33 +00:00
Miguel Ángel f6de05efec chore: release v0.8.17 2026-08-28 00:35:58 +00:00
Miguel Ángel 720ff5ac9c chore: release v0.8.16 2026-08-27 01:32:37 +00:00
Rajan Pantha 18409c9f27 fix(cli): keep phrase-level CJK and Thai transcripts as separate cues (#3436)
* fix(cli): keep phrase-level CJK and Thai transcripts as separate cues

wordsToCues inferred whether entries were already grouped into phrases
by testing for internal whitespace. Chinese, Japanese, Thai and the
other scripts written without inter-word spaces never satisfy that
test, so their phrase-level transcripts were treated as word-level and
re-grouped into a single cue covering the whole clip.

A three-phrase Chinese transcript produced one cue; the same transcript
in English produced three. The failure was silent: the export
succeeded, and the user found out by watching the captions.

For entries with no whitespace at all, fall back to entry length when
they are in a spaceless script. Whisper emits word-level tokens for
those scripts one or two characters at a time, while a phrase-level cue
runs to several times that. The median is used so one long token cannot
declare word-level input pre-grouped, and a couple of short cues cannot
declare a real transcript word-level.

--preserve-cues still forces the same thing, and behaviour for
space-separated scripts is unchanged.

Fixes #3353

* test(cli): pin the spaceless phrase length threshold
2026-08-26 20:17:25 +00:00
Miguel Ángel 0c9d234bd8 Merge pull request #3481 from heygen-com/fix/web-audio-cross-origin-silence-v2
fix(core): prevent cross-origin Web Audio capture from silencing audio
2026-08-25 23:41:58 -04:00
Miguel Ángel 740f7ead89 chore: release v0.8.15 2026-08-26 03:23:41 +00:00
Miguel Ángel 7c40efbc62 fix(fonts): harden localizer release diagnostics 2026-08-26 03:02:24 +00:00
Miguel Ángel d6de083411 feat(cli): stamp font compiler version in localized HTML 2026-08-26 01:11:38 +00:00
Miguel Ángel ec68d40cc6 fix(cli): keep font localizer process ownership explicit 2026-08-26 01:11:38 +00:00
Miguel Ángel 38f8f9250a feat(cli): expose deterministic font localization 2026-08-26 01:11:38 +00:00
miga-heygenandMiga 3202f3fb87 fix(producer): trip DE parallel-router circuit breaker on stalls and hangs (#3479)
Root cause: the per-worker capture calls in captureFrameRange
(parallelCoordinator.ts) take no abort signal of their own, and only
checked `signal.aborted` BEFORE starting each frame — a no-op once a
worker is already awaiting an in-flight call. On WSL2, the native
drawElement/BeginFrame capture call can hang indefinitely at frame 0
with no error. The DE parallel-router's existing stall watchdog
(captureStreamingStage.ts) correctly fires `stallController.abort()`
after HF_DE_STALL_MS, but that abort had no way to reach a
worker already wedged inside a hung capture call — so
executeParallelCapture's Promise.all waited forever, the render hung
indefinitely, and the CLI's circuit breaker (which only runs after
executeRenderJob settles) never got a chance to trip.

Fix: race each per-frame capture call against the signal actually
firing (raceAgainstAbort), the same "can't cancel, only race" pattern
already used by the sequential capture path. Once the watchdog's abort
is observed, the wedged worker rejects, executeParallelCapture settles,
and the existing pinned-fallback retry / "reverted" outcome / circuit
breaker machinery (already correct) runs end to end.

Also widen the CLI breaker's trip condition from the literal string
"reverted" to "not a clean routed success", so any future non-success
outcome the observability layer records also latches the breaker
instead of silently falling through.

Closes #3441

Co-authored-by: Miga <noreply@anthropic.com>
2026-08-25 23:54:49 +00:00
James 71e8e92ae9 fix: document safe template default editing 2026-08-25 06:54:02 +00:00
James 1e11608d44 fix: expose promoted template media slots 2026-08-25 05:33:50 +00:00
cce17da5a9 fix(core): prevent cross-origin Web Audio capture from silencing audio
Classify each <audio> element before Web Audio capture: same-origin,
CORS-opted-in, or a non-http(s) scheme stays on the primary
createMediaElementSource() path; cross-origin media without a
crossorigin opt-in withholds that call (the Web Audio spec makes such a
node output silence without throwing) and falls back to fetch +
decodeAudioData, preserving the FX graph whenever the server allows
CORS. Recheck the route at the transport's irreversible capture
boundary, and account for currentSrc, src, and <source> candidates the
same way the HTML resource-selection algorithm does.

Emit a stable preview diagnostic (`runtime_web_audio_bypass`) at media
discovery time, not only from playback scheduling, so `hyperframes
check` — which seeks but never plays — can surface it as a
`web_audio_bypass` finding. Diagnostics are suppressed during export
rendering, where the producer mixes audio offline and already applies
the FX chain. The existing non-unit-rate fail-closed rule stays scoped
to fx-chain/automation so this fix does not newly mute grouped or
above-unity tracks.

Takes over #3459 with the data-native-audio escape hatch removed per
review feedback: the automatic cross-origin detection already covers
the cases that mattered, so the extra per-element opt-in attribute,
its route-classifier branch, and its diagnostic path are dropped in
favor of a single automatic behavior.

Fixes #3458

Original-Author: desenmeng
Co-Authored-By: desenmeng <desenmeng@users.noreply.github.com>
Co-Authored-By: Miga <noreply@anthropic.com>
2026-08-25 04:39:51 +00:00
Miguel Ángel 81069fe47f chore: release v0.8.14 (#3474) 2026-08-24 20:03:19 -04:00
Miguel Ángel 045b3a4fd7 feat(cli): report which catalog items a render actually used (#3470)
`registry_item_added` fires when a catalog block is installed and
`render_complete` fires when a video is produced, but nothing joined them, so
"did this video use the catalog?" had no answer.

`hyperframes add` now records each installed item in `hyperframes.json`
(installed files are plain composition HTML with no provenance marker, so this
manifest is the only record that a file came from the registry), and
`render_complete` reports both the items the project installed and the blocks
the rendered composition actually reaches. An item installed and then never
mounted was tried and dropped, which no add-time event can express.

The scan answering "which sub-compositions does this file mount" now has one
owner, `collectSubCompositionSrcs` in `@hyperframes/parsers`, shared with
lint's `lintMissingOrEmptySubComposition`. It holds two invariants that were
previously restated per call site and got re-derived wrongly: it is a text scan
rather than a DOM query, because `<template>` content is inert and every
sub-composition except the render entry is wrapped in one; and references
resolve root-relative at every nesting level, matching `parseSubCompositions`.
It walks tag by tag rather than running open-ended spans across the whole file,
so a malformed composition cannot stall the render plan.

Also: `registryItems` is declared in the config schema, which closes with
`additionalProperties: false`, with an ajv-backed test pinning every key the CLI
writes; counts are never truncated by the name cap, and the reported used blocks
stay a subset of the reported installed ones, with `registry_items_truncated`
marking a windowed list; and an unreadable manifest reports itself rather than
posing as a project that never used the catalog.
2026-08-24 19:56:06 -04:00
Miguel Ángel b2fc18b2df fix(skills,lint): correct composition-contract claims the code contradicts (#3468)
The runtime absorbed a series of authoring mistakes over time and `runtime/init.ts`
says so in its own comments, but the skills kept teaching the old rules. Four of
them actively cost an agent a failing run: add `crossorigin` (lint rejects it
unconditionally), never build a timeline inside `async` (lint calls that the
documented contract), never `gsap.set` later-scene clips (two fixHints instruct
exactly that), and 12 copyable media snippets with no `id`, which render silent.

Corrected in every place each claim appeared, including `hyperframes-animation`,
three workflow scripts, the scaffolded project instructions, the CLI `docs`
command, and the public docs site: `data-track-index` is a Studio display lane
the render never reads, `class="clip"` is a layout convention rather than a
visibility requirement, timed elements may nest, the visibility window is
half-open, sub-composition host dimensions are backfilled, and the root-fill rule
applies only to the layered-composite path.

Behaviour changes, each backed by a render rather than by reading code:

- `timeline_registry_missing_init` deleted. The runtime creates the registry
  before any inline script; a composition without the guard line renders and
  animates correctly.
- `video_nested_in_timed_element` kept, message corrected. A rendered repro shows
  the nested-with-local-start case really does break, so the rule guards a real
  defect, but nothing is "FROZEN": the extractor ignores the wrapper's offset
  while visibility uses it, so the clip shows wrong frames and then vanishes.
- `mediaRenderIds` now stamps media whose source is a `<source>` child, closing a
  duplicate-id gap the old `[src]`-only selector left open.
- Stale messages fixed on `subcomposition_root_styled_by_class` and
  `deprecated_data_layer`.

`coreSkillContent.test.ts` pinned the literal sentence that made root
`data-start` look required, so it is narrowed to structure plus the regression it
genuinely catches.

Not covered, and flagged in the PR: the media global-vs-local start heuristic in
`runtime/init.ts` is the root cause behind the nested-video defect. Removing it
changes the meaning of existing compositions and needs its own deprecation.
2026-08-24 18:04:39 -04:00
Vance Ingalls 3ed971d018 chore: release v0.8.13 2026-08-24 12:51:02 -07:00
Miguel Ángel e5a5e6b151 fix(cli): keep overlap waivers local to marked text (#3464)
* fix(cli): scope overlap waiver to marked text

* fix(skills): guard changelog caption rail

* fix(skills): densify changelog caption checks

* test(skills): satisfy strict seek typing
2026-08-24 14:22:03 -04:00
Santhi Prakash 95e1ac9f04 fix(skills): skip mirror fan-out to agents that read the universal store (#3325) 2026-08-24 09:41:19 -04:00
Vance Ingalls 2ca578f945 chore: release v0.8.12 (#3457) 2026-08-23 19:54:55 -07:00
Vance Ingalls 2685c8f223 docs(audio): document grouped audio and its guardrails (#3455)
* fix(core): harden audio FX and group identity

* fix(core): address audio group review feedback

* fix(core): align preview transport with grouped audio

* test(core): pin audio group gain ceiling

* fix(core): preserve solo bridge through stack

* fix(engine): harden grouped audio rendering

* docs(engine): explain grouped mix fallback invariant

* test(engine): allow grouped mixes to finish on Windows

* feat(lint): validate audio group membership and timing

* test(lint): pin audio group membership guards

* fix(studio): unify audio IDs and group state

* fix(studio): make audio-group edits transactional

* fix(studio): keep preview state synchronized

* fix(studio): align audio rows, automation lanes and headers

* fix(studio): stabilize timeline audio derivations

* refactor(studio): simplify group metadata memoization

* style(studio): keep timeline layout within size gate

* fix(studio): keep timeline preset apply off auditions

* fix(studio): harden carve and FX rack behavior

* fix(studio): repeat audio FX reveal requests

* fix(studio): reconnect property-panel audio controls

* fix(studio): unify property panel audio detection

* fix(studio): satisfy panel and deletion gates

* feat(studio,core)!: remove solo and the group meter

* docs(audio): keep removal rationale current

* refactor(core): retire studio solo bridge

* docs(audio): document grouped audio and its guardrails

* docs(audio): point handoff at replacement stack
2026-08-23 19:19:27 -07:00
Miguel Ángel 32d58a73e3 chore: release v0.8.11 (#3440) 2026-08-23 14:49:13 -04:00
Miguel Ángel 59a69a145b chore: release v0.8.10 (#3426) 2026-08-22 11:16:32 -04:00
Vance Ingalls f6e8e8ddfd chore: release v0.8.9 (#3422) 2026-08-22 05:57:57 -07:00
Santhi Prakash 718bf5ef32 fix(producer,cli): surface every tried manifest path in the missing-manifest error (#3370) (#3387)
Closes #3370

## What

When `hyperframeRuntimeLoader` could not locate `hyperframe.manifest.json`, the loader reported a single fallback path that was never searched for (`/usr/local/lib/core/dist/hyperframe.manifest.json`). Inside a Docker render the user is then told to look at the wrong directory; the file that was actually missing (`/usr/local/lib/node_modules/hyperframes/dist/hyperframe.manifest.json`) was nowhere in the message.

## Why

`resolveHyperframeManifestPath()` built a 5-element `candidates` array, walked it with `existsSync`, and on total miss returned the last candidate. The error then quoted that candidate verbatim. The reporter even shows the exact reproducing command from a published image.

A second issue rode the same failure path: `packages/cli/src/commands/render.ts:902` keeps attaching the hint `"Try --docker for containerized rendering"` to users who are *already inside* the container. The container sets `ENV CONTAINER=true` and nothing reads it.

A third small thing came along: `CWD_RELATIVE_MANIFEST_PATHS[0]` was a byte-identical duplicate of `SIBLING_MANIFEST_PATH` — same path, two names.

## How

1. Hoist the candidate list to a single `MANIFEST_CANDIDATES` owner in `hyperframeRuntimeLoader.ts` and share it between the resolver and the error reporter. De-duplicate while doing it.
2. Add `triedManifestPaths()` as a tiny export so callers (and tests) can see what was actually searched.
3. Replace the source-text regex test that asserted on string positions inside `const candidates = [...]` with a behaviour test that points `PRODUCER_HYPERFRAME_MANIFEST_PATH` at a missing file and asserts the thrown error names it. Also exercise the no-override branch to confirm the sibling path is the first entry.
4. In `render.ts`, check `process.env.CONTAINER === "true"` before attaching the `--docker` hint. The chrome-launch and macos-old-chrome remediation branches already short-circuit before the hint, so an empty string is a safe value when the user is in the container.

## Test plan

- [x] `bunx vitest run src/services/hyperframeRuntimeLoader.test.ts` — 7/7 pass (`hyperframeRuntimeLoader error path (#3370)` describe covers the missing-manifest message and the tried-paths export).
- [x] `bunx tsc --noEmit` in `packages/producer` and `packages/cli` — clean.
- [x] `bunx oxfmt --check` and `bunx oxlint` on the touched files — clean.
- [x] `bunx fallow audit --base origin/main` — no new findings on the touched files.
- [x] Targeted producer unit lane: `node scripts/run-test-lane.mjs unit` — same 7 pre-existing failures as `origin/main` before the change (htmlCompiler.parity, audioPadTrim.integration); no regressions introduced.

Files touched:
- `packages/producer/src/services/hyperframeRuntimeLoader.ts`
- `packages/producer/src/services/hyperframeRuntimeLoader.test.ts`
- `packages/cli/src/commands/render.ts`
2026-08-22 02:09:11 -04:00
Vance Ingalls 6f82acf50c chore: release v0.8.8 (#3411) 2026-08-21 19:04:29 -07:00
James Russo dac8f9f912 feat: add promoted template edit contracts (#3407)
* feat: add promoted template edit contracts

* fix: address template contract review feedback
2026-08-21 18:39:17 -07:00
Miguel Ángel ea95b7d44e fix(cli): stop a caught post-render throw reporting a valid render as failed (#3409)
A render that produced and validated its artifact still exited 1. Reported
again from the field on 0.8.7: the MP4 was on disk and an independent ffprobe
and full decode both passed, and the CLI exited 1 immediately after logging
`artifact validated`.

`render-success-state.ts` exists for exactly this and documents three earlier
cases, so the sentinel was already there. Its gap is which paths read it: the
uncaughtException and unhandledRejection handlers both consult
`isRenderSucceeded()`, but a post-render throw that the command wrapper CATCHES
never reaches either. It becomes an ordinary non-zero CommandResult, and
`finalizeCli` wrote that straight to `process.exitCode`.

The result was a run that disagreed with itself: `commandSucceededForTelemetry()`
already lets a validated render override a failure, so telemetry recorded
success while the shell saw exit 1.

Sanitize once in `finalizeCli`, where every command result funnels through,
rather than wrapping the individual steps. Which step threw does not matter;
that the artifact is committed does. The throw is still printed, so it stays
visible for diagnosis without being fatal.

Reproduced first as a failing test (`expected 1 to be +0`) on macOS, so this is
not Windows-specific — the field reports are one instance of it. A second test
pins the other side: a command that throws with no validated render still exits
non-zero, so the sanitizer cannot swallow a genuine failure.
2026-08-21 21:01:43 -04:00
Miguel Ángel 5842dd8df4 fix(studio): invalidate the preview signature off the watcher that sees project writes (#3364)
* fix(studio): invalidate the preview signature off the watcher that sees project writes

The preview ETag is a hash of the project's files, memoised per project
directory. That cache was cleared from Vite's own watcher, which
`server.watch.ignored` deliberately excludes `data/projects/**` from, so
nothing ever cleared it: the ETag stayed frozen for the life of the dev
server, the preview answered every revalidation with 304, and the browser
went on serving the composition as it was when it first loaded.

The visible cost is thumbnails. Their disk cache key already content-hashes
the composition, so an edit correctly asks for a fresh capture, but the
capture is taken against the stale page, and a clip's filmstrip keeps
showing frames of a layout that no longer exists until the dev server is
restarted.

Studio already runs its own chokidar watcher over exactly these
directories, because Vite's would answer a composition edit with a full
page reload. That watcher now owns the invalidation, and the cache asks it
to follow any project directory it has not seen. All five event types
count: an added or deleted asset changes the signature as surely as an
edited one.

The cache moves behind `createProjectSignatureCache` so the invalidation
rule is a unit under test rather than a subscription buried in the adapter.

* fix(studio): filter signature invalidation, and stop the CLI server missing motion saves

Review follow-up on the unfiltered invalidation.

The watcher fired on everything under a project dir, but the signature walk
skips 14 directories and `.thumbnails` is one of them. That directory is
where the thumbnail route keeps its disk cache, and every capture also reads
the preview, so populating a timeline row discarded the memo on roughly every
request of the one workload it exists for.

The filter is a single exported predicate beside the exclusion set it reads,
and it is applied inside `invalidate` rather than at the watcher, so no caller
can subscribe and forget it. It is deliberately not `WATCHER_EXCLUDED_DIRS`:
that set is character-identical but drops all of `.hyperframes/`, and the
signature reads two manifest files back out of there.

Which is the same bug, still live, in the CLI server: its watcher filters
through `shouldWatchProjectFile`, so `.hyperframes/studio-motion.json` never
reached the listener that clears the cached signature. Studio writes that file
at runtime, so saving motion state left the preview ETag stale until restart.
The watcher now admits signature-relevant paths and the reload listener
re-applies its own filter, so what triggers a browser reload is unchanged.

Also from review: drop the `createViteAdapter` signature-cache default, which
produced exactly the memo-nothing-clears bug this PR fixes, and correct the
docstring — the content hash is already gated behind a stat fingerprint, so
what the memo saves is the walk.
2026-08-21 19:13:37 -04:00
Miguel Ángel 41af866bcb chore: release v0.8.7 (#3402) 2026-08-21 15:21:20 -04:00
Miguel Ángel 8b67bb6db5 fix(cli,studio): surface project lint in Studio (#3393)
* fix(cli,studio): surface project lint in Studio

* fix(studio): preserve per-file lint coverage
2026-08-21 15:11:24 -04:00
Miguel Ángel 63eb35041c fix(deps): bump puppeteer so the browser hides its console window on Windows (#3394)
Windows users see a console window per chrome-headless-shell worker during a
render. Those windows come from Puppeteer's own launcher, not from any spawn
in this repo, so the windowsHide work on our ffmpeg spawns could not reach
them.

@puppeteer/browsers added windowsHide: true to its spawn in 3.2.1. It is
absent in 3.1.0 and 3.2.0. puppeteer-core pins that dependency exactly, and
25.8.0 is the first release pinning 3.2.1 (25.5.0 -> 3.1.0, 25.6.0 and
25.7.0 -> 3.2.0), so 25.8.0 is the minimum that carries the fix rather than
a preference for the latest.

Verified after install that exactly one copy resolves, at 3.2.1, and that its
launcher carries the flag. A draft render still completes.

Refs #3379
2026-08-21 11:50:18 -04:00
Miguel Ángel a9ea07edde fix(cli): reject blank default composition entries (#3392)
* fix(cli): reject blank default composition entry

* fix(cli): complete blank entry safeguards
2026-08-21 11:00:43 -04:00
Vance Ingalls 556fe936f8 chore(cli): bump pinned Chromium to 152.0.7977.30 (#3231)
Picks up crbug 522872457's fix (CL 8032671), which landed after the
152.0.7935.0 canary cut and so was absent from the old 152.0.7928.2 pin.

Re-probed every 3D signal the compile gate matches, drawElementImage vs a
CDP screenshot of the identical state, on the shipping headless-shell
binary. PSNR, old pin -> new pin:

  backface-visibility:hidden    1.4 dB  ->  14.8 dB   still DAMAGED
  preserve-3d (no backface)    46.7 dB  ->  46.7 dB   clean
  perspective()                45.2 dB  ->  45.2 dB   clean
  matrix3d()                   45.2 dB  ->  45.2 dB   clean
  rotate3d()                   45.3 dB  ->  45.3 dB   clean
  translateZ under perspective 29.9 dB  ->  29.9 dB   marginal

The upstream fix repaired the collateral damage only: dropped sibling
content and lost backgrounds now render, but a culled backface is still
painted. So the 3D gate stays. Beta rather than Canary because
153.0.8000.0 measured identical on every variant.

Follow-up filed as PRINFRA-486: four of the five signals the gate matches
were never broken on any build tested, so it may be able to narrow to
backface-visibility alone. Needs a corpus eval first — this probe covers
static angles only, and animated 3D subtrees take a different path.
2026-08-20 23:39:45 -07:00
Vance Ingalls 7563b644a2 fix(cli): surface HYPERFRAMES_BROWSER_PATH hint on Windows chrome-headless-shell launch crashes (#2481)
* fix(cli): surface HYPERFRAMES_BROWSER_PATH hint on Windows chrome-headless-shell launch crashes

Field feedback (#hyperframes-cli-feedback ts=1784116246, win32/x64, CLI 0.7.58) hit
`Failed to launch the browser process ... Code: 3221225595` with no stderr. Exit
code 3221225595 = 0xC0000409 = STATUS_STACK_BUFFER_OVERRUN, a Windows stack-
corruption fatal from the pinned chrome-headless-shell binary. The reporter
recovered by pointing HYPERFRAMES_BROWSER_PATH at system Chrome; render then
used the screenshot fallback and produced the MP4 cleanly.

The generic "Try --docker" hint the CLI already emits didn't name that env var,
so the workaround was undiscoverable. Add a Windows-scoped launch-crash
remediation sibling to `chromeLaunchRemediation` (Linux, `linuxDeps.ts`) and
`wrapDownloadFailureWithBrowserPathHint` (download-time, `manager.ts` — #2443).
Fresh concrete case for the #2078 lineage (closed with explicit invite to
resubmit on a concrete case).

- New `packages/cli/src/browser/windowsCrash.ts` — `isWindowsChromeCrashError`
  gates on Puppeteer's `Failed to launch the browser process` wrapper AND the
  specific crash code (decimal `3221225595`, hex `0xC0000409`, or symbol
  `STATUS_STACK_BUFFER_OVERRUN`), so unrelated Windows launch failures don't
  mis-fire this hint. `windowsChromeCrashRemediation` returns the actionable
  block scoped to win32.
- `render.ts` `handleRenderError` calls it after the existing
  `chromeLaunchRemediation` (Linux) check; both fall through to the generic
  errorBox if neither matches.
- Tests: 9 vitest cases covering positive matches on all three code forms,
  negative on Linux-shared-lib launch failures, negative on the code alone
  without the launch wrapper, and off-platform / non-launch short-circuits.

— Via

* fix(cli): fail the Windows crash branch through failCommand, not process.exit

`scripts/check-cli-process-ownership.mjs` AST-walks every non-test file
under `packages/cli/src` (bar `cli.ts`) and forbids direct process
termination — only the CLI entrypoint owns exit. The new Windows
chrome-headless-shell arm called `process.exit(1)` while both sibling arms
(Linux shared-lib, macOS) and the generic fallback call `failCommand()`,
so the required Lint job failed on that line and preview-regression failed
downstream of its preflight.

`failCommand()` carries the central failure-hook wiring, so this is the
behaviour the branch already wanted.
2026-08-20 23:38:58 -07:00
James Russo 36c7dffe5c chore: release v0.8.6 (#3386) 2026-08-20 21:47:29 -07:00
Miguel Ángel 7a8f8a0b45 chore: release v0.8.5 (#3375) 2026-08-20 19:03:09 -04:00