import.meta.env is undefined in Next.js Turbopack/Webpack, causing
"Cannot read properties of undefined" when the studio telemetry client
loads. Wrap accesses in try-catch so they gracefully fall back.
Also hardcode the PostHog API key and host — they're public write-only
values with no reason to be overridable via env.
When the distributed render path stitches chunks with `-c copy`,
ffmpeg averages the container framerate from PTS rather than
carrying the source's exact rational rate, producing values like
`360000/12001` instead of `30/1` and ~5ms duration drift over
60s.
This is a known ffmpeg behavior at the concat-demuxer-copy
boundary. The industry-standard fix is `-r <fps>` as an input
flag on the concat step plus an output flag on the subsequent
mux step — both with `-c copy` retained, no re-encode required.
Three sites updated:
- `assemble.ts` concat step: `-r <fps>` input flag.
- `chunkEncoder.muxVideoWithAudio`: `-r <fps>` output flag.
- `chunkEncoder.applyFaststart`: same, threaded from caller.
Adds `r_frame_rate` + duration-equivalence assertions to
`assemble.test.ts` to close the regression hole.
Remaining review follow-ups:
- killProcessTree now escalates to SIGKILL after 500ms if SIGTERM
doesn't kill the process (same pattern as killTrackedProcesses).
Covers orphan cleanup and dev/local mode tree kill.
- Added unit tests for both new modules:
- processTracker.test.ts (6 tests): track/remove on exit/error,
kill running processes, SIGKILL escalation for SIGTERM-resistant
processes, idempotency.
- orphanCleanup.test.ts (5 tests): tree kill with children,
SIGKILL escalation, non-existent PID handling, orphan detection
returns 0 when clean.
- Blocker: arm 3s force-exit timer BEFORE awaiting cleanup, not
inside .finally(). Prevents hang if drainBrowserPool() blocks on
dead Chrome.
- Reorder cleanup: killTrackedProcesses() (sync, fast) runs first,
then async browser drain. Ffmpeg dies immediately instead of
surviving if the hard timer fires early.
- SIGKILL escalation: processTracker now SIGTERMs all tracked
processes, then SIGKILLs survivors after 500ms grace period.
- Scope pgrep to current user (pgrep -u $(id -u)) so orphan
detection doesn't touch other users' Chrome on shared machines.
- Add process.on('exit') handler for crash paths (unhandled
exceptions/rejections that bypass signal handlers).
- Document Windows no-op behavior on killProcessTree handlers.
FFmpeg's VFR-to-CFR normalization produces slightly different frame
counts across versions due to timestamp rounding in the fps filter.
The ±1 tolerance was too tight for Linux FFmpeg builds. Widen to ±3
frames — still catches the 25% shortfall regression these tests
guard against.
The preview command's shutdown handler only closed the HTTP server,
leaving Chrome (browser pool) and ffmpeg processes alive. This caused
silent resource leaks — orphaned processes consuming CPU and RAM with
no parent.
Root cause: preview.ts never called drainBrowserPool() or killed
tracked ffmpeg processes. The thumbnail browser in studioServer.ts
registered its own competing signal handlers that raced with
preview's shutdown.
Fix:
- Add a central process tracker (processTracker.ts) that registers
every spawned ffmpeg across engine and producer packages
- Centralize thumbnail browser cleanup via exported
closeThumbnailBrowser() instead of scattered signal handlers
- Wire preview shutdown to call closeThumbnailBrowser(),
drainBrowserPool(), and killTrackedProcesses() before closing the
HTTP server (embedded mode)
- Add killProcessTree() for dev/local modes where Chrome runs in a
child process tree
- Add startup orphan detection that finds and kills orphaned
chrome-headless-shell/Puppeteer Chrome processes (PPID=1) from
previously crashed sessions
Closes#1038
Split PlayerControls.tsx into focused sub-components (SeekBar,
WorkAreaOverlay, MuteButton, LoopButton, FullscreenButton,
ShortcutsPanel, SpeedMenu) and extracted seek bar drag/progress
tracking into useSeekBarDrag hook.
Split manualEditsDom.ts patch-builder functions into
manualEditsDomPatches.ts with data-driven helpers to reduce
duplication and complexity.
Extracted per-type reapply helpers from reapplyPositionEditsAfterSeek
and factored out identity-matrix check from
stripGsapTranslateFromTransform.
Raised file-size limit from 500 to 600 lines, removed
.filesize-allowlist.
The #f6f6f4 off-white background was showing through the 30px grid
gaps and behind translated cards during the zoom/unzoom transitions,
creating visible white strips in the rendered video. Changed to
#1a1a20 (near-black) so gaps read as intentional dark separators.
Also added a .zoom-backdrop div behind the grid in the zoom demo that
fades to the focus card's gold gradient during the transition, covering
any gaps left by sibling cards translating off-screen.
Co-authored-by: Kanyini <onebenson@gmail.com>
The renderer's Page.captureScreenshot can introduce a 1-2px offset
at viewport boundaries due to compositor subpixel rounding. Grid rows
are now 341px (3×341 + 2×30 = 1083), overflowing the 1080 viewport
by 3px. The overflow is clipped by overflow: hidden, but ensures any
capture offset still sees grid content rather than the body background.
Also removed flex centering from .demo-canvas — unnecessary now that
the grid fills the viewport, and it was a source of non-deterministic
positioning under multi-worker rendering.
Co-authored-by: Kanyini <onebenson@gmail.com>
Cards 360×340 with 30px gap = exactly 1920×1080. No body background
visible at any frame. Focus scale drops from 9 to 6 (360×6 = 2160,
still overshoots viewport by 240px).
Co-authored-by: Kanyini <onebenson@gmail.com>
Audit of every detection rule in the registry against actual vendor
source code. Rules that lacked a public-source citation were guesses
and have been removed; surviving rules now all cite the file + line
that emits the marker.
Codex — replace per @magi's investigation:
- Drop CODEX_HOME (config override read at startup, NOT propagated to
child processes — would miss most Codex invocations).
- Drop CODEX_SANDBOX (macOS Seatbelt only; covered by the others).
- Add CODEX_THREAD_ID (set unconditionally on every spawned shell
command — codex-rs/protocol/src/shell_environment.rs:6 +
codex-rs/core/src/unified_exec/process_manager.rs:1010).
- Add CODEX_CI (hardcoded in UNIFIED_EXEC_ENV — process_manager.rs:70).
- Keep CODEX_SANDBOX_NETWORK_DISABLED (default-on sandbox marker —
codex-rs/core/src/sandboxing/mod.rs:135-138).
Cursor — drop unverified CURSOR_TRACE_ID and CURSOR_AGENT guesses.
Keep TERM_PROGRAM=cursor (set by Cursor's integrated terminal).
Pi — new rule. https://github.com/earendil-works/pi
packages/coding-agent/src/cli.ts:13 unconditionally executes
process.env.PI_CODING_AGENT = "true";
at module entry, so every subprocess Pi spawns sees this marker.
Same propagation pattern as Hermes.
Removed (no source-cited marker found in this audit):
- aider — verified Aider sets no AIDER_* env vars; only OR_SITE_URL and
OR_APP_NAME (OpenRouter integration). No reliable marker.
- gemini_cli — GEMINI_SANDBOX/GEMINI_CLI_TRUST_WORKSPACE are conditional
on CLI flags; no unconditional marker found.
- jules, devin — closed source, no public marker documentation.
These vendors can be re-added later with a source citation; absence
in the registry will silently false-negative (events land in the null
bucket), but won't false-positive on other vendors.
Per @james-russo's review: do source-level research before shipping
detection rules. Memory updated to enforce this for future work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three follow-ups from @miguel-heygen's review:
1. HERMES_QUIET — switch to existence check.
`env["HERMES_QUIET"] === "1"` was brittle vs. future Hermes changes
(e.g. if cli.py ever sets it to "true"). The var name itself is
specific enough that existence is the right signal.
2. CI_PROVIDERS — convert to a discriminated union.
`mode: "truthy" | "presence"` is stricter than the previous pair of
optional boolean flags (which allowed entries with neither set).
3. Sandbox detection tests — add coverage.
- Docker positive: /.dockerenv present → docker.
- Negative case: plain Linux laptop with no markers → null.
Together with the gVisor 4.4.0 fix in the previous commit, that addresses
all three actionable callouts (the discriminated-union nit was non-blocking
but worth doing while in the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses PR feedback from @magi: kernel string `4.4.0` is also the
Ubuntu 16.04 LTS / older-real-kernel version, so accepting it alone
false-positives. Now `4.4.0` only counts as gVisor when /proc/version
also contains "gVisor". `*-gvisor` kernel strings remain standalone-
sufficient since no real production kernel reports them.
Adds a regression test that an Ubuntu 16.04 box reporting
`Linux version 4.4.0-1128-aws (buildd@lcy01)` is NOT classified as
a gVisor sandbox.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two new properties to every CLI telemetry event so we can tell
managed-sandbox traffic (Codex Cloud, Claude Code Web, etc.) apart from
real developer laptops without geolocation guesswork:
- sandbox_runtime: 'gvisor' | 'firecracker' | 'docker' | 'kvm' | 'wsl' | null
gVisor detected via kernel string ('4.19.0-gvisor' or legacy Sentry
'4.4.0') + /proc/version. Firecracker via /dev/vsock + DMI sys_vendor.
Docker reuses the existing /.dockerenv + cgroup probe.
- agent_runtime: claude_code | codex | cursor | copilot_agent | jules
| replit | devin | aider | gemini_cli | hermes | openclaw | null
Detected by the EXISTENCE of well-known vendor env vars only — values
are never read. Hermes rule keys on HERMES_QUIET=1 (set unconditionally
at hermes-agent/cli.py:50). openclaw rule keys on OPENCLAW_STATE_DIR
or OPENCLAW_CONFIG_PATH (set explicitly in the spawned child env at
openclaw/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts).
Drive-by cleanups required by fallow because system.ts and client.ts
fall into the audit scope of this PR:
- Extract detectWSL into platform.ts to break the system.ts ↔ agent_runtime.ts cycle.
- Refactor detectCI / getCIName into a single CI_PROVIDERS table.
- Dedupe flush / flushSync via a shared drainQueueToPayload helper.
Privacy posture unchanged: HYPERFRAMES_NO_TELEMETRY=1 still opts out;
disclosure in docs/packages/cli.mdx updated to enumerate the new fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Increase --focus-scale from 8 to 9 — at scale 8 the card matched
the viewport width exactly (240×8=1920), leaving zero tolerance for
subpixel rounding. Scale 9 overshoots by 240px on each axis.
- Reduce grid gap from 40px to 24px — the 1040px grid in a 1080px
viewport left only 20px of padding, causing bottom-row cards and
their box-shadows to clip at the frame edge.
- Remove transform-style: preserve-3d from both snippets — no 3D
transforms are used, and preserve-3d can cause compositing artifacts
with overflow: hidden ancestors.
- Unzoom: set --pu-sibling-fade to 0 and drive sibling opacity via
GSAP (starting at t=2.0s). Previously, CSS-driven opacity made cards
70% visible when they first peeked into the viewport, creating
colored strips at the frame edge during the reveal.
Co-authored-by: Kanyini <onebenson@gmail.com>
Two companion components inspired by the eBay Playbook hero transition:
- parallax-zoom: center card scales up to fill the frame while siblings
parallax outward. Single CSS variable (--pz-progress 0→1), fully
seekable and deterministic.
- parallax-unzoom: the reverse — focus card starts at full-frame scale
and shrinks back into its grid position while siblings parallax inward.
Uses --pu-progress with the pu prefix to avoid variable collisions when
both components live in the same composition.
Designed to chain: zoom INTO a card in scene 1, unzoom OUT of it in
scene 2 to reveal a fresh grid underneath.
Includes demo compositions, registry manifests, and catalog pages.
Preview assets rendered and uploaded to CDN.
Co-authored-by: Kanyini <onebenson@gmail.com>
Repeated Sandbox.Timedout chunks can leave @sparticuz/chromium
returning a falsy/empty path on subsequent invocations — warm
instances on the same execution environment never re-extract
chromium. The downstream puppeteer-core assertion about needing an
executablePath or channel buries the actionable cause; a cost-
analysis sweep took ~30 min to root-cause from that trace.
Guard the resolver: if mod.executablePath() returns a non-string,
empty string, or a path that does not exist on disk, throw a typed
ChromeBinaryUnavailableError whose message points at the recycle
remedy (env-var bump or redeploy). Add the error name to the three
NON_RETRYABLE lists so SFN short-circuits instead of burning four
15-min retries on a function that won't recover.
Same typed-error contract for the chrome-headless-shell fallback so
both sources fail consistently. Tests pin the wedge path (empty
string + non-existent file) and the carried metadata (source +
resolvedPath).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`--width 3840 --height 2160` against a composition with
`data-width="1920"` silently produces a 1080p output because the
runtime lays out the page at the composition's authored dimensions —
real footgun we hit during a cost-analysis sweep. Warn early and point
at `--output-resolution` (the supersampling escape hatch) so the user
doesn't burn a 30-minute render learning the override rule.
Skipped when `--output-resolution` is set (the supported supersampling
path — the user is opting in), when `--json` is set (machine consumers),
or when `index.html` isn't on disk (typical with `--site-id`).
Helper lives in a shared module so render + render-batch agree on the
parse + message. Tests cover both attribute orders, single/double
quotes, the silent paths, and the warning path. Best-effort regex over
the canonical attr shape — malformed HTML falls through to no warning
rather than blocking the render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CDK construct compiles tasks.LambdaInvoke to the optimized
arn:aws:states:::lambda:invoke integration, which emits Task* history
events with the Lambda response wrapped in .Payload. getRenderProgress
was only listening for the older LambdaFunction* events, so every CDK-
deployed stack reported $0 total cost and zero invocations on success
— a high-visibility regression that only surfaced when we manually
walked SFN history during a cost-analysis sweep.
Add cases for TaskScheduled (count invocation), TaskSucceeded (parse
Payload + accumulate billed duration / frame counts), and TaskFailed
(record error). Keep the LambdaFunction* paths so anyone wiring the
raw lambda:invokeFunction.sync task type still works. Factor out the
shared FramesEncoded-attribution logic so both branches agree on the
"only RenderChunk frames count" rule.
Tests pin a real-shape regression: replay the inspector-launch
1080p/30fps history (1 Plan + 16 RenderChunks + 1 Assemble) and assert
lambdaUsd lands at ~$0.582 — matching the cost-analysis script's
direct read against SFN history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vai (vanceingalls) caught a 10× tolerance mismatch between the script
and the prose. Rames confirmed as blocking:
step-5-build.md:458 (per-beat evidence rule): ±0.05s
step-6-validate.md (playback verification): ±0.1s
w2h-verify.mjs:29 (SFX_DRIFT_TOLERANCE_S): 0.5s
So an agent writing per-beat evidence at ±0.05s reports a 0.3s drift
as FAIL, while the script reports the same drift as PASS. The pasted-
verbatim report contradicts the agent's evidence block — exactly the
kind of internal contradiction this PR was built to eliminate.
Converged on ±0.1s everywhere:
- w2h-verify.mjs:29: SFX_DRIFT_TOLERANCE_S = 0.1 (3 frames at 30fps)
- step-5-build.md:458: ±0.05s → ±0.1s, with cross-reference noting it
matches the script + step-6 playback floor
The other ±0.5s constants in step-6 are for total audio/video duration
and storyboard beat-range matching — those are coarser-grained timing
checks (not SFX-to-visual sync). Left as-is intentionally.
Regression check: huly-v3 now flags 4 SFX drifts instead of 3 — the
new one is glitch-1.mp3 at 0.20s drift (6 frames). The old 0.5s
tolerance was masking this real timing issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allows authored-at-1080p compositions to render at 4K/2K via Chrome
deviceScaleFactor supersampling without re-laying-out the composition.
Plain --width 3840 silently lays out at 1920×1080 because data-width/
data-height attrs override Config.width — this flag is the supported
way to ask the renderer to supersample.
Accepts canonical CanvasResolution names (landscape, landscape-4k,
portrait, portrait-4k, square, square-4k) and aliases (1080p, 4k, uhd,
hd, 1080p-portrait, 4k-portrait, 1080p-square, 4k-square). Wired
through render + render-batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues from Miguel's + Rames's reviews:
**[Blocking] find / violates CLAUDE.md guidance (Miguel)**
CLAUDE.md says: "When running find, search from . (or a specific path),
not / — scanning the full filesystem can exhaust system resources on
large trees." I introduced 3 instances of `find /` in skill prose to
help sub-agents locate skill files from unknown CWDs. Replaced all 3
with `find "$HOME" ... -maxdepth 10`. Verified all 4 skill files
resolve correctly under $HOME on the testbed setup.
Files: step-3-storyboard.md (×2), step-5-build.md, step-6-validate.md.
**[Blocking] SFX audio regex assumed attribute ordering (Miguel + Rames)**
The v2 audioRegex required src= to appear lexically BEFORE data-start=
in the same <audio> tag. But capabilities.md:365 — in the same skill —
documents the canonical pattern with src= LAST:
<audio id="..." data-start="..." data-duration="..." data-volume="..."
data-track-index="..." src="...">
Real compositions following the docs would have audio tags that don't
match the regex → SFX reported as MISSING → false FAIL in the script
output → false alarm in the user-facing summary. Exactly what v2 was
supposed to fix.
Replaced with the same two-step shape that readBeatDurationsFromIndex
already uses correctly: match `<audio[^>]*?>` to grab the whole tag,
then extract src= and data-start= from the tag string with independent
regexes. Verified both attribute orderings (src first, src last) now
work via inline node test.
**[Minor] readBeatCompositions / readBeatDurationsFromIndex re-read on
every call (Rames)**
Added process-scoped caches to both helpers. The script is a one-shot
CLI so no invalidation needed — first call hits disk, subsequent calls
return the cached result. readBeatCompositions was called 3×,
readBeatDurationsFromIndex 2× — now 1× each.
**Regression checks**
- huly-v3: 4 PASS · 3 FAIL · 1 INFO (unchanged — same 3 real issues
flagged: 48px wordmark, missing shaders, 3 SFX drifts)
- huly-launch-v4: 6 PASS · 0 FAIL · 2 INFO (unchanged)
- Lint + format: clean
2 files changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rational `Fps = { num, den }` refactor in 5dcc89c broke callers
passing `fps: 30` (the form documented in every code example and used
by external consumers). FFmpeg received `undefined/undefined` as the
framerate, causing a cryptic exit-code error.
Add `FpsInput = number | Fps` and `toFps()` normalizer in
@hyperframes/core. `createRenderJob` now accepts both forms —
plain integers are promoted to `{ num, den: 1 }` at the boundary;
`RenderConfig.fps` stays strict `Fps` internally so no downstream
code changes.
Also fixes the producer and engine docs, which showed phantom
`input`/`output` fields on `createRenderJob` and a wrong
`executeRenderJob(job)` signature (missing `projectDir`/`outputPath`
args).
Closes#1031
Two fixes for the 3M+ unhandled_promise_rejection events/day spike:
1. Filter: suppress "Error fetching ... 404" rejections from composition
code — these are asset-not-found content errors, not Studio bugs.
2. Rate-limit: cap both error and rejection telemetry at 50 per session.
After the cap, emit a single *_cap_reached event so we know capping
occurred without generating unlimited events.
3. Root cause: webAudioTransport now checks response.ok before decode
and caches failed URLs in _failedSrcs so repeat ticks don't re-fetch
the same 404 on every playback frame.
Also add playground/ to fallow ignorePatterns — local experiment
directory was tripping the audit gate.
A fresh agent session ran the v1 verify script and the disclosure pasted
into their final summary showed 3 FAIL rows for things that weren't
actually defects:
Headline font-size: flagged Beat 2 (wordmark SVG), Beat 3 (UI grid),
Beat 5 (terminal). None of these legitimately have text headlines.
Timeline coverage: flagged 5/6 beats because the script's regex only
saw `tl.X(..., 2.5)` literal positions and missed forEach loops,
variable-position tweens, and long-duration scaler tweens.
Beat durations: flagged 2 beats because my "duration X.Xs near beat
label" fallback false-matched non-beat durations
(e.g., "shader runs — duration 0.7s" near a "Beat 1" mention).
The agent had to write ~5 paragraphs defensively justifying each false
FAIL. That's friction we can fix.
Tested against the agent's actual project (huly-launch-v4): went from
4 FAIL (3 false positives + 1 real bare-table parser miss) to 0 FAIL.
Also re-verified huly-v3 still correctly catches its 3 real issues
(48px wordmark, missing shaders, 3 SFX drifts) — no regression.
**Brand visuals check**
Switched from "≥30% asset usage" (gameable, rewards quantity over quality)
to "at least 1 beat references a captured hero/image/svg" — quality
signal that's cheap to satisfy when real, hard to fake. Excludes fonts,
logos, favicons, contact-sheets.
**Headline check**
Now only flags beats where the LARGEST font-size is in the 40–<80px
range — the "aspiring headline but too small" zone. Below 40px = beat
has no text headline by design (terminal, UI labels, SVG-only); skip.
≥80px = proper headline; pass. Eliminates the false positives on
SVG-dominated and UI-grid beats while still catching the real "headline
too small" failure (Beat 4 at 72px in this run; Beat 1 wordmark at 48px
in another).
**Timeline coverage check**
Three improvements:
1. Detects forEach loops + for-loops containing tl.X() calls — beats
with these have events at positions the static parser can't read;
mark as INFO-skipped rather than failed.
2. Detects long-duration tweens — if a single tween's duration covers
≥70% of the beat duration (camera dolly, breathing animation), the
beat has full coverage via persistent motion; skip the position check.
3. New paren-balanced parser for extracting tl.X() position arguments —
the v1 regex was matching `rgba(86,131,218,0.35)` and capturing 0.35
as a tween position. The new parser walks paren depth and only
captures top-level trailing numeric args. No more rgba false matches.
**Shader transitions check**
Two fixes:
1. Filter out inventory lines — lines listing 3+ shader names are
"what's available," not "what's planned for use." Real use
mentions one or two shaders per line.
2. Apply the same SFX-context exclusion to the declared side that the
present-check side already had — "glitch" inside `sfx/glitch-1.mp3`
no longer counts as a declared shader transition.
For huly-v3: was 6 declared (1 phantom from inventory + 5 + glitch
from SFX), now 2 declared (light-leak, cinematic-zoom) — matches the
storyboard's actual plan.
**Beat duration check**
1. Dropped the "duration X.Xs within 200 chars of beat label" fallback
— too loose; matched shader durations, animation durations, anything
labeled "duration". This was the source of the 0.70s misread in the
debrief.
2. Added a bare-number timing-table parser for the format
`| 1 | 0.00s | 5.20s | 5.20s | ... |` (with optional `>` blockquote
prefix). Computes duration = end - start.
3. Added a negative lookahead so `\bB3\b` doesn't false-match "B3.1"
sub-beats and grab the wrong row.
4. Filter buildBeatIds to only numbered beats — skips the root
composition (`data-composition-id="main"`) so it doesn't inflate
"parseable" count.
**Brand visuals + asset count**
Excluded fonts/ subdirectory (always-used via @font-face → would
always pass) and contact-sheet-*.jpg (pipeline outputs, not website
inputs). Both inflated the denominator and weakened the signal.
**Edge case fixes**
- Removed `basename` unused import (oxlint).
- Fixed shader-name substring overlap: longest-name-first matching so
"cross-warp-morph" doesn't double-count as "cross-warp".
- SFX timestamps now collect ALL audio tags per file (multi-timestamp
SFX like click×3); picks closest index timestamp to each storyboard
timestamp instead of just keeping the last.
**Step 6 doc**
Updated the skill's "w2h-verify — the source of truth" section to
describe the new checks accurately and what failure mode each catches.
2 files changed, +486/-109. Format + lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flip the fallback from false to true so the blocks panel is on for
everyone out of the box. Users can still disable it via
VITE_STUDIO_ENABLE_BLOCKS_PANEL=false if needed.
Three rounds of text-based enforcement plateaued. A third agent debrief
showed the same failures: 9% asset usage (vs ≥30% floor), shader
transitions declared in STORYBOARD.md but not in shipping index.html,
SFX timestamps drifted up to 12.4s, animation-map skipped, MP4 not
rendered, honest-disclosure section omitted from final summary.
The pattern is clear: language-only enforcement is selectively
interpretable by the agent under ship pressure. Move enforcement into
tooling — facts the agent can't fudge.
**New script: `skills/website-to-hyperframes/scripts/w2h-verify.mjs`**
Pure file-analysis script (no shell spawns). Computes six checks and
prints a PASS/FAIL/INFO table:
1. Asset usage — assets referenced in compositions ÷ assets captured;
target ≥30%. Tested against videos/huly-v3: caught 6/74 = 8% FAIL.
2. Shader transitions consistency — STORYBOARD.md-declared shaders vs
index.html. Longest-name matching to avoid substring false positives
(cross-warp-morph not double-counted as cross-warp). Tested: caught
6 declared / 1 present / 5 missing.
3. SFX timestamp drift — parses STORYBOARD.md table rows for
`sfx/X.mp3` + time-with-`s`, parses index.html <audio data-start>,
flags drift >0.5s. Tested: caught 12.4s drift on click.mp3 that
the agent debrief didn't even mention.
4. animation-map.json existence — explicit file check.
5. Rendered MP4 existence — scans project root, output/, renders/.
6. Required artifacts — STORYBOARD.md, DESIGN.md, SCRIPT.md, index.html
all present.
Exit code: 0 (all pass) or 1 (one or more fail). The script's output
becomes the Step 6 deliverable — paste verbatim into the user-facing
summary.
**Skill update: `step-6-validate.md`**
Adds `w2h-verify report` to the DoD checklist with the rule: paste the
FULL output verbatim into the final summary. Cherry-picking rows,
substituting adjectives for percentages, or omitting FAIL lines is
explicitly forbidden. If a row says FAIL, either fix it and re-run
until PASS or include the FAIL line verbatim in "What I did NOT
verify" with a one-sentence reason.
Test run against the project that prompted this:
```
SUMMARY: 1 PASS · 4 FAIL · 1 INFO
- Asset usage: FAIL 6/74 (8%) target ≥30%
- Shader transitions: FAIL 6 declared, 1 present, 5 missing
- SFX timestamps: FAIL 3 drifted >0.5s (max 12.4s)
- animation-map.json: FAIL missing
- Rendered MP4: INFO no .mp4 found
- Required artifacts: PASS
```
The agent could selectively ignore "the WCAG warnings are false
positives." The agent cannot selectively ignore a line that says
`6/74 (8%) — target ≥30%`.
2 files changed (+ 1 new script, ~330 lines). Format checks pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`injectInterceptor` used `String.prototype.replace(target, replacement)`
to inject the runtime `<script>` before `</head>`. The replacement
string is a substitution template — `$&` expands to the matched
substring, and the minified runtime IIFE contains legitimate `$&`
sequences (e.g. `if(te&&$&!y.hasAttribute(...))`), so every `$&` in
the body was silently rewritten to `</head>`, producing
`Unexpected token '<'` SyntaxErrors and breaking every timeline in
the bundle.
Switch to the function-replacer form so the runtime body is passed
through verbatim. Add a regression test that diffs the bundled
runtime body against `getHyperframeRuntimeScript()` and asserts only
one `</head>` survives in the document — the test exercises the
`<head>`-present injection path (the only branch that uses the
substitution template; the no-`<head>` fallback uses slice+concat
and was unaffected).
Only the bundler is affected — `producer/fileServer.ts` already uses
the function form via `injectScriptsIntoHtml` in
`htmlDocument.ts`, so render output was correct. Snapshot, preview,
studio, layout, and validate all consume `bundleToSingleHtml` and
were broken before this fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A second agent debrief (different session, with the prior enforcement edits
applied) revealed the most damning failure yet: the agent used 1 of 65
captured assets. They wrote their own "Asset Audit" table saying SKIP for
64 hero illustrations, brand SVGs, and signature visuals. The entire point
of capture is to USE the brand's assets — that gate was bypassed entirely.
The debrief also revealed 5 other patterns the prior enforcement missed.
This commit closes all 6.
**Pattern A — Asset Audit gate (step-3-storyboard.md)** — BIGGEST FIX
Adds a non-skippable Asset Audit section in Step 3 that requires viewing
every page of capture/assets/contact-sheet-*.jpg + svgs/contact-sheet-*.jpg,
pasting 5 distinctive assets per page (with descriptions of what's actually
pictured), and choosing USE/SKIP per asset with one-sentence justification
for each SKIP. Brand-defaults floor: at least one beat MUST use a captured
hero illustration/photograph/signature diagram — not just the logo. The
forbidden list explicitly calls out: reading asset-descriptions.md alone
without opening the contact sheets, and rebuilding signature graphics in
CSS when the brand's own SVG of that graphic is in capture/assets/.
**Pattern B — Auto mode scope (SKILL.md + step-2-brief.md)**
Clarifies that auto mode covers user-PREFERENCE gates (TTS provider,
voice, beat count, captions yes/no — where the agent decides on the
user's behalf) but NOT quality-VERIFICATION gates (Asset Audit, per-beat
HTML read, DoD checklist, honest disclosure). Adds explicit test for
distinguishing: if the answer changes the content of the video, it's a
preference; if the answer is "did the verification happen?", it's a
quality gate. The agent that skipped the captions question by reasoning
"auto mode says bias toward action" was misusing auto mode.
**Pattern C — 3-path audio/motion verification (step-6-validate.md)**
Replaces the prior "Path 1 or explicit deferred" with three explicit
paths: (1) Play preview in Playwright, (2) render low-res MP4 and read
frames at ≥5fps, or (3) explicit deferred disclosure with QUANTIFIED
coverage gap ("18/900 frames = 2% coverage"). The percentage in Path 3 is
mandatory — vague "deferred to user" was the loophole. Forbidden: claiming
"confirmed via snapshot" as audio/motion evidence; 18 PNGs from a 900-
frame video is 2% coverage, not verification.
**Pattern D — Sub-agent diagnoses are hypotheses (beat-builder-guide.md)**
When a sub-agent reports "this is a linter false positive" / "this is a
known bug", that is a HYPOTHESIS from one symptom — not a verified
finding. Before propagating the workaround to other beats, main agent
must EITHER read the source to confirm OR explicitly disclose the
unverified claim. The debrief showed the main agent applied beat-2's
"linter false positive" diagnosis to beat-4 without ever reading
packages/core/src/lint/utils.ts to confirm.
**Pattern E — Re-snapshot after parallel sub-agents (step-5-build.md)**
When sub-agents run in parallel, each snapshots a project where sibling
beats may not exist yet. Their snapshots at beat boundaries or during
shader transitions show the WRONG content (typically previous beat).
Required after all complete: a canonical project-wide snapshot via the
CLI — that's what Step 6's DoD uses. Sub-agents' intermediate snapshots
are sanity checks, not the deliverable.
**Pattern F — STORYBOARD.md must be updated when divergence accepted
(beat-builder-guide.md)**
When a sub-agent diverges from spec ("the real brand mark is lowercase
'huly' not uppercase 'HULY'") AND the main agent accepts the divergence,
the main agent MUST patch STORYBOARD.md to reflect reality. Otherwise
the spec lies and the next session reading it as ground truth gets the
wrong information. Examples covered: brand mark casing, cell size at
scale, SFX timing alignment.
6 files changed, +131/-11 (net +120 lines of enforcement).
Format checks pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the audit-fix commit (e47bc6c6) landed clarity fixes, a real test run
revealed that the skill still got skipped at the gates that matter most.
The agent's honest debrief listed 9 distinct patterns where it judged
"deliver fast" over "verify what the skill said to verify."
This commit forces evidence at each of those gates so silent skips are no
longer possible without lying — at which point the gate fails by design.
**Patterns 1, 3, 8 — step-5-build.md**
- Pattern 1: "Read each beat HTML top-to-bottom" gate now requires a
structured evidence block per beat with quoted CSS hex codes, headline
font-size, captured asset paths, GSAP first/last events, and SFX trigger.
"I read it and it looks fine" / "the sub-agent confirmed" are not
acceptable. Snapshots are 3 frames out of 300+ in motion.
- Pattern 3: SFX timestamp computation rule. Every data-start MUST be
computed (beat-local + beat global start = global timestamp), not
estimated by eye. The agent typed `data-start="6.0"` for a storyboard
moment at 5.0s — a 1-second drift, not a rounding error.
- Pattern 8: Recurring sub-agent workarounds must be surfaced under
"Tooling issues encountered" — burying them means the next session
hits the same bug.
**Patterns 2, 6, 7, 9 — step-6-validate.md**
- Pattern 2: WCAG contrast warnings now require per-warning verification
with quoted validator output and opacity check at the sampled timestamp.
Blanket dismissal as "mostly transition-window false positives" is
explicitly forbidden.
- Pattern 6: animation-map.json check added to the DoD checklist —
runs `skills/hyperframes/scripts/animation-map.mjs` and confirms
per-beat event coverage.
- Pattern 7: Audio + motion verification is now a separate DoD item from
snapshot verification. Snapshots are silent stills; you must actually
play the preview and confirm SFX lands at storyboard timestamps within
±0.1s. CLI-only sessions must explicitly disclose this as deferred.
- Pattern 9: Honest disclosure section added — final user-facing summary
MUST end with "What I verified" and "What I did NOT verify" blocks.
"Looks great, ready to ship" with no disclosure now fails the gate.
**Patterns 4, 5 — beat-builder-guide.md**
- Pattern 4: Sub-agent FLAG protocol. Required phrasing for non-blocking
issues is concrete and actionable with line numbers. Forbidden phrasing:
"if X feels too long, you could...", "consider tweaking...", "might
want to...". Main agent must address each FLAG or write a rejection.
- Pattern 5: Spec ambiguity escalation. If the storyboard names a
transition without establishing the start state ("Row 1 transitions
blue → orange" but Row 1's initial color isn't specified), sub-agent
MUST flag it and ask for confirmation rather than guess. Picking an
interpretation silently means the build "looks fine" while diverging
from intent.
3 files changed, +170/-21 (net +149 lines of enforcement language).
Format checks pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>