716 Commits
Author SHA1 Message Date
Vance Ingalls f8f945d42e chore: release v0.7.43 2026-07-08 15:51:39 -07:00
Vance Ingalls 5f9ee0b678 fix(cli,engine): close review gaps in Chrome resolution fix
Address PR #2082 review feedback:
- Route studio thumbnail + render call sites through preferManagedChrome
  so studio renders no longer silently fall back to whatever system
  Chrome happens to be installed.
- `hyperframes browser ensure` now resolves through the same
  preferManagedChrome path render uses, so it reports what render will
  actually pick instead of any system Chrome it happens to find.
- Point the unsupported-Chrome fallback log at `browser ensure --force`
  instead of `doctor`, which doesn't check Chrome/drawElement capability.
- Fix stale findFromCache comment: the HF pin is now a Dev-channel build
  that can be newer than a user's puppeteer-cache Stable install.
2026-07-08 15:25:59 -07:00
Vance Ingalls 8854bad8f9 fix(engine,cli): resolve drawElement to a Chrome build that actually has it
canvas.drawElementImage is an unlaunched Dev/Canary-only Blink feature
(~151+). The CLI's pinned CHROME_VERSION fallback was still 131.0.6778.85 —
a puppeteer 24→25.2.1 bump that pinned it to Chrome Dev 151.0.7912.0 was
written on 2026-06-29 but never merged (orphaned local commit, no PR). Any
render on that pin, or on the shared puppeteer-cache binary, or on system
Chrome (Stable, no drawElementImage at all) got a canvas.getContext("2d")
missing the method and crashed mid-capture with "ctx.drawElementImage is
not a function" instead of falling back (HF#2060).

Three changes:
- Bump puppeteer/puppeteer-core to ^25.2.1 across every package that
  depends on it, and CHROME_VERSION to 152.0.7928.2 (today's Dev channel;
  confirmed via direct probe to implement drawElementImage, unlike 131).
- `ensureBrowser({ preferManagedChrome: true })`, always used by `render`:
  resolve straight to our pinned/cached build, skipping both the shared
  puppeteer-cache preference and system Chrome. Rendering shouldn't depend
  on whatever arbitrary Chrome a machine happens to have — that's exactly
  how this regressed (any Mac with Chrome.app installed bypassed the CLI's
  pin entirely).
- A runtime capability probe in the engine, right before any other
  drawElement work: if `drawElementImage` isn't a function on the injected
  canvas, route to the existing screenshot-fallback gate instead of
  crashing. This is the real backstop — it protects every resolution path
  (env override, stale cache entry, a future Chrome regression), not just
  the ones `preferManagedChrome` reaches.

Verified end-to-end: rendering against chrome-headless-shell 131 (confirmed
to lack drawElementImage) now falls back cleanly and produces a valid MP4
instead of crashing; rendering against a capable build still engages
drawElement normally. 922 engine tests + 1373 CLI tests pass.

Fixes #2060.
2026-07-08 14:14:28 -07:00
Miguel Ángel 38b27c4d82 fix(cli): report unknown-flag errors + cover nested subcommands (HF#2033) [P2] (#2072)
* fix(cli): report unknown-flag errors + cover nested subcommands (HF#2033)

Two flag-hygiene gaps behind the assertKnownFlags arc:

1. Telemetry loss: assertKnownFlags ran BEFORE the try/catch in the command
   wrapper, so an unknown-flag throw skipped reportCommandFailure entirely —
   zero signal on how often users hit bad flags. Moved the assertion inside
   the try so it reports like any other failure.

2. Nested-subcommand scope: cli.ts wraps only the top-level command loaders,
   so command groups' leaves (cloud/*, auth/*, figma/*, lambda/*, capture/*,
   skills) were never wrapped — citty dispatches to the leaf, whose run had no
   assertion and no failure reporting. So `hyperframes cloud render --badflag`
   silently ignored the flag. trackCommandFailures now recurses through
   cmd.subCommands (normalizing citty's Resolvable entries to loaders) and
   wraps every leaf. Identity is preserved for bare no-run/no-subcommand defs.

Verified: `auth status --badflag` now errors "Unknown flag: --badflag"
(previously silent); `auth --help` still dispatches; top-level `lint
--badflag` still rejected. Tests: unknown-flag rejection is reported, and a
nested subcommand's failure reaches onFailure.

* test(cli): guard indexed subCommands access for noUncheckedIndexedAccess

CI Typecheck (tsc, unlike the local tsup build) flagged the nested-subcommand
test: indexing `subCommands["render"]` yields `T | undefined` under
noUncheckedIndexedAccess, so invoking it tripped TS2722/TS18048. Guard the
loader before calling it.
2026-07-08 16:24:36 -04:00
Miguel Ángel 701ae9e9b9 fix(cli): contrast audit reads an element's own opaque background (#1975)
The WCAG contrast audit estimated each text element's background by sampling a
4px pixel ring just OUTSIDE its bounding box. For an element that paints its
OWN opaque background (a caption pill, a CTA button, a solid card), the text is
composited over that solid color, not over whatever surrounds the box. Sampling
the ring there measured the text against the scene behind the element (often a
dark photo), producing false ~1:1 ratios and flagging perfectly readable CTAs
and captions. Users reported the warning persisting no matter how they changed
the background color, because the audit was never reading it.

Resolve the nearest fully-opaque background-color by walking the element up its
ancestor chain, and use it when present; keep sampling the ring only when the
text sits over image pixels (a background-image is hit first) or no opaque
background exists. The pure decision lives in a new commands/contrast-bg.ts with
unit tests; contrast-audit.browser.js (injected as a raw string, so it cannot
import) inlines the same logic, mirroring the existing duplicated-WCAG-math
note.
2026-07-08 15:53:21 -04:00
Miguel Ángel ab129023de docs(cli): fix render examples that pass a file as the project dir [P2] (#1974)
* docs(cli): fix render examples that pass a file as the project dir

The render command's positional argument is the project directory (default
"."), resolved via resolveProjectOrThrow; a specific composition file is
passed with -c/--composition. Several docs showed `hyperframes render
index.html` / `render ./my-composition.html`, which treats the HTML file as
the project dir and fails with "Not a directory". Correct the guide and the
cli README to render the project's index.html directly (or point at a file
with -c).

* docs: fix render index.html example in the Open Design guide too (R1)

R1 flagged that open-design-hyperframes.md carried the identical
`npx hyperframes render index.html` example this PR fixes in the Claude
guide — same failure vector ("Not a directory" for a file positional).
Corrected to `npx hyperframes render` run from the project directory.
2026-07-08 15:53:17 -04:00
Miguel Ángel e018318225 fix(cli): add --no-clipboard no longer throws "Unknown flag: --clipboard" (#2067)
The add command declared its flag literally as `"no-clipboard"`, but citty
treats `--no-<name>` as the negation of a boolean `<name>` arg. So
`--no-clipboard` parsed as negating a (nonexistent) `clipboard` arg and
assertKnownFlags threw "Unknown flag: --clipboard" — even though --help
advertised --no-clipboard as valid.

Declare the positive `clipboard` (boolean, default true) instead and read
`args.clipboard === false`; citty's built-in negation then handles
`--no-clipboard` correctly. --help still lists both spellings.

Verified: `hyperframes add data-chart --no-clipboard` now succeeds instead of
erroring on the flag.
2026-07-08 15:53:13 -04:00
Miguel Ángel f5f94a9495 fix(cli): warn when a WebM render silently drops its alpha channel [P2] (#2044)
* fix(cli): warn when a WebM render loses its requested alpha channel

HyperFrames always encodes WebM with an alpha-capable pixel format
(yuva420p), but some ffmpeg/libvpx builds silently emit opaque yuv420p
even when handed alpha input and -pix_fmt yuva420p. The render succeeds
and plays back fine, so the lost transparency is only discovered after
compositing (users report shipping a solid-black clip and colorkeying it
out by hand).

After a WebM render, best-effort ffprobe the output's pix_fmt; if it
lacks alpha, print a non-blocking warning that names the concrete remedy
(--format mov / ProRes 4444). Only WebM is checked (mp4 is intentionally
opaque; mov/png carry alpha through paths that don't hit libvpx-vp9), and
a failed probe stays silent rather than warning speculatively.

Pure decision (pixelFormatHasAlpha / webmAlphaAdvisory) unit-tested;
verified end-to-end that a transparent WebM render now surfaces the
warning while an MP4 render stays silent.

* fix(cli): key WebM alpha check on ALPHA_MODE tag, not pix_fmt (R1 blocker)

R1 (Rames/Via) correctly flagged the detection as ~100% false-positive on
working builds. libvpx-vp9 stores the alpha plane in a Matroska
BlockAdditional sidecar, so ffprobe ALWAYS reports pix_fmt=yuv420p for a
correct transparent WebM (per docs/guides/rendering.mdx #1823 and the
webm-concat-copy smoke test). The real signal is the stream-level
ALPHA_MODE=1 tag: a working encode writes it; a build that can't emit the
sidecar omits it and produces genuinely opaque output.

Re-cut the probe to read stream_tags=alpha_mode (JSON, case-insensitive) and
warn only when a probed WebM lacks ALPHA_MODE=1. Tests inverted accordingly
(alphaMode:true → silent; alphaMode:false → warn). Verified end-to-end: a
transparent webm render on an alpha-preserving build (ALPHA_MODE=1) now emits
0 warnings; previously it warned on every webm.
2026-07-08 15:53:08 -04:00
81884a7495 fix(cli,skills): install workflow skills on demand instead of re-pulling the full set (#2012)
* fix(cli,skills): install workflow skills on demand instead of re-pulling the full set

Users report every init re-pulls all 21 skills into ~/.agents/skills
whenever anything is stale or missing - heavy, noisy, and it re-expands
deliberate partial installs.

Split the set into two tiers:

- core: the /hyperframes router + hyperframes-* domain skills +
  media-use, which every workflow references structurally. init and
  bare 'skills update' keep these (plus anything already installed)
  fresh, and never expand the install.
- on demand: the end-user workflow skills (and figma). They install at
  trigger time via 'skills update <name...>' - positional names are
  the only way update expands an install: one targeted
  'skills add --skill <name>' covering only stale/missing targets, a
  fast no-op when current, presence-verified after install, exit 1 on
  unknown names, and a presence-only degrade when GitHub is
  unreachable.

The /hyperframes router now runs 'skills update <workflow>' after
routing and before reading the workflow skill, so a routed workflow is
guaranteed present even on a machine that only has the core set. Each
on-demand skill also opens with the same self-maintenance step (run
'npx hyperframes skills update <name>' silently), so a workflow
triggered directly - without the router - still refreshes itself and
restores any missing core skill before relying on it.

When the manifest is unreachable (offline / rate-limited) the engine
degrades honestly instead of claiming success: named runs presence-check
the request plus a pinned fallback core list (unit-pinned to skills/)
and blind-install whatever is absent; a bare strict update fails loudly
so the 'check || update' chain can't pass while everything stays stale;
init reports the skipped freshness check. --json emits structured
errors on failure paths.

skills check still lists every skill, but exits non-zero only for
stale installed skills, an incomplete core set, or removed leftovers -
workflow skills not yet installed are reported as available on demand.
Bare 'hyperframes skills' (and 'skills add --all') remain the explicit
full-set installs.

Verified end-to-end with a sandboxed $HOME: fresh init installs the 9
core skills only; 'skills update slideshow' adds exactly that skill
(no-op on re-run, exit 1 on unknown names); bare update refreshes
without expanding; a live Claude Code run routed PR-to-video, executed
the router's update step, and the workflow skill appeared before use;
and a second live run triggered an installed workflow directly, whose
opening maintenance step restored a deliberately removed core skill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(skills): clarify update-engine contracts + document lazy-install model

- skills.ts: note the UpdateSkillsResult.unknown strict-mode contract,
  verifyInstalled's non-strict (warn-not-throw) intent, and that a
  partial install stays "refreshed but never expanded" (review nits).
- docs/guides/skills.mdx: add a "Keeping skills current" section covering
  the core-eager / workflow-on-demand model and the skills check|update
  commands, per the repo's catalog-maintenance rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: kiritowoo <295860553+kiritowoo@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Miao Yang <miao.yang@heygen.com>
2026-07-09 01:30:51 +08:00
Vance IngallsandClaude Fable 5 e2c88ef689 fix(core,producer): composition CSS variables reach the render path at eval time
Live testing of the compile-time variable emission surfaced four gaps:

- The producer render path never emitted the compile-time stylesheet (only
  the preview bundler did), so eval-time reads — GSAP .from immediateRender,
  top-level getComputedStyle — saw undefined vars in rendered output. The
  producer's inlineSubCompositions now calls the shared
  emitRootCompositionVariableStyles and passes the variable hooks.
- --variables overrides weren't visible at eval time. They now thread from
  the orchestrator / distributed plan through compileStage into the emitted
  rules (window.__hfVariables still covers script reads).
- Per-declarer rules anchored on data-composition-id, which two inlined
  instances of one sub-composition share — instance A's rule restyled
  instance B, and a rule directly on the declarer defeated the host's
  inherited data-variable-values. Rules now anchor on per-instance
  data-hf-var-scope markers and layer nearest-host values over declared
  defaults, mirroring the runtime loader.
- Emission ignored authored CSS; a declared default now yields to a var
  already defined in an authored <style> block (define-if-absent, matching
  the runtime injection).

Also: the figma importer emits background-color (longhand) for solid fills.
GSAP backgroundColor tweens cannot read a var() through the background
shorthand — its pending-substitution longhands serialize empty, so .from
captured nothing and settled on transparent (pre-existing GSAP interaction,
reproduced with no composition variables involved).

Validated live: eval-time default + override, .from + override, two-instance
host branding, authored :root precedence, SDS brand-loop pixel parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:28:16 -07:00
Vance IngallsandClaude Fable 5 def276524b fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint
Brand-loop live test (SDS duplicate, plans/figma/brand-loop-test-plan.md)
proved the recolor chain end-to-end and surfaced three gaps:

- runtime now defines every declared composition variable as a CSS
  custom property (document root at init + scoped sub-comp hosts in the
  loader), so imported var(--slug, literal) fills resolve live — without
  this the frozen literal always won and variable-driven rebranding
  could not propagate. Slug kept byte-compatible with the figma
  importer (parity test). render --variables overrides win.
- figma component --name: variant frames are often all named
  'Platform=Desktop' and slug-collided across imports.
- imported fragments carry data-hf-snippet and the project linter skips
  composition-root rules for them.
- /figma skill documents the field-tested non-Enterprise tokens path
  (MCP get_variable_defs joined with REST boundVariables ids).

Shared-helper extractions (injectScopedStyles, flattenedRoot module,
parseHostVariableValues, rasterizeFallback, shapeCss) satisfy the
dedup/complexity audit the runtime changes tripped.

Validated live: brand-loop renders purple from the attribute alone (no
manual :root); 118 figma + 662 runtime/compiler + 331 lint tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:47:13 -07:00
Miguel Ángel cebce603df fix(cli): pin arm64 render Chromium to Playwright headless-shell (#2039) (#2040)
The arm64 render image had no pinned browser: chrome-for-testing publishes
no linux-arm64 build, so Dockerfile.render fell back to Debian bookworm's
rolling `chromium` package. Its current arm64 build (150.0.7871.46) SIGTRAPs
at startup (exit 133), breaking `render --docker` 100% on Apple Silicon.

Install a pinned, non-Debian chrome-headless-shell from Playwright on arm64
(Google's build, not Debian's repackage). The wrapper wires whichever binary
landed into PRODUCER_HEADLESS_SHELL_PATH and now fails the build loudly if
neither is present, instead of silently using the broken Debian chromium.
Bonus: arm64 gains BeginFrame deterministic capture it previously lacked.
amd64 path is unchanged.

Verified on Apple Silicon: same arm64 image, Debian chromium 150 -> exit 133,
Playwright arm64 headless-shell (Chromium 149) -> exit 0.
2026-07-07 21:07:19 -04:00
Vance Ingalls c6408b620e Merge pull request #2038 from heygen-com/release/v0.7.42
chore: release v0.7.42
2026-07-07 16:56:45 -07:00
Miguel Ángel 401dd1d27f fix: media-use bug-bash fixes (codex gate, id race, provider/reuse/adopt guards) + CLI unknown-flag rejection (#2033)
* fix(media-use): codex gate misfires as 'not logged in' when piped

codexUnavailableReason() gated generation on parsing `codex login status`
stdout, but that command prints 'Logged in using ChatGPT' to stderr and
exits 0 — so the piped stdout media-use captures (execFileSync returns
stdout only on success) was empty, and the gate falsely reported 'not
logged in'. Every headless / CI / agent run was blocked from codex image
gen even when fully authed.

Gate on the durable credentials file ($CODEX_HOME/auth.json) instead of
the TTY/stderr-only human text. Token validity is still proven by the
exec, which fails cleanly on a stale login. The stdout `features list`
capability check is unchanged.

Verified: reproduced the false 'not logged in' block, then after the fix
generated end-to-end via `resolve -t image --provider codex` (valid
1254x1254 PNG, source=generated, provider=codex.image_gen).

* fix(media-use): bug-bash fixes — id race, provider/reuse/adopt guards

From the bug-bash against main:

- MU-23 (HIGH): concurrent resolves raced on nextId (read-max-then-append,
  non-atomic), so parallel agents got duplicate ids and clobbered each
  other's files. Add allocateId(): a coarse per-project lock (.media/.lock,
  15s stale-steal) around id allocation that scans the manifest AND the
  type dir for reserved ids, then O_EXCL-creates a placeholder file so the
  slow download between allocate and append can't collide. 5 parallel
  resolves now yield 5 distinct ids + files.
- X4: --reuse imported across a type mismatch (bgm asset under images/).
  Apply typesMatch on the --reuse path; reject mismatches (icon<->image
  still interchangeable).
- X5: --provider silently overrode --local-only and made a network call.
  --local-only is now a hard guard: network providers are skipped even
  under a forced provider; the miss message explains the conflict.
- BUG-2: --provider ignored the exact-cache floor and could hand back an
  asset from a different provider. A forced --provider now bypasses all
  reuse rungs (regenerate with THIS provider); the unforced floor is intact.
- MU-26/X6: 0-byte assets accepted. --adopt skips 0-byte files (loud); ingest
  refuses a 0-byte local file (freezeUrl already rejects empty responses).
- BUG-4: unknown/unavailable --provider now errors with the available list
  instead of a generic 'no provider could resolve' (typo != catalog miss).
- BUG-5: --reuse "" gave the wrong 'type and intent required' error; it now
  routes to a clear empty-sha message.
- BUG-3: voice duration leaked an unrounded float into index.md; round all
  durations to 0.1s centrally at record build (matches probe).
- Nits: whitespace-only --intent is rejected; nudge grammar (exists/exist).

Tests: allocateId reservation + registry local-only-wins added; full
media-use suite green. All fixes verified e2e.

* fix(cli): reject unknown flags instead of silently ignoring them

citty is permissive: an unrecognized flag was dropped, not rejected — so
`render . --out x` (the flag is --output/-o) silently ignored --out and
rendered to the default renders/<name>.mp4 path. A mistyped flag read as a
render/catalog miss.

Add assertKnownFlags(): validate every dash-prefixed token against the
command's declared args + aliases + the global set (help/version/json)
before the command runs, in the shared trackCommandFailures run-wrapper so
every leaf command is covered. Handles --flag=value, --no-<bool> negation,
camelCase<->kebab arg names, and combined shorts; stops at --; positionals
and flag values pass through.

Verified: `render . --out x` -> 'Error: Unknown flag: --out'; --output/-o/
--json/--help still accepted. Unit tests added.

* docs(skills): install with --full-depth so agents get current main

The documented `npx skills add heygen-com/hyperframes` fetched the
skills.sh registry blob, which lags GitHub main by hours — so users
following the docs got a stale skill (e.g. media-use v1: no --candidates,
voice stubbed). The CLI's own `hyperframes skills` command already forces
a full clone via --full-depth to bypass this; the docs didn't pass it.

Add --full-depth to every documented install command (README, CLAUDE.md,
docs/guides/skills.mdx) with a one-line note on the lag. Addresses the
user-facing half of the publish/registry lag (#2034).

* chore(media-use): collapse resolve.mjs import to satisfy oxfmt --check

* fix(cli): extract longFlagName to keep flag validator under complexity gate

Also regenerate skills-manifest.json (resolve.mjs formatting change re-hashed
the media-use skill). Fixes the Fallow audit + skills-manifest-in-sync CI gates.
2026-07-07 19:19:28 -04:00
Vance IngallsandClaude Fable 5 f6cd711bf0 chore: release v0.7.42
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:04:26 -07:00
Vance IngallsandClaude Fable 5 924727a0b4 feat(producer,cli): drawElement priority inversion — single-worker streaming over auto-parallel (#2026)
* feat(producer,cli): drawElement priority inversion — single-worker streaming over auto-parallel

clamp:parallel eats 50% of local renders (1,326/fortnight; DE engagement
stuck at 3.8%) by routing multi-worker renders to unverified screenshot
capture. Benchmarks (2026-07-08, 4 comps x W1/W2/W3/W5) show that above the
~900-frame amortization crossover, single-worker VERIFIED drawElement
streaming beats screenshot-parallel at EVERY worker count (2,380f: 66s vs
109-127s; 3,600f: 33s vs 39-56s; parallel scaling flattens past W2), while
below it DE's fixed init cost loses by <=2.2s.

- shouldPreferSingleWorkerDrawElement (exported predicate + 7 unit tests):
  inverts an AUTO-resolved multi-worker render to workerCount=1 when the
  comp matches the benchmarked configuration — default-on DE (darwin
  hardware clamp upstream), no compile gate, no forced-screenshot hint,
  mp4 output, single-worker streaming eligible, and totalFrames >=
  HF_DE_SINGLE_MIN_FRAMES (default 900; 0 disables). Explicit --workers N
  is always honored.
- Inverted renders keep the probe session and land on the worker-encode
  streaming drain — the ONLY path with runtime self-verification, so this
  moves ~40% of previously-clamped renders onto the verified fast path.
  Comps that later hit an init-time gate (~1.5% of local renders) render
  single-worker screenshot streaming; accepted trade.
- Telemetry: de_worker_inversion on render_complete (orchestrator ->
  perfSummary.workerInversion -> CLI), plus the worker_resolution
  observability checkpoint now records deWorkerInversion.

Validation: e2e matrix on 2,381f comp — auto->5 workers inverted to 1,
DE verified 4x inf PSNR, RENDER_OK; short comp (360f) auto stays 5-worker;
explicit WORKERS=3 honored; HF_DE_SINGLE_MIN_FRAMES=0 disables. Canary
suite 7/7 (PSNRs identical). renderOrchestrator tests 86/86.
tsc/oxlint/oxfmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(producer): review fixes — inversion routing guards, calibration skip, retry revert

Max code-review round on the inversion (13 confirmed findings):

- Streaming spawn-failure disk fallback now clamps default-on drawElement
  (deClampReason=disk_path, DE-mode probe closed) exactly like the
  pre-capture clamp — previously it carried useDrawElement=true onto the
  unverified disk path, the hole the verified-path confinement exists to
  close, newly reachable for every inverted render.
- Predicate gained the routing knowledge it was blind to: layered/HDR and
  shader-transition comps (drawElement never runs there), supersampling
  (deviceScaleFactor>1 init gate), a probe session whose init gates already
  disengaged DE, and the PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true explicit
  parallel-DE opt-in (honored like --workers N).
- Eligibility is evaluated BEFORE capture calibration and skips it when the
  inversion pins workers to 1 regardless of the estimate — the throwaway
  calibration browser + sample captures cost ~41s on the 2,381-frame
  benchmark comp (auto render: 111.6s -> 70.1s total).
- Self-verify retry reverts the inversion: the re-render returns to the
  pre-inversion parallel screenshot path (disk) instead of single-worker
  screenshot streaming, the slowest shape for exactly the comps drawElement
  damages.
- HF_DE_SINGLE_MIN_FRAMES="" (set-but-empty) now falls back to the 900
  default instead of aliasing the 0 kill switch.
- Timeout advisory uses the RESOLVED worker count — an inverted render that
  times out no longer prints "Retry with --workers 1" (the configuration
  that just failed).
- Telemetry: deWorkerInversion recorded in capture observability (failed
  renders are attributable), emitted as literal false when not fired
  (queryable denominator), and the drawElement perf input shape is one
  exported DrawElementPerfInput type instead of three copies.
- Tests: requestedWorkers undefined (the value production actually passes)
  + the four new predicate guards; 91/91.

Validation: e2e auto render — calibration skipped (deInversionEligible),
inversion fires, DE verified 4x inf, total 70.1s (was 111.6s);
HF_DE_SINGLE_MIN_FRAMES=0 restores calibration + parallel; canary suite
7/7 (PSNRs identical); tsc/oxlint/oxfmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(producer,cli): review round 2 — loss-cohort telemetry, retry-plan helper, boundary tests

- de_worker_inversion is now a tri-state string ("inverted" | "reverted" |
  "none") instead of a boolean: the self-verify retry marks the render
  "reverted" rather than resetting to false, so the dashboard can segment
  the lost-inversion cohort first-class instead of inferring it from
  deSelfVerifyFallback + frame-count joins (james-russo #1).
- The retry rollback is extracted to resolveInversionRetryPlan (pure,
  exported) with unit coverage: pre-inversion worker-count restore,
  streaming re-resolution (multi-worker retry -> disk), "reverted" state,
  null when never inverted (james-russo #2).
- WOULD_RESOLVE_MULTI_WORKER named constant replaces the bare sentinel 2
  (james-russo #5); minFrames: -1 boundary case added (miga #3).

94/94 renderOrchestrator tests; tsc/oxlint/oxfmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(producer,cli): emit de_pre_inversion_workers for the parallel counterfactual

The ramp-down decision needs "did DE beat the parallel render it displaced",
not just "did DE beat single-worker screenshot". Emitting the worker count
the auto-resolution chose BEFORE the inversion pinned it to 1 makes the
parallel counterfactual computable per render (screenshot ms/frame from the
verify samples / W x the measured parallel-efficiency curve). Set only when
the inversion fired.

Smoke: 2,381f auto render -> de_worker_inversion="inverted",
de_pre_inversion_workers=5, mode=drawelement, verify armed 4. 99/99 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:48:07 -07:00
Miguel Ángel 4b3c73d941 fix(cli): upgrade and update notice use the detected install method
* fix(cli): upgrade + update-notice use the detected install method

hyperframes upgrade hardcoded 'npm install -g', so bun/pnpm/brew users either
saw it fail or silently got a shadowed npm copy while their real (older) binary
kept running. Route the install through detectInstaller() via a new
installInvocation() argv helper; for skip kinds (ephemeral npx/bunx,
project-local, unknown) print 'npx hyperframes@latest' instead of guessing.

The passive update notice now shows the detected manager's command too. Semver
safety guard consolidated into a shared isSafeVersion(). Suppression gates and
the background auto-update flow are unchanged.

* test(cli): pin the shell:false contract of the --yes install path

Export runDetectedInstall and add a mocked-execFileSync test asserting the
detected manager binary is spawned with the exact installInvocation argv,
{stdio:inherit, shell:false}, and that an install failure sets a non-zero exit
code without throwing. Addresses review nit on the untested --yes path.

* fix(cli): guard the registry version at the boundary; execFile the auto-installer

Security (addresses review): a poisoned registry data.version (e.g.
'1.2.3; rm -rf /') was cached unvalidated and flowed into the background
auto-updater, which ran it via exec() -- a shell -- so a registry compromise
meant RCE on the next CLI run. isSafeVersion only covered the two touched
consumers (upgrade, notice), not this third sibling (scheduleBackgroundInstall).

- Guard at the registry boundary in checkForUpdate: only a strict-semver STRING
  is trusted; a non-string or metachar-bearing data.version is never cached and
  falls back to the last known-good version. The cache-read and fallback paths
  re-validate too, so a pre-existing poisoned cache can't leak through. One gate
  closes all three consumers and any future one; per-consumer checks stay as
  defense in depth.
- The detached auto-installer now runs via execFile(bin, args, shell:false),
  reusing installInvocation, matching the interactive runDetectedInstall path --
  the shell is gone from that path entirely.

Tests: reject poisoned / non-string registry version (never cached); accept a
valid semver.
2026-07-07 18:40:43 -04:00
Miguel Ángel de27b46680 fix(cli): default render fps to the composition's data-fps
* fix(cli): default render fps to the composition's data-fps

hyperframes render hard-coded fps to 30 when --fps was omitted, ignoring a
data-fps declared on the composition root — so a composition authored at
data-fps="24" silently rendered at 30fps unless the user knew to pass --fps 24.
The runtime already honors data-fps; the CLI now matches it.

Precedence: explicit --fps > composition root data-fps > 30. New pure
readCompositionFps() extracts the root data-fps via linkedom (mirrors the
runtime's root resolution: [data-composition-id][data-root=true], else the
outermost [data-composition-id]); render validates it through parseFps and
falls back to 30 on an absent/invalid value. Unit-tested.

* fix(cli): honor composition data-fps on cloud renders and --composition targets

The local render command read data-fps from project.dir/index.html even when
--composition rendered a different file, and the lambda/cloudrun render paths
ignored data-fps entirely (hardcoded ?? 30). Both are the same silently-wrong-
fps bug on other render entry points:
- render.ts resolves the entry file first, then reads data-fps from the file
  actually being rendered (falling back to index.html).
- lambda render/render-batch and cloudrun render/render-batch default fps from
  the composition's data-fps, accepted only when it is one of the cloud-allowed
  values {24,30,60}, else the existing 30 default. Explicit --fps still wins.

* fix(cli): drop citty fps default so data-fps resolution actually runs

The fps arg had default: "30", so citty set args.fps="30" on omission and
resolveDefaultFpsArg short-circuited (explicitFps never null) — reverting the
command to always-30 and making the whole data-fps feature a no-op (caught in
review). Remove the arg default; the "30" fallback already lives at
parseFps(fpsArg ?? "30"). Adds a regression guard asserting the arg has no
default.

* test(cli): read citty args through a plain record in the fps-default guard

The regression guard accessed cmd.args.fps directly, but citty types args as
Resolvable<ArgsDef> so .fps failed typecheck in CI. Read it through a plain
record cast.
2026-07-07 17:10:10 -04:00
Miguel Ángel 92f3116dee fix(cli): re-download the browser when the cached archive is corrupt
A partially-downloaded or interrupted chrome-headless-shell archive left in the
cache makes @puppeteer/browsers' install() throw "invalid
end-of-central-directory" during extraction. That error propagated out of the
browser check and hard-blocked the render, forcing users onto the fallback
renderer until they manually cleared the cache — a recurring Windows failure.

Detect the corrupt-archive extraction error (isCorruptArchiveError), clear the
cache to drop the bad archive, and retry the download exactly once; non-corrupt
errors and a second corruption still propagate (no infinite retry). The pure
predicate and the recovery wrapper are unit-tested.
2026-07-07 17:10:06 -04:00
Miguel Ángel 4834de37f4 fix(cli): lint sets process.exitCode instead of process.exit() to flush stdout
`hyperframes lint --json` wrote the JSON payload with console.log() and
then immediately called process.exit(). process.exit() terminates the
process before Node flushes an asynchronously-buffered stdout, which is
what a non-TTY (piped) stdout is — so `hyperframes lint --json | tee`,
`> out.json`, or any agent/CI capture silently lost the entire payload
on Windows (reported on 0.7.31 non-TTY). The same console.log-then-exit
pattern was on all four exit sites (both --json branches and the
human-readable + thrown-error paths), so any of them could truncate.

Fix: set process.exitCode and return, letting run() unwind so Node
drains stdout before exiting with the code. This is exactly the pattern
the other commands (publish/transcribe/upgrade/play/present) already
use; lint was the outlier still calling process.exit() after writing.

Test: new lint.test.ts drives the command's run() with mocked
lintProject/resolveProject and a process.exit spy that throws if
called. Covers the --json-with-errors, --json-clean, --json-thrown, and
human-readable paths — each asserts process.exit is never called and
the correct exitCode is set. Fails against the pre-fix code (the spy
throws on the first process.exit).
2026-07-07 17:10:02 -04:00
Miguel Ángel 60463d0bd5 chore: release v0.7.41 2026-07-07 20:30:24 +00:00
Miguel Ángel 2bd9bb6f69 chore: release v0.7.40 (#2024) 2026-07-07 12:30:13 -04:00
Vance IngallsandClaude Fable 5 4c8064d4d3 fix(cli): figma component import survives unrenderable nodes (#2022)
Live testing against a real community file (Ratings) found a nested
instance node figma refuses to render as svg — which aborted the entire
component import. The rasterize loop now retries the node as png, and
only if both formats fail warns and skips THAT node (placeholder keeps
its data-figma-rasterize marker, no src) instead of failing the import.
On the file that surfaced this, the png retry recovers the node — 31/31
placeholders get assets.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 01:59:48 -07:00
WaterrrForever 306a291dea fix(skills): audit descriptions — trim routing prose, fix stale facts, add missing triggers (#1990)
* fix(skills): audit descriptions — trim routing prose, fix stale facts, add missing triggers

Descriptions are the always-loaded routing tier; this audit rebuilds them on
one principle: discriminate by input shape, not pipeline internals.

- Trim creation-workflow descriptions to positive trigger + nearest-neighbor
  disambiguation + /hyperframes escape hatch; full routing prose already
  lives in each skill body's route-confirm block and the router
- Codify the workflow-vs-domain split as ownership (owns the end-to-end
  deliverable vs capability layer pulled in mid-flight) in /hyperframes,
  and widen "make me a video" framing to deck / composition port
- Fix stale facts: embedded-captions identity count (desc 32, body 17 →
  actual 36 = 10 classic + 26 themed), six→ten visual languages, retired
  RVM/Standard wording in router details, figma shader transport
  (MCP → MCP source / native export), keyframes "cursor demos" (no backing
  content), hyperframes-media scripts/audio.mjs leak
- Register missing capabilities: motion-graphics maps category (was in
  categories/ but absent from its own table, description, and router),
  asset-fusion + news triggers, slideshow page-to-deck + presenter mode,
  general-video editing, talking-head-recut 16:9/9:16/4:5 canvas,
  cli feedback + lambda sites, product demos, mood-brief BGM generation
- website-to-video: relabel promo-shaped video types to keep the promo
  boundary with /product-launch-video; drop headless-Chrome wording
- music-to-video: lyric timing via /hyperframes-media transcription or
  user-supplied lyrics, placed on the beat grid
- Sync catalogs in lockstep (CLAUDE.md, AGENTS.md, README,
  docs/guides/skills.mdx, CLI project templates): add music-to-video +
  slideshow entries, complete the domain-skill lists, and extend the
  catalog-maintenance rule to cover AGENTS.md and the templates

Validated with a 35-case description-only routing eval: 35/35 both before
and after the rewrite (including new maps / asset-fusion / news probes).

* fix(skills): post-media-v2 consistency — stale media ref, router figma wording, catalog rows

- music-to-video: lyric transcription now routes to /media-use (the
  retired /hyperframes-media was still referenced)
- router capability map: figma row gains the shaders fact (MCP source /
  native export), matching the SKILL.md source of truth
- media-use catalog rows (CLAUDE.md, README, docs/guides/skills.mdx):
  add image models + captioning, aligning with the v2 description
- catalog rule #1: root AGENTS.md carries the workflow list only (it
  has no domain-skill section) — rule wording now says so
2026-07-07 14:28:46 +08:00
Miguel Ángel 3d8372f880 feat(cli): associate signed-in HeyGen account with telemetry (#2020)
* feat(cli): associate signed-in HeyGen account with telemetry

Sign-in telemetry currently attributes everything to the anonymous
install id, so the sign-in funnel can be counted but a completed sign-in
can't be tied to the account it produced. This associates the two.

- On a completed sign-in, emit a PostHog `$identify` alias whose
  `$anon_distinct_id` is the install's anonymousId, so events recorded
  before sign-in stitch to the same person, and tag `auth_login_completed`
  with the account identity (the pre-plumbed `distinctId`).
- `/v3/users/me` exposes no opaque user_id, so the identity key is the
  account email, falling back to username (single `identityKey` helper).
- Both no-op under the `telemetry disable` opt-out and only fire after
  the user chooses to sign in.

Privacy disclosure updated in lockstep, since this is the first PII the
CLI attaches: the first-run telemetry notice and the telemetry section
of docs/packages/cli.mdx now state that signing in links your account
email to your usage.

Tests: identifyUser payload + no-op, completion attribution incl.
username fallback and no-identity-on-reject/empty. Verified end-to-end
against the built CLI: pre-auth events anonymous, $identify carries
$anon_distinct_id, completion carries the account email.

* docs(cli): disclose the username identity fallback

Review gating item: identityKey is `email ?? username`, but the
first-run notice and cli.mdx said only "email", so an emailless
account's username would reach PostHog undisclosed. `/v3/users/me`
treats email as optional (pickString), so the fallback is live code,
not dead — disclose it rather than assert an unverifiable email
guarantee. Both surfaces now say "email, or username if the account
has no email".

Also soften the identityKey comment: it implied username is "less
identifying", but HeyGen usernames are often email-shaped, so the note
now states username is a fallback, not a privacy win.
2026-07-07 02:23:54 -04:00
Vance Ingalls 229e88eac5 chore: release v0.7.39 2026-07-06 22:48:42 -07:00
Vance IngallsandClaude Fable 5 b26c27576b feat(engine,producer,cli): verify video comps via deferred DE init + capture p50 (#2015)
* feat(engine,producer,cli): verify video comps via deferred DE init + capture p50

Closes the two biggest gaps in the first day of v0.7.38 wild data: 88% of
drawElement renders (video comps initialized via probe sessions) ran with
self-verification unarmed, and speedup was measurable on only 3 of 76 renders.

- Deferred drawElement init: probe sessions initialize before video
  extraction, so they have no frame injector — ground-truth screenshots
  would capture black <video> boxes, and verification skipped the whole
  comp. DE init now stops after the gates for injector-less video comps
  (deInitDeferred; autoAlpha flag retracted in case no path completes it)
  and completeDeferredDrawElementInit finishes verification + canvas
  injection + worker-encode at capture time, once
  prepareCaptureSessionForReuse has attached the injector. Validated
  end-to-end: a probe-path video comp now arms 4 ground-truth frames with
  real video pixels (3x inf + 64.7dB) and renders drawElement verified.
- capture_p50_ms: per-frame capture durations are sampled
  (capturePerf.frameMs; batch frames get the batch mean) and the median
  ships as CapturePerfSummary.p50TotalMs -> RenderPerfSummary.captureP50Ms
  -> render_complete capture_p50_ms. Unlike capture_avg_ms it is immune
  to first-frame warmup and stage-setup amortization — smoke: avg 15ms vs
  p50 8ms on the same render, p50 matching the measured steady-state
  floor. Dashboard speedup tiles can drop their frame-count floor once
  this ships.
- video_count on render_complete: segments speedup by video-injection
  comps (whose per-frame gain is legitimately lower) vs pure-graphics.

Canary suite 7/7; engine suite 905 passed (1 pre-existing upstream
failure); tsc/oxlint/oxfmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(producer): complete deferred drawElement init on the disk capture path

Review (miga): a probe-initialized video comp falling back to the disk path
kept deInitDeferred and silently stayed in screenshot mode — a regression
for PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true renders that previously ran
drawElement there. Complete the deferred init on the sequential disk path
under the same explicit-opt-in test the orchestrator clamp uses; default-on
renders stay on the screenshot baseline (this path has no drain-time
self-verification, per the #1998 confinement rule).

Validated: video comp + PRODUCER_ENABLE_STREAMING_ENCODE=false + explicit
opt-in logs "(deferred drawElement init)" completion on capture_disk and
renders correct video pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:47:29 -07:00
Miguel Angel Simon Sierra 8c3590a90e feat(cli): parakeet ASR engine for transcribe (--engine) + HYPERFRAMES_PYTHON override 2026-07-06 23:34:32 -04:00
Vance Ingalls e9c37b5fdb Merge branch 'main' of github.com:heygen-com/hyperframes 2026-07-06 17:48:21 -07:00
Vance Ingalls d8d3a93b0d chore: release v0.7.38 2026-07-06 17:47:34 -07:00
Miguel Ángel 9fe06f30da feat(cli): forward feedback submissions to the backend feedback endpoint (#2003)
* feat(cli): forward feedback submissions to backend endpoint

* fix(cli): truncate feedback fields to backend caps + ack before forwarding

Addresses PR review (via):
- Truncate comment (2000) / cli_version (100) / env (500) to the backend DTO
  caps before POSTing, so a pasted stack trace is forwarded truncated instead
  of rejected with a 422 the best-effort path swallows silently.
- Print "Thanks for the feedback!" before the best-effort forward so the ack
  isn't blocked behind the (bounded) network call.

* fix(cli): type feedback fetch mock
2026-07-06 20:44:42 -04:00
Vance IngallsandClaude Fable 5 1005703441 feat(engine,producer,cli): drawElement release telemetry on render_complete (#2002)
Default-on drawElement ships with a runtime self-verification net (#1998);
this makes its in-the-wild behavior observable. Every render_complete event
now answers: which capture mode actually ran, why drawElement disengaged
when it did (compile gate / producer clamp / engine init gate), whether the
self-verify net fired and why, and how much margin verification had.

Follows the static-dedup telemetry pattern: engine session fields →
CapturePerfSummary → RenderPerfSummary.drawElement → snake_case props on
render_complete.

New event props: de_capture_mode, de_compile_gate, de_clamp_reason,
de_gate_reason, de_worker_encode, de_verify_armed, de_verify_checked,
de_verify_min_db (margin above the 32dB threshold — drift here is the
early-warning signal before fallbacks spike), de_verify_init_ms,
de_self_verify_fallback, de_fallback_reason, de_blank_suspects,
de_blank_deterministic_accepts, de_blank_recaptures, de_boundary_frames,
de_ncpr_fallbacks.

Validated end-to-end on live renders: drawelement path reports mode/verify
counters/minDb/init cost; a blur-gated comp reports mode=screenshot +
gate_reason=css_effect:filter; a forced verification failure reports
self_verify_fallback=true + fallback_reason=psnr.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:39:46 -07:00
Xuanru LiandClaude Opus 4.8 906c8d04f8 fix(cli): localize remote assets before validate so it matches render (#2001)
validate served the composition over a loopback origin and let headless
Chrome fetch remote <img crossorigin>/@font-face assets cross-origin, while
the render pipeline downloads them to disk first. Buckets whose CORS
allowlist omits the loopback origin then failed the CORS-mode request with a
false net::ERR_FAILED that never occurs in the real render, pushing authors
(and agent pipelines) to delete crossorigin — which disables WebGL
color-grading/shaders for that asset.

Reuse producer's localizeRemote{Media,Image,FontFace}Sources in validate,
downloading into a temp dir served as an extra static-server asset root
(project dir untouched, cleaned up after). validate now matches render.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:20:18 -07:00
Vance IngallsandClaude Fable 5 ccc1308839 docs(figma): storyboard blurb reworded + frames-are-app-states escalation (#2004)
Field feedback from a raw-API agent build (join-the-world-flow): the
catalog blurb's word 'animatics' encodes the PNG-slideshow architecture
the skill body explicitly forbids — an agent routing by the blurb
concludes the shipped behavior is frames-as-pictures. Reworded to
'reconstructed motion (frames read as states, not slides)' across all
catalog surfaces (skill frontmatter, CLAUDE.md, README, skills.mdx,
hyperframes router, figma guide).

Also codifies the stronger doctrine that build demonstrated as
storyboard rule 10: when every frame is the same product UI in
successive states, rebuild the app as live DOM (Phase-3 for stateful
parts, real pixels for static chrome — code what changes state, freeze
what doesn't) and perform frame deltas as interactions instead of
tweens. Spec §5.1 records the escalation + field origin.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:55:47 -07:00
Vance Ingalls 241f9d683e feat(studio,studio-server,cli): render cancel end-to-end + renders/nle/storyboard UX (#1963) 2026-07-06 16:43:48 -07:00
Vance IngallsandClaude Fable 5 ec06f4bf89 feat(engine,producer): drawElement fast-capture default-on with runtime self-verification safety net (#1998)
* feat(engine,producer): drawElement fast-capture default-on with runtime self-verification safety net

Flip useDrawElement + worker-encode defaults on (HF_DE_BATCH default 4),
clamped in resolveConfig to hosts where drawElement can engage (macOS +
hardware-GPU browser) so page-side shader compositing is untouched
everywhere else; explicit env opt-in keeps attempt-and-gate semantics.

Safety net makes default-on safe: the compile/init gates catch predictable
incompatibility; this catches the intermittent residue no static analysis
can see (stale paints, dropped background images, transient blank frames).

- engine: captureDeVerificationFrames — K=4 (HF_DE_VERIFY) ground-truth
  screenshots at init, after gates + armStaticDedup, BEFORE canvas
  injection (post-injection screenshots show the canvas bitmap, not the
  DOM). Runs the video-injection hook per sample; double-captures so
  rAF-driven text counters settle (a single immediate screenshot captures
  stale text and false-positives). Skips png, <10 frames, implausible
  __hf.duration (infinite-repeat GSAP sentinel).
- producer: guardFrame on both worker-encode drains — rolling-median blank
  guard with retry-once at drain (byte-identical retry ⇒ deterministic dark
  frame, accepted; retry save/restores the static-dedup anchor) + ffmpeg
  PSNR self-verify vs ground truth (HF_DE_VERIFY_MIN_DB, default 32dB;
  natural agreement ≥45dB, damage ≤25dB). Breach dumps the frame pair to
  tmpdir and throws DrawElementVerificationError.
- orchestrator: one-shot retry — on verification error the whole render
  re-runs with forceScreenshot (slower, never wrong); telemetry flag
  deSelfVerifyFallback.
- tooling: de-canary-suite.sh (7-comp release gate with expected verdicts),
  de-gatecheck.sh (init-only corpus routing classifier), we-render.mjs.

Validated: canary suite 7/7; 611-comp routing sample 54% drawelement /
37.5% gated / 8.3% comp-defect; 12/12 risk-band renders clean on bare
defaults (48/48 verify samples); engine suite 888 passed; caught two real
intermittent damage classes in the wild (background-image drop, root-props
offset) that previously shipped silently.

Kill switches: PRODUCER_EXPERIMENTAL_FAST_CAPTURE=false,
HF_DE_WORKER_ENCODE=false, HF_DE_BATCH=0, HF_DE_VERIFY=0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(engine,producer): harden the drawElement self-verification net (max code-review findings)

15 confirmed findings from the adversarial review of the default-on flip;
the load-bearing five:

- Ground-truth capture no longer scrubs GSAP state: seek(0) + forced frame
  FIRST (lazy .from()/overlap tweens record start values on first seek —
  mid-timeline scrubs corrupted them for the whole render, and since DE
  frames and truth shared the corruption, PSNR passed on damaged output),
  then ascending even-spread fractions, page left at frame 0.
- Default-on drawElement is confined to the verified path: resolveConfig
  requires worker-encode (the drain that runs the net), the orchestrator
  disengages the default when the render takes the disk path or parallel
  capture (no drain verification there), and closes a drawElement-initialized
  probe session rather than letting the unverified path reuse it. Explicit
  PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true keeps old attempt-and-gate behavior.
- Blank-frame retry can no longer splice wrong-frame pixels: recapture goes
  through recaptureDrawElementFrameForVerify — no static-dedup shortcut
  (lastEncodeResult runs ahead of the drain) and no "No cached paint record"
  screenshot fallback (post-injection that captures the canvas = the LAST
  drawn frame); any recapture failure falls back the whole render.
- Verify indices derive from the producer-resolved duration
  (CaptureOptions.compositionDurationSeconds) instead of raw __hf.duration,
  so samples always land inside the drained range.
- The platform clamp accepts "auto" GPU mode — the stock CLI resolves auto,
  and the literal-"hardware" clamp made default-on a no-op for the primary
  audience (masked in validation by explicitly-set env).

Also: NaN-safe env parses (HF_DE_VERIFY / HF_DE_VERIFY_MIN_DB / HF_DE_BATCH);
video comps skip verification when the session has no frame injector (probe
sessions — black-video truth false-positived); psnr infrastructure failures
skip the sample instead of failing the render; boundary-saturated sample
indices are skipped; shader-transition comps prefer page-side compositing
over default drawElement and compile-gated comps get page-side compositing
restored; observability.clearFailure un-brands the recovered first streaming
attempt; canary suite exempts known-marginal "any" comps from the cross-path
PSNR gate; dead we-render options removed; clamp tests pin their env.

Validated: canary suite 7/7; auto-GPU bare render engages the full stack;
disk-path and worker-encode-off renders disengage default drawElement;
malformed HF_DE_VERIFY_MIN_DB still verifies at the default threshold;
engine suite 890 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(engine,producer): review fixes — PSC intent, verify-threshold clamp, fail-closed canaries

Addresses miguel-heygen's review on #1998:

- Page-side compositing restore preserves explicit caller intent (blocker):
  resolveConfig now records pageSideCompositingAutoDisabled only when IT
  turned page-side compositing off because drawElement was on; the
  compile-time drawElement gates restore page-side compositing only when
  that flag is set. An explicit enablePageSideCompositing:false from the
  programmatic API or HF_PAGE_SIDE_COMPOSITING=false stays off. Pinned by
  two config tests.
- HF_DE_VERIFY_MIN_DB clamped to [10, 60] with a warning on out-of-range
  values: below ~10dB the check passes severe damage; above ~60dB natural
  encoder differences force a screenshot fallback on every verified render.
- de-canary-suite.sh + de-gatecheck.sh run under set -euo pipefail with
  explicit `|| true` on expected-nonzero commands (render exits handled by
  the suite's own checks, grep no-match, kill/pkill/wait races) and a hard
  FAIL when the PSNR compare produces no value — release canaries fail
  closed. Full suite re-run green (7/7) under the new flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:29:30 -07:00
Vance Ingalls e04f6dda37 feat(engine,cli): drawElement fast-capture config + CLI flag (#1916)
## drawElement fast-capture — config + CLI flag (stack 1/6)

Foundation layer for the drawElement fast-capture feature: the config surface and CLI/Docker plumbing that the rest of the stack builds on.

### What this adds
- **`packages/engine/src/config.ts`** — new config fields for fast capture: `useDrawElement` / `enableDrawElementWorkerEncode` (macOS-GPU `drawElementImage` capture + worker-offloaded JPEG encode), resolved from env in `resolveConfig` (env `HF_DE_WORKER_ENCODE`). Wired alongside main's existing `staticFrameDedup` (unified downstream in 4/6).
- **`packages/cli/src/commands/render.ts`** — `--experimental-fast-capture` flag → sets `experimentalFastCapture`; `--debug` passthrough.
- **`packages/cli/src/utils/dockerRunArgs.ts`** — pass the fast-capture env through to the container.
- **`.github/workflows/fast-video-validation.yml`** — CI job validating fast-capture renders.
- `.oxlintrc.json` / `.fallowrc.jsonc` — ignore-pattern housekeeping for the new paths.

### Notes
- Config-only + entrypoint; no capture behavior yet (that's 2/6–4/6).
- Tests: `config.test.ts`, `dockerRunArgs.test.ts` added.

---
**Stack (drawElement fast-capture, rebased onto current `main`, supersedes #1295 + #1444):**
1. **#1916 config + CLI** ← you are here
2. #1917 drawElementImage capture service
3. #1918 3D projection + compositor-effect risk gate
4. #1919 frame-capture core (routing, worker-encode, static-dedup unification)
5. #1920 producer render stages + remote bg-image localizer
6. #1921 lint rule + player media sync

⚠️ Intermediate PRs (1–5) are split by package boundary for review and **do not each compile independently** (cross-file deps); the complete feature is green at the stack tip (#1921) — tsc-clean on engine + producer, 231 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-06 15:57:30 -07:00
Miguel Ángel 769dc702c1 feat(cli): emit sign-in lifecycle telemetry (#2000)
* feat(cli): emit sign-in lifecycle telemetry

The CLI tracks command and render lifecycles but emits nothing for
`auth login`, so sign-in outcomes are invisible on the observability
dashboards — a completed sign-in, an abandoned browser flow, and a
rejected key all look identical (absent). This leaves a blind spot in
the same funnel the render events already cover.

Add three events mirroring the existing `trackX` pattern:
  - auth_login_started    (method: oauth | api_key)
  - auth_login_completed  (method)
  - auth_login_failed     (method, reason)

`reason` is a fixed low-cardinality enum (flow_error / no_credential /
rejected / invalid_input). No token, key, identity, email, or free text
is ever attached — consistent with the existing anonymous telemetry and
the `telemetry disable` opt-out. Wired into both the OAuth and
--api-key paths in `auth login`, with unit coverage for the new events.

* fix(cli): close sign-in telemetry funnel dropout gaps

Follow-up so `started` reconciles to `completed + failed` on the common
abandonment paths, which the first cut missed:

- Interactive prompt cancel (Ctrl-C) now surfaces as a throw that the
  single catch in the api-key path records as `aborted`, instead of a
  bare exit with no event.
- A stdin read that times out in non-TTY `--api-key` mode now records
  `aborted` before the error propagates, rather than exiting silently.
- OAuth split: a timed-out browser callback (user closed the tab) is
  tagged `flow_timeout`, separated from real `flow_error` (IdP/network),
  since the walk-away timeout is the dominant non-error dropout.

Also pre-plumb an optional `distinctId` on the three trackers, mirroring
trackRenderComplete/trackRenderError. Unused today; it lets a later
identity-level attribution be a one-line callsite change rather than a
signature sweep. Coverage added for the new reasons and forwarding.
2026-07-06 16:59:47 -04:00
ukimsanov 413ee07da5 fix(studio-server): share background removal job runner 2026-07-06 13:27:45 -07:00
ukimsanov a3bf7eb995 feat(studio-server): add media processing routes 2026-07-06 13:25:04 -07:00
Vance IngallsandClaude Fable 5 e9076324e7 feat(cli): figma import telemetry — subcommand labels, typed error codes, figma_import event (#1979)
Closes the observability gaps on the figma integration:
- withFigmaErrors takes a command label (figma:asset|tokens|component) and
  reports the failure inline before its process.exit — the top-level
  trackCommandFailures wrapper never sees self-exiting commands, so typed
  codes (NO_TOKEN, BAD_TOKEN, FORBIDDEN, RATE_LIMITED) were invisible.
  FigmaClientError codes surface as the error name for dashboarding the
  first-run funnel (NO_TOKEN -> later success = onboarding conversion).
- new figma_import event per import: phase, duration, reused (dedup
  effectiveness), tokens variables-vs-styles mode + entry count
  (Enterprise gating rate), unresolved-binding + rasterized-node counts
  (fidelity degradation). No fileKeys, node ids, names, or descriptions.
- /figma skill fires the events beacon (figma-motion / figma-shaders /
  figma-storyboard) for the MCP phases that never touch the CLI.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 23:27:58 -07:00
James RussoandClaude Opus 4.8 232d591479 feat(cli/telemetry): surface unrecognized agents in the agent_runtime=null bucket (#1978)
* feat(cli/telemetry): surface unrecognized agents in the agent_runtime=null bucket

agent_runtime is a closed allowlist: an agent we have no rule for collapses
to null with no trace of what it was, so ~18% of CLI users are unattributable
and new agents stay invisible until reverse-engineered by hand.

Add detectAgentHints(), a self-populating residual signal computed only for
the null bucket (gated off classified events):
- agent_hint: value of AGENT / AI_AGENT (the emerging self-identification
  convention; Crush and Goose set AGENT=<name>) — names agents the allowlist
  misses.
- term_program: raw TERM_PROGRAM (editor name) — catches the IDE-terminal
  class the same way the cursor/windsurf rules do.
- agent_env_hints: sorted, comma-joined "agent-ish" env-var KEY names present
  but matched by no vendor rule — a fingerprint that clusters by agent.

Privacy stays consistent with the existing "never read secret-shaped values"
stance: agent_env_hints emits key names only; the three value-reads are vars
whose sole purpose is non-secret identification, each passed through a strict
short-slug allowlist so anything long/spaced/secret-shaped is dropped.

Breaking down agent_hint / agent_env_hints filtered to agent_runtime IS NULL
AND is_tty=false gives a ranked leaderboard of new agents to promote into
VENDOR_RULES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli/telemetry): guard agent_hint/term_program against short credential-shaped values

Review feedback (Magi, #1978): the short-slug allowlist in sanitizeHint() still
accepted short credential-shaped values (AGENT=sk-ant-api03,
AGENT=AKIAIOSFODNN7EXAMPLE, AGENT=github_pat_abc), so the "never emit a secret"
claim wasn't actually enforced — only overlong values were dropped.

Add a credential-shape guard on top of the slug allowlist:
- known token/credential prefixes (sk-, ghp_, github_pat_, akia, ya29, ...)
- any unbroken alphanumeric run >= 16 chars (key bodies, hex, base64-ish),
  while agent names segment on _/-/. and keep each run short.

Replace the single overlong-value test with the short credential shapes from the
review (parametrized) plus a positive case (gemini_managed_agent) proving real
multi-segment names still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:31:13 -07:00
James 9de41ce316 chore: release v0.7.37 2026-07-06 05:59:36 +01:00
Miguel Ángel e8d19b1b43 chore: release v0.7.36 2026-07-05 19:25:42 +00:00
Miguel Ángel 98b539df72 fix(cli): prefer real ffmpeg exe over cmd shim (#1958) 2026-07-05 12:24:50 -07:00
Miguel Ángel 2f55ea678a chore: release v0.7.35 2026-07-05 18:48:24 +00:00
Miguel Ángel 114d31919e chore: release v0.7.34 (#1954) 2026-07-05 10:55:11 -07:00
Miguel Ángel 78cca797f1 chore: release v0.7.33 2026-07-04 22:33:27 +00:00
Miguel Ángel 16fe1368ff chore: release v0.7.32 2026-07-04 21:23:19 +00:00
Miguel Ángel 3f49f107eb fix(cli): validate seeks the runtime player directly, not raw timelines (#1895)
* fix(cli): validate seeks the runtime player directly, not raw timelines

validate's seekTo() only checked for window.__hf.seek (a bridge object
the producer's render-pipeline file server injects) before falling
back to grabbing window.__timelines and calling .seek() on each raw
GSAP timeline directly. validate serves compositions through a plain
static file server that never injects that bridge, so this fallback
ran on every single validate invocation.

Seeking a raw timeline moves the animation state but skips the
runtime's own [data-start]/[data-duration] visibility sync
(syncMediaForCurrentState in packages/core/src/runtime/init.ts), which
is what sets an off-window clip's inline visibility/display styles.
Skipping it left elements outside their timeline window looking fully
visible to any check that reads computed style afterward at that seek
time.

This surfaced as validate's WCAG contrast audit
(contrast-audit.browser.js) flagging text in off-window clips against
whatever background happened to be behind them, since its own
visibility filtering trusts the runtime to have already hidden them.

Fix: prefer window.__player.renderSeek, which the composition runtime
exposes directly on every page load (no bridge required) and which
does run the visibility sync, before falling back to the __hf/raw
timeline paths. No changes needed to contrast-audit.browser.js itself
since its existing visibility check now sees correct computed style.

No new test added: seekTo's branch selection runs entirely inside a
page.evaluate() callback, which Puppeteer serializes via .toString()
for the browser context, so it can't import and call a project-local
window.__player stub from a jsdom/vitest test without testing a copy
of the logic rather than the shipped code. Verified instead by reading
the runtime chain end-to-end: window.__player.renderSeek is always set
by packages/core/src/runtime/init.ts's createPlayerApiCompat, calls
through to player.renderSeek, which calls syncMediaForCurrentState().

* fix(cli): wait for runtime seek target in validate
2026-07-04 14:08:26 -07:00