* fix(producer): pass variables to duration probe
* fix(producer): tolerate rounded frame-boundary durations
* fix(cli): resolve relative data-start references in composition duration
`compositions --json` computed each timed child's start with a bare
parseFloat(data-start ?? "0") in parseCompositions (host duration) and
parseSubComposition (sub-comp duration). A relative reference like
data-start="s1" ("start when clip s1 ends") is not numeric, so parseFloat
returned NaN and that clip's contribution to the max-end was silently
dropped — a host with two 3s clips (2nd data-start="s1") reported duration 3
instead of 6, breaking compositions/inspect/snapshot for composition-clip
relative timing.
Resolve relative references the same way the extractor does (parseStartExpression
from @hyperframes/core + a findReferenceTargetEl/resolveReferencedStart port,
since the engine's referenceResolver isn't a public export across the package
boundary). Verified: host duration now 6; 3 tests pass.
(Implemented via Codex; verified independently.)
When `@puppeteer/browsers`' install() rejected for any reason the corrupt-
archive recovery path couldn't handle (`All providers failed for
chrome-headless-shell <ver>`, DNS/network, macOS Gatekeeper, a second
corruption), the raw error propagated with no mention of the escape
hatch. Wrap the surfaced error to name `HYPERFRAMES_BROWSER_PATH` with a
platform-specific example (macOS/Windows/Linux) and keep the original
via `cause`.
Field feedback ts 1784055194.202169 in #hyperframes-cli-feedback
(darwin/arm64, HF CLI 0.7.57) hit this and recovered by pointing the
env var at system Google Chrome — they discovered the workaround on
their own because the error didn't tell them.
Download-time sibling of #2078 (SIGTRAP at launch, same remediation,
different trigger).
— Via
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a `core-skills` entry to .claude-plugin/marketplace.json declaring
the core skill set (the /hyperframes router, the hyperframes-* domain
skills, and media-use). The upstream vercel-labs/skills CLI reads that
entry's `skills` array for its interactive picker: the core set renders
under a "Core Skills" group and everything else falls into "Other", so
a human running `npx skills add heygen-com/hyperframes --full-depth`
can tell the always-needed core set apart from the on-demand creation
workflows — mirroring the core/on-demand tiers `hyperframes skills
update` already enforces (isCoreSkill in skillsManifest.ts).
The array deliberately lives on a separate marketplace entry, NOT on
plugin.json or the `hyperframes` entry: Claude Code treats a manifest
`skills` array as that plugin's skill allowlist (verified against
claude CLI), so attaching it to the full plugin would narrow it from
all skills to the core 8. As a side effect the new entry is itself a
coherent Claude Code plugin — `core-skills@hyperframes` installs just
the core set — while `hyperframes@hyperframes` keeps auto-discovering
everything.
A new pin test keeps the marketplace list in lockstep with isCoreSkill
and the skills/ tree (alongside the existing FALLBACK_CORE_SKILLS pin),
and asserts the full plugin carries no allowlist. Agent installs are
unaffected — the upstream CLI detects agent environments and installs
non-interactively, and the hyperframes CLI always passes explicit
--skill flags.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(cli): stable-URL re-publish (send owned id, honest UX, --update, team id file)
Resolve a stable project id (committed team id > machine store > mint), send it
when authenticated so an owned re-publish updates the same URL in place, and
persist the server id+url. Report updated-vs-created honestly, add --update to
target a project explicitly, write a committable .hyperframes/project.json so a
team shares one link, and show the prior URL on re-publish.
* test(cli): env-gated E2E round-trip for stable-URL re-publish
Publish -> edit -> re-publish asserts one URL with updated content against a
live EF (HYPERFRAMES_E2E_API_URL + an authenticated runner); skipped otherwise.
* fix(cli): real team space, auth-gate --update/--space, safe team-file write
- --space + committed .hyperframes/project.json (projectId+spaceId) send X-Space-Id so a
team converges on one link; personal space stays the default for solo users
- --update/--space error when unauthenticated, and warn loudly when a resolved-but-invalid
token silently downgrades to a new anonymous URL (no more generic-tip-only)
- team-file write in its own try/catch so a read-only dir can't fake 'Publish failed'
- parseUpdateTarget handles scheme-less URLs + query/hash; X-Space-Id on metadata only (not S3 PUT)
- share readJsonRecord across the local + team descriptors
* test(cli): e2e team-space convergence + cross-space hijack guard
* test(cli): unit-test parseUpdateTarget url shapes (export for test)
* fix(cli): warn on committed-team miss too, not just --update
Sub-compositions are required to wrap markup, script, and style in <template>, but template content is an inert DocumentFragment that document-level querySelectorAll does not traverse — so 'hyperframes keyframes' silently surfaced zero tweens and zero CSS keyframes for every spec-conformant sub-composition. Extract from the document and every template content fragment (nested templates included, walked iteratively). Documents the extraction ordering contract and the deliberately document-root-only sub-composition discovery scan; pins template, multi-template, nested-template, and mixed-script cases with tests.
Send `X-HeyGen-Client-Source: hyperframes` from buildAuthHeaders on every
HeyGen API call (both OAuth and API-key), so backend billing meta can
isolate hyperframes CLI usage. The single buildAuthHeaders chokepoint covers
the core CLI + cloud client. Mirrors the media-use tagging; the OAuth-only
X-HeyGen-Source cli free-gate header is unchanged.
* feat(cli): coordinate-frame layout findings in check
Four production compositions shipped with 100-600px layout drift, each a
different coordinate-frame confusion the check graded info or missed
entirely: viewport pixels written as container left/top, gsap x/y
treated as absolute position, a -350px margin fighting flex centering,
and stage-relative path coords drawn into a nested SVG.
Three new layout findings close the class:
- positioned_out_of_parent: an absolute/fixed element rendering mostly
outside its positioning ancestor (warning) — the parent needs no
overflow clipping, which is what let container_overflow miss it.
- box_out_of_canvas: a painted panel breaching the canvas (warning) —
text is canvas_overflow's, media is frame_out_of_frame's, painted
boxes were nobody's.
- connector_detached: a connector path whose endpoints land far from
every anchorable element (warning) — measured coordinates drawn into
an SVG with a different origin.
canvas_overflow additionally promotes from info to warning when held
across samples AND the breach exceeds 5% of the canvas.
All three are persistence-tiered and respect data-layout-allow-overflow.
Verified against the four incident compositions: every one now surfaces
its drift as held warnings (previously: info or silence).
* fix(cli): harden coordinate-frame findings against review false positives
Reworks all three findings after two-lens review (adversarial FP hunt in
real Chrome + maintainer pass):
- escaped_container (was positioned_out_of_parent): uses offsetParent
(transform-aware, skips fixed-as-canvas), exempts fully-detached
callouts within an attachment allowance while still flagging
touching-but-mostly-outside drift.
- panel_out_of_canvas (was box_out_of_canvas): paint alone qualifies
(flat solid panels were a false negative), fully off-canvas rects are
parked entrances and stay silent, pointer-events:none marks decorative
layers, hero-sized breaches warn while small bleeds stay info.
- connector_detached: endpoints via getPointAtLength + getScreenCTM
(viewBox, preserveAspectRatio, group transforms, every command type),
defs/marker/clipPath subtrees skipped, word-boundary connector naming,
containment tier limited to opaque non-ancestor targets (a text-bearing
wrapper contains its own diagram's endpoints).
- canvas_overflow promotion requires partial visibility — a fully
off-canvas rect is a parked entrance, not drift.
Verified: the four incident compositions still surface their drift as
held warnings; the review's false-positive repros (fixed HUD, callout,
parked entrance, corner bleed, marker arrowheads, g-transform and
viewBox-scaled connectors) are clean at warning level. Docs and the CLI
skill reference now describe the coordinate-frame findings.
* fix(cli): panel ownership is geometric — direct-text panels were a silent false negative
A painted panel whose direct text stays in-bounds while its box breaches
the canvas produced neither finding: canvas_overflow measures the text
range and panel_out_of_canvas skipped every own-text element. Skip the
panel finding only when the element's own text ALSO breaches (that
geometry belongs to canvas_overflow); pin the message/fixHint wording of
all three findings with positive assertions; document the SVG-internal
anchor blind spot.
* fix(cli): classify panel decoration by paint kind, not pointer-events
pointer-events:none exempted the framed-painting incident's gold frame
layers — hero content that happens to disable hit-testing. Decoration is
now gradient-only paint (spotlights, textures, vignettes); url() images,
solid fills and borders are content regardless of pointer-events.
* fix(cli): add fixHint to the test-local AuditIssue shape
* fix(cli): gradient stops decide content vs decoration; ownership matches canvas_overflow's tolerance
A gradient with any solid stop (alpha >= 0.6) is content — heroes and
cards painted with linear-gradient were invisible under the blanket
gradient exemption; all-translucent stops (spotlights, vignettes) stay
decoration. The text-ownership check now uses the audit tolerance that
canvas_overflow itself fires at, making the contract strict-mutex: any
text breach past that tolerance cedes the element, so a shallow 20px
text breach no longer double-reports.
Address Miguel's R1 blocker + Rames/Miga's testability nit:
- The `-frames:v 3` decode samples AT MOST 3 frames; a legitimate 1- or
2-frame WebM (256 or 512 bytes) was returned as `undefined` (probe
failure), silently skipping the advisory even when every available
pixel was opaque. Accept any positive whole-frame byte count ≤ 768
(multiples of 256), distinguishing successful short-EOF from partial/
malformed decode.
- Export `sampledAlphaIsFullyOpaque` and add 11 direct tests covering:
3/2/1-frame opaque decodes → true; transparent pixel at pos 0 or
final byte → false (guards the alpha-byte stride); non-frame-multiple
/ over-3-frame / zero byte counts → undefined; execFileSync throw →
undefined; findFFmpeg missing → undefined; and one args-shape guard
pinning the load-bearing `-c:v libvpx-vp9` before `-i` (without which
the default decoder silently discards VP9 alpha and the whole check
would false-positive on genuinely-transparent WebMs).
- Update advisory wording from "3 sampled decoded frames" to "every
sampled decoded pixel" so the message is honest for short WebMs.
18/18 tests pass locally under `vitest run`.
Review feedback on #2358: the batch-miss RENDER_FAILED in
runAssetImportMany (asset.ts) throws the same typed error as
client.ts's single-node renderNode, but wasn't labeled — so
cli_error.endpoint would silently come back undefined for the
flow that most heavily exercises /v1/images.
cli_error had no way to tell which figma REST call (images, files_nodes,
variables_local, styles, ...) actually hit RATE_LIMITED/FORBIDDEN/etc, so
the dashboard could see failures spike but not which call caused them.
FigmaClientError now carries a low-cardinality endpoint label (never the
raw fileKey/nodeId), threaded through to cli_error's endpoint property.
Extends webmAlphaCheck.ts (from #2044) with a pixel-level decode probe.
After the tag check passes, decodes 3 sampled frames via
`ffmpeg -c:v libvpx-vp9 -pix_fmt rgba -f rawvideo` at 8x8 and emits a
distinct advisory if every alpha byte reads 255.
#2044 detects the "tag absent" failure mode (ffprobe shows no
`alpha_mode` in stream tags). It doesn't catch a stricter case reported
on CLI 0.7.56 / Windows 11: ALPHA_MODE=1 present but BlockAdditional
alpha side data empty. Under current logic webmAlphaAdvisory sees
`alphaMode: true` and stays silent, so the render ships as opaque
without any signal.
The -metadata:s:v:0 alpha_mode=1 push is a muxer directive that some
ffmpeg builds write unconditionally, independent of whether libvpx-vp9
emitted the alpha plane. Tag presence is necessary but not sufficient
evidence of preserved alpha.
Advisory text names both possibilities (opaque composition OR silent
alpha drop) plus the concrete workaround (png-sequence + prores repack).
Fast path (no tag or missing tag) is unchanged. Probe adds ~1s per
WebM render only when the tag says alpha.
Adopt the fake-timer pattern the sibling "recovers when a crashed reclaimer
leaves both lock directories" test in the same file already uses. Without
fake timers, if the dynamic `import("./manager.js")` beat between
`installFsMocks({ initialMtimeMs: Date.now() })` and the `withInstallLock`
call exceeds `staleMs` (50 ms on a busy shared runner with `vi.resetModules()`
per beforeEach), the new immediate-stale short-circuit added in #2328 fires
on iteration 1, breaks out before `waitedMs` reaches `waitNoticeMs=20`, and
no "Waiting for another hyperframes process" warn ever emits — the assertion
at `manager.test.ts:428` (`expected false to be true`) then fails.
Under fake timers, `Date.now()` is frozen at the mtime seed, so the lock
stays non-stale across the dynamic import and the wait-notice branch
observes real polling; `vi.advanceTimersByTimeAsync(staleMs + pollMs * 5)`
then drives the loop past both the wait-notice threshold and the stale
deadline so the reclaim + acquisition still resolves.
Test-only change; no production-code diff. Verified 27/27 in
`packages/cli/src/browser/manager.test.ts` under `vitest run`.
* fix(cli): three occlusion-probe false-positive sources in text_occluded
- pointer-events:none text is invisible to elementFromPoint, so the probe
always hit whatever paints beneath and misread visible text as buried;
restore hit-testing on the element for the duration of the probe
- a backgroundImage counted as opaque regardless of alpha, so a 4%-alpha
grid/scrim gradient qualified as an occluder; gradients now occlude only
when their colours reach alpha > 0.6 (url() images unchanged)
- a visible container whose every text-bearing descendant is still at
opacity 0 (entrance not started) was probed anyway; skip when no text
ink is on screen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cli): address review — document-wide hit-testing restore, gradient compositing, whitespace ink
- restore hit-testing for ALL pointer-events:none elements during the text
audit pass (not just the probed text): an occluder that itself carries
pointer-events:none is invisible to elementFromPoint, which made truly
buried text read as clean once the text alone became hittable
- hasVisibleTextInk ignores whitespace-only text nodes (indented markup
defeated the gate) and uses a 0.05 floor so mid-fade text keeps its
persistence occurrences
- hasOpaqueBackground composites gradient alpha with background-color
(two 0.5-alpha layers paint at ~0.75); gradientMaxAlpha returns opaque
for any colour function it cannot score (oklch/lab/...); percentage
alpha values now parse as fractions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cli): walk the elementsFromPoint stack in occluderAt
A transparent layer that becomes hittable (pointer-events restored) must
not mask an opaque occluder painting beneath it — single-point
elementFromPoint returned the transparent top and dropped two genuinely
buried cases in the census acceptance run; the stack walk keeps 10/10.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cli): composite stacked background-image layers when judging occluder opacity
Two 0.5-alpha gradient layers paint at 0.75 combined; taking the max
color-stop alpha across the whole declaration under-counted them and
suppressed real text_occluded findings. Split layers at top-level commas
(paren-aware), score each, composite as 1-prod(1-a_i). Also pins the
0.05 text-ink floor with a boundary test (review feedback on #2357).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cli): keep walking the occlusion stack past pair-specific exemptions
sharedPreserve3d and isCrossSceneTransitionOverlap excuse one hit, not
the whole probe; returning null let a transparent decorative layer in
the text's 3D context mask a real occluder below it (review feedback).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`hyperframes lambda deploy` runs `sam deploy --resolve-s3`, and SAM's
managed artifacts bucket (aws-sam-cli-managed-default) is created with
default SSE encryption. Setting that requires s3:PutEncryptionConfiguration,
which the generated deploy policy did not grant, so a first deploy by a
user provisioned exactly per `lambda policies user` 403s on the bucket and
the managed stack rolls back.
Add s3:GetEncryptionConfiguration and s3:PutEncryptionConfiguration to the
s3Bucket action set (Get pairs with Put for CloudFormation update/drift
reads, matching the existing Get/Put pairs in the list). Also add a hint to
the sam-deploy failure path pointing at the ROLLBACK_COMPLETE recovery step,
since first-time users hit the stuck-rollback error on their retry.
Fixes#2137
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review on #2266: the gradient-text exclusion was too broad — any
background-clip:text skipped the invisible-text check, so a broken/missing
gradient (clip:text with no image and a transparent background, which paints
nothing) went unreported. Now exclude only when a real background fills the
glyphs (background-image != none, or an opaque background-color). Expands the
test suite to the reviewer's full case set: direct transparent fill, inherited
transparent fill over an opaque child color, color:transparent fallback, opaque
baseline, gradient-over-real-background exclusion, broken-gradient still flagged,
and empty-text no-op.