## Summary
- Adds `process.exit(0)` after render completes in the CLI `render` command
- Fixes the process hanging indefinitely after `npx hyperframes render --output video.mp4`
- Root cause: Node.js `fetch()` keep-alive pool (from update checks and telemetry) keeps TCP connections open, preventing the event loop from draining
- Telemetry is preserved — the `exit` handler in `cli.ts` calls `flushSync()` which spawns a detached child process
## Test plan
- [x] Run `npx hyperframes render --output test.mp4` and verify the process exits after completion
- [x] Verify telemetry events are still sent (check PostHog)
## What
Added three new lint rules to the Hyperframe HTML linter to catch common runtime errors and invalid script references.
## Why
These lint rules prevent silent failures and runtime errors that can break Hyperframe compositions:
1. Timeline assignments without initialization guards cause silent failures when `window.__timelines` is undefined
2. Mismatched timeline IDs between `data-composition-id` attributes and `window.__timelines` keys prevent proper auto-nesting
3. Hallucinated script sources referencing non-existent `@hyperframe/` packages result in 404 errors
## How
Implemented three new lint rules with corresponding error codes:
- `timeline_registry_missing_init`: Detects timeline assignments without proper initialization guard using regex pattern matching
- `timeline_id_mismatch`: Cross-references composition IDs from HTML attributes against timeline registry keys to identify mismatches
- `hallucinated_script_src`: Checks script `src` attributes against known bad patterns for non-existent CDN packages
Each rule provides specific error messages and fix hints to guide developers toward correct implementations.
## Test plan
- [x] Unit tests added/updated
- [x] Manual testing performed
- [ ] Documentation updated (if applicable)
Added comprehensive test coverage for all three new lint rules, including both positive and negative test cases to ensure proper detection and avoid false positives.
## What
Enhanced timeline collection to discover GSAP-animated scene elements and persistent overlays through runtime introspection.
## Why
The existing timeline collection only captured elements with explicit timing attributes (`data-start`, `data-track-index`) or media elements, missing scene elements that are animated purely through GSAP tweens and persistent overlay elements that should appear for the full composition duration.
## How
Added two new discovery mechanisms to `collectRuntimeTimelinePayload`:
1. **GSAP Timeline Introspection**: Walks the master timeline's tweens using `getChildren()` to find animated elements, calculates absolute time ranges by traversing parent timelines, and bubbles child tween ranges up to their nearest scene-level ancestors (direct children of root with IDs).
2. **Persistent Overlay Detection**: Identifies direct children of the root composition that weren't captured by DOM queries or GSAP introspection, treating them as full-duration overlay elements while filtering out non-visual elements (script, style, meta tags) and hidden elements.
Both mechanisms respect existing track assignments and create new tracks when needed to avoid conflicts.
## Test plan
- [x] Unit tests added/updated
- [x] Manual testing performed
- [ ] Documentation updated (if applicable)
Added comprehensive test coverage for:
- GSAP-animated scene element discovery via timeline introspection
- Time range bubbling from child elements to scene ancestors
- Persistent overlay inclusion as full-duration clips
- Proper filtering of non-visual elements (script/style tags)


## What
Updated the GitHub Actions regression workflow to monitor specific package directories instead of the entire packages folder.
## Why
This change provides more granular control over when regression tests are triggered, allowing the workflow to run only when changes are made to the core, producer, or engine packages rather than any package in the repository.
## How
Modified the path filters in the regression workflow to explicitly list the three critical package directories (`packages/core/**`, `packages/producer/**`, `packages/engine/**`) instead of using the broad `packages/**` pattern.
## Test plan
How was this tested?
- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated (if applicable)
The blank template was a bare `<div>` fragment without `<!DOCTYPE>`,
`<head>`, or `<body>`. This caused:
- Blank preview (bundler/runtime can't initialize from a fragment)
- "Failed to run lint" (parsing errors on the malformed document)
All other templates (swiss-grid, vignelli, warm-grain, play-mode) are
proper HTML documents — blank was the only outlier.
Also improves lint error reporting to show the actual error message
instead of the generic "Failed to run lint."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Changes
- Add optional `entryFile` parameter to render API endpoints (`/v1/render` and `/v1/render-stream`)
- Enable rendering individual sub-compositions by extracting them from index.html context when the entry file is a `<template>` wrapper
- Change base64 audio/video linting from detecting "fabricated" media to prohibiting all inline base64 media
- Add manifest path resolution for bundled producer deployments
## API Changes
- `RenderConfig` — new optional `entryFile` field for specifying HTML file to render
- `server.ts` — parses `entryFile` from request body, validates file exists in project directory
- `executeRenderJob` — uses `entryFile` instead of hardcoded `"index.html"`
## Template Extraction
- `extractStandaloneEntryFromIndex` — extracts sub-composition hosts from index.html and creates standalone render context
- Handles `<template>` entry files by finding matching `data-composition-src` in index.html and isolating that host
- Resets `data-start` to 0 for standalone rendering
## Linting Updates
- Change rule #3.7 from detecting "fabricated" base64 media to prohibiting all inline base64 audio/video
- Lower detection threshold from 100+ to 20+ base64 characters
- All base64 media now triggers error severity with clearer messaging about file size bloat
## Usage
```json
POST /v1/render-stream
{ "projectDir": "/path/to/project", "entryFile": "compositions/intro.html" }
```
Omit `entryFile` for default behavior (renders `index.html`).
## What
When a composition has an empty GSAP timeline (no animations), `window.__hf.duration` was always 0, causing `hyperframes render` to time out after 45 seconds waiting for `duration > 0`.
## Why
The HF bridge script reads duration from `window.__player.getDuration()`, which returns the GSAP timeline duration. An empty timeline has duration 0. The render engine waits for `window.__hf.duration > 0` to confirm the runtime is ready — so compositions with no animations would always deadlock.
This hits the `--template blank` scaffold immediately: it generates an empty GSAP timeline and relies solely on `data-duration="10"` for composition timing.
## How
One-line change to the bridge script: when `getDuration()` returns 0, fall back to reading `data-duration` from the root `[data-composition-id]` element. This is the same value the static compiler already extracted — so we get a correct duration without any extra browser round-trips.
```js
get duration() {
var d = p.getDuration();
return d > 0 ? d : getDeclaredDuration(); // reads data-duration from root element
}
```
## Test plan
- [x] `hyperframes init my-video --template blank && hyperframes render my-video` completes successfully
- [x] Output: `output.mp4` — 10s, 1920×1080, 30fps ✓
- [x] Compositions with actual GSAP animations unaffected (fallback only triggers when timeline duration is 0)
- [x] Build passes, lint/format clean
- version.ts: replace hardcoded "0.1.0" with __CLI_VERSION__ injected by
tsup at build time from package.json — fixes version mismatch where
`hyperframes --version` reported 0.1.0 while package was 0.1.4
- tsup.config.ts: add define.__CLI_VERSION__ using package.json version
- render.ts: renderDocker error handler showed "Try --docker" even when
already using --docker — changed to "Check Docker is running: docker info"
- dev.ts: add missing --port arg to embedded mode; findAvailablePort now
starts from the user-supplied port instead of hardcoded 3002
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
## Summary
- copy data-start from the host sub-composition node to the inlined inner composition root
- preserve the correct runtime offset lookup for nested compositions after producer compilation
- avoid nested GSAP timelines snapping to their end state when the host starts later than t=0
## Summary
- rewrite workspace protocol dependencies to publish-safe semver ranges before the npm publish workflow runs
- keep workspace protocol references in source manifests for normal monorepo development
- ensure the published `@hyperframes/producer` manifest no longer ships unresolved `workspace:` deps
## Why
The internal repo hit a Docker build failure because the published `@hyperframes/producer` metadata still contained `workspace:^` dependencies for `@hyperframes/core` and `@hyperframes/engine`. `npm install` cannot resolve those outside the monorepo, so the published package itself was the root cause.
## Validation
- `bun install --frozen-lockfile`
- `bun run build:producer`
- `bun run prepare:publish-manifests`
- `npm pack --workspace packages/core`
- `npm pack --workspace packages/engine`
- `npm pack --workspace packages/producer`
- installed the three tarballs together in a clean temp project with `npm install --ignore-scripts`
- extracted the producer tarball and verified its `package.json` contains `^0.1.3` for `@hyperframes/core` and `@hyperframes/engine`, not `workspace:^`
## Release v0.1.3
Bumps all packages to v0.1.3.
### Changes since v0.1.2
- fix(producer): resolve manifest from sibling dist/ directory
- fix(producer): add margin reset when wrapping HTML fragments
- fix(producer): exclude test files from tsc build
- feat(cli): add opt-out anonymous telemetry via PostHog
- feat: add HyperFrames skills for AI coding tools
- fix(cli): resolve npx hyperframes from inside monorepo
- fix(ci): publish workflow fixes (remove provenance, idempotent steps, remove prepublishOnly)
After merging, the release tag is created automatically, which triggers npm publish.
## Summary
- When `index.html` is a bare fragment (just a `<div>` without `<!DOCTYPE>`), `ensureFullDocument()` wraps it in a minimal HTML document
- The wrapper was missing a CSS reset, so Chrome applied its default `body { margin: 8px }` — creating visible white lines at the top and left edges of rendered video
- Added `* { margin:0; padding:0; box-sizing:border-box }` and `body { overflow:hidden; background:#000 }` to the fragment wrapper
## Test plan
- [x] Render a composition that starts with a bare `<div>` (no `<!DOCTYPE html>`)
- [x] Verify no white lines appear at the edges of the rendered MP4
Already-published versions cause npm to return E403. With
continue-on-error, the workflow skips published packages and
continues to publish the remaining ones. Safe to re-run.
The publish workflow already runs `bun run build` before publishing.
The prepublishOnly scripts tried to run pnpm/bun which may not be
available during `npm publish`. Replace with no-op to prevent failures.
npm provenance requires a public GitHub repo. The repo is currently
set to "internal" visibility, which causes E422 on publish.
Remove --provenance until the repo is made public.
- Remove pnpm/action-setup (no pnpm-lock.yaml exists after bun migration)
- Remove cache: pnpm from setup-node (caused "lockfile not found" error)
- Use bun for install/build, npm for publish (npm comes with node)
- Pass NODE_AUTH_TOKEN per publish step
## What
Enhanced base64 media detection to identify fabricated data and fixed CSS scoping to preserve @import rules.
## Why
The linter was flagging all base64 media as prohibited, but the real issue is fabricated/fake base64 data that won't actually play. Additionally, CSS @import rules were being corrupted during composition scoping, breaking font imports and other external stylesheets.
## How
- Updated base64 media linting to detect fabricated data by checking for repetitive patterns and suspicious characteristics
- Changed error code from `base64_media_prohibited` to `fabricated_inline_media` with severity based on suspicion level
- Fixed `scopeCssToComposition()` to extract @import rules before applying selector scoping, then prepend them to the final output
- Added minimum length threshold (100 chars) for base64 detection to focus on substantial media files
## Test plan
- [x] Added regression test `css-import-scoping` to verify @import rules survive CSS scoping and render correctly
- [x] Updated linter logic to distinguish between legitimate and fabricated base64 media
- [x] Verified CSS scoping preserves @import statements while properly scoping selectors
scopeCssToComposition corrupted @import url() rules because they have
no {} block. The selector regex ([^{}@]+)\{ treated the text after @
as a selector, producing invalid CSS like:
@[data-composition-id="x"] import url('...')
This broke font loading, CSS variable resolution, and all composition
styling in rendered output.
Fix: extract @import rules before running the scoping regex, then
prepend them back unmodified.
Also adds:
- Regression test fixture (css-import-scoping)
- Common-mistakes docs: autoplay/loop, GSAP TextPlugin, sub-composition
positioning
- Format fix for hyperframeLinter.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Adds playback rate and loop support for media elements
Introduces per-element playback rate control and looping functionality for `<video>` and `<audio>` elements in the runtime media system.
### Playback Rate Control
- Reads `defaultPlaybackRate` property from media elements (set via JavaScript)
- Clamped to [0.1, 5] range for safety
- Multiplied with global transport rate during playback
- Adjusts timeline duration calculation: 10s source at 0.5x rate = 20s on timeline
- Defaults to 1.0 when not specified
### Loop Functionality
- Reads native `loop` attribute from media elements
- When enabled, wraps `relTime` using modulo of source duration
- Restarts from `mediaStart` offset when source reaches end
- Works correctly with partial media clips (respects `data-media-start`)
- Defaults to false when not specified
### Implementation Details
- Extends `RuntimeMediaClip` type with `playbackRate`, `loop`, and `sourceDuration` fields
- Updates `refreshRuntimeMediaCache` to parse new properties from DOM elements
- Modifies `syncRuntimeMedia` to apply per-element rates and handle loop wrapping
- Maintains backward compatibility with existing media clips
### Test Coverage
Adds 8 new unit tests covering:
- Playback rate parsing from DOM elements
- Rate clamping to valid ranges
- Duration adjustment calculations
- Combined per-element and global rate application
- Loop wrapping with and without `mediaStart` offsets
- Non-looping behavior verification
- data-playback-rate: per-element slow-mo/fast-forward (0.1-5x range)
Multiplied with global transport rate. Affects timeline duration
calculation when source duration is used as fallback.
- loop: native HTML loop attribute now works correctly in the runtime.
Wraps media playback from mediaStart when source reaches end.
Enables looping short clips over longer durations.
Both follow the existing data-media-start/data-volume pattern.
## Summary
- Adds `editor-agent-prod` regression test fixture for the producer
- Tests preview/render parity for compositions with sub-compositions, GSAP timelines, overlay elements, and `data-end` attributes
- Adds `styles-g` shard to regression CI workflow to run the new test
- Creates comprehensive Basel Form-styled video composition with intro/outro cards, reveals, persistent overlays, and captions
- Includes biomorphic SVG assets and Inter font integration for typographic consistency
- Tests complex animation timing with staggered entrances, crossfades, and synchronized A-roll movements
## Test plan
- [x] Regression CI passes for editor-agent-prod shard
- [x] Existing regression tests unaffected
- [x] Preview/render output matches expected visual and timing specifications
## What
Enhanced the hyperframe linter with new media validation rules and improved error detection for critical HTML issues.
## Why
The linter needed to catch more critical HTML errors that cause compositions to fail at render time, including self-closing media tags, placeholder URLs, and fabricated base64 data. Additionally, the media URL validation needed better concurrency control and more accurate error reporting.
## How
**Linter improvements:**
- Added detection for self-closing `<audio/>` and `<video/>` tags that cause rendering issues
- Added validation for placeholder media URLs ([placehold.co](http://placehold.co), [example.com](http://example.com), etc.) that return 404 errors
- Enhanced fabricated base64 media detection with severity levels (error for suspicious patterns, warning for others)
- Renamed `suspicious_global_gsap_selector` to `unscoped_gsap_selector` for clarity
- Improved error messages and fix hints to be more actionable
**Media URL validation enhancements:**
- Added concurrency control to `lintMediaUrls()` with configurable batch processing (default 15 parallel requests)
- Improved error handling and timeout management for URL accessibility checks
- Enhanced error messages to focus on URL replacement rather than tool-specific suggestions
**Runtime improvements:**
- Added null checks in picker module to prevent errors with missing DOM nodes and attributes
- Added TypeScript configuration for the runtime module with strict type checking
**Configuration cleanup:**
- Removed unused render seek configuration options from engine config
- Updated file server to support both inline scripts and external script URLs in head injection
- Fixed runtime script marker references for proper script stripping
## Test plan
- [x] Unit tests added for new linter rules (self-closing media, placeholder URLs, fabricated base64)
- [x] Test coverage for renamed linter code verification
- [x] Validation of fabricated vs non-suspicious base64 media detection
- [x] Manual testing of media URL accessibility checking with concurrency limits
- [x] Testing of picker module null safety improvements