* 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>
Amortizes per-frame CDP protocol overhead (~3.5-9ms/frame) by looping
seek -> paint-wait -> drawElementImage -> createImageBitmap in ONE
page.evaluate for runs of consecutive frames; bitmaps still post to the
encode worker per frame. Validated on 19 stratified DE comps: median
1.20x on top of worker-encode (to 1.56x), zero damaged frames, edge
comps (static-dedup-heavy, clip-cut) bit-identical; mid-batch failure
re-captures via the per-frame path (screenshot-fallback semantics
preserved). Off by default; opt in with HF_DE_BATCH=4.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(engine): drawElementImage capture service
* feat(engine): 3D projection + compositor-effect risk gate
* fix(engine): gate filter drop-shadow wherever blur gates (review)
detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only
detected blur( in its three scan paths — a drop-shadow comp stayed on the
fast path despite the gate's own correctness contract. Detect drop-shadow(
in computed styles, stylesheet rules, and GSAP tween vars, pinned by a
focused test that runs the real page-side closure against a DOM shim
(computed / stylesheet / tween coverage + blur regression + effect-free
null).
Addresses miguel-heygen's blocker on #1918.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension
# Conflicts:
# packages/engine/src/services/screenshotService.ts
* fix(engine): document HF_FORCE_DRAWELEMENT as diagnostic-only; make armStaticDedup idempotent (review)
Addresses miguel-heygen's blockers on #1919:
- HF_FORCE_DRAWELEMENT promoted from a stale "SCRATCH/Uncommitted" comment to
a documented diagnostic flag: it exists for upstream-Chromium repro work
(gate-vs-API isolation, crbug 521861819 149-vs-151) and R&D on gated effect
classes; renders under it may be damaged BY DESIGN since it bypasses gates
whose thresholds encode measured damage. Never production; the safety-net
blank guard also stands down under it so diagnostic frames arrive unmodified.
- armStaticDedup is now idempotent: the drawElement init path arms dedup
before canvas injection, then initializeSession called it again — the
second run overwrote the armed state with skipReason="capture_mode"
(captureMode is "drawelement" by then), producing contradictory telemetry
(armed frames + a skip reason), and re-ran the verification seeks on the
fallback path. It now no-ops once staticFrames or a skip decision exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(producer): fast-capture render stages + remote bg-image localizer
* feat(lint,player): fast-capture lint rule + player media sync
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): drawElementImage capture service
* feat(engine): 3D projection + compositor-effect risk gate
* fix(engine): gate filter drop-shadow wherever blur gates (review)
detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only
detected blur( in its three scan paths — a drop-shadow comp stayed on the
fast path despite the gate's own correctness contract. Detect drop-shadow(
in computed styles, stylesheet rules, and GSAP tween vars, pinned by a
focused test that runs the real page-side closure against a DOM shim
(computed / stylesheet / tween coverage + blur regression + effect-free
null).
Addresses miguel-heygen's blocker on #1918.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension
# Conflicts:
# packages/engine/src/services/screenshotService.ts
* fix(engine): document HF_FORCE_DRAWELEMENT as diagnostic-only; make armStaticDedup idempotent (review)
Addresses miguel-heygen's blockers on #1919:
- HF_FORCE_DRAWELEMENT promoted from a stale "SCRATCH/Uncommitted" comment to
a documented diagnostic flag: it exists for upstream-Chromium repro work
(gate-vs-API isolation, crbug 521861819 149-vs-151) and R&D on gated effect
classes; renders under it may be damaged BY DESIGN since it bypasses gates
whose thresholds encode measured damage. Never production; the safety-net
blank guard also stands down under it so diagnostic frames arrive unmodified.
- armStaticDedup is now idempotent: the drawElement init path arms dedup
before canvas injection, then initializeSession called it again — the
second run overwrote the armed state with skipReason="capture_mode"
(captureMode is "drawelement" by then), producing contradictory telemetry
(armed frames + a skip reason), and re-ran the verification seeks on the
fallback path. It now no-ops once staticFrames or a skip decision exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(producer): fast-capture render stages + remote bg-image localizer
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): drawElementImage capture service
* chore(ci): ignore drawElementService exports pending upstack consumers
Fallow's per-PR audit diffs against the merge base, so the bottom of the
fast-capture stack (#1917) sees drawElementService's exports as unused —
their consumers (frameCapture) land in #1919, two PRs upstack. ignoreExports
entry documents this and can be dropped once #1919 merges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): 3D projection + compositor-effect risk gate
* fix(engine): gate filter drop-shadow wherever blur gates (review)
detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only
detected blur( in its three scan paths — a drop-shadow comp stayed on the
fast path despite the gate's own correctness contract. Detect drop-shadow(
in computed styles, stylesheet rules, and GSAP tween vars, pinned by a
focused test that runs the real page-side closure against a DOM shim
(computed / stylesheet / tween coverage + blur regression + effect-free
null).
Addresses miguel-heygen's blocker on #1918.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension
# Conflicts:
# packages/engine/src/services/screenshotService.ts
* fix(engine): document HF_FORCE_DRAWELEMENT as diagnostic-only; make armStaticDedup idempotent (review)
Addresses miguel-heygen's blockers on #1919:
- HF_FORCE_DRAWELEMENT promoted from a stale "SCRATCH/Uncommitted" comment to
a documented diagnostic flag: it exists for upstream-Chromium repro work
(gate-vs-API isolation, crbug 521861819 149-vs-151) and R&D on gated effect
classes; renders under it may be damaged BY DESIGN since it bypasses gates
whose thresholds encode measured damage. Never production; the safety-net
blank guard also stands down under it so diagnostic frames arrive unmodified.
- armStaticDedup is now idempotent: the drawElement init path arms dedup
before canvas injection, then initializeSession called it again — the
second run overwrote the armed state with skipReason="capture_mode"
(captureMode is "drawelement" by then), producing contradictory telemetry
(armed frames + a skip reason), and re-ran the verification seeks on the
fallback path. It now no-ops once staticFrames or a skip decision exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): drawElementImage capture service
* chore(ci): ignore drawElementService exports pending upstack consumers
Fallow's per-PR audit diffs against the merge base, so the bottom of the
fast-capture stack (#1917) sees drawElementService's exports as unused —
their consumers (frameCapture) land in #1919, two PRs upstack. ignoreExports
entry documents this and can be dropped once #1919 merges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): 3D projection + compositor-effect risk gate
* fix(engine): gate filter drop-shadow wherever blur gates (review)
detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only
detected blur( in its three scan paths — a drop-shadow comp stayed on the
fast path despite the gate's own correctness contract. Detect drop-shadow(
in computed styles, stylesheet rules, and GSAP tween vars, pinned by a
focused test that runs the real page-side closure against a DOM shim
(computed / stylesheet / tween coverage + blur regression + effect-free
null).
Addresses miguel-heygen's blocker on #1918.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): drawElementImage capture service
* chore(ci): ignore drawElementService exports pending upstack consumers
Fallow's per-PR audit diffs against the merge base, so the bottom of the
fast-capture stack (#1917) sees drawElementService's exports as unused —
their consumers (frameCapture) land in #1919, two PRs upstack. ignoreExports
entry documents this and can be dropped once #1919 merges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Base of the studio UX-review stack (148 findings audited across the studio; 13 critical). This PR hardens the shared `components/ui` primitives that every later PR in the stack builds on.
## Changes
- **Button / IconButton**: visible `focus-visible` outline (studio accent); `disabled:pointer-events-none` removed (replaced with `disabled:cursor-not-allowed`, hover/active gated behind `enabled:`) so disabled buttons can host explain-why tooltips.
- **Tooltip**: keyboard support (`onFocus`/`onBlur` triggers), `role="tooltip"`, Escape-to-hide, viewport flip (top↔bottom) + horizontal clamping. API unchanged — all ~28 call sites unaffected.
- **HyperframesLoader**: `role="status"` on the loader; determinate track is a real `role="progressbar"` with `aria-valuenow/min/max` (was `aria-hidden`).
- **VideoFrameThumbnail**: error event resolves to a static fallback-label tile instead of an infinite shimmer; `motion-reduce` guard.
- **NEW `useDialogBehavior`**: shared modal contract — document-level Escape, Tab focus trap, focus-first-on-open, focus-restore-on-close, `canClose()` veto for dirty-draft guards. Adopted by every modal later in the stack.
- **NEW `SearchInput`**: shared search primitive with required `aria-label`, panel-input token style (kills the two-divergent-search-styles inconsistency in the sidebar).
- **studio.css**: `hf-toast-in/out` + `hf-backdrop-in` keyframes with `prefers-reduced-motion` guards (the previous `animate-in fade-in` classes were dead — no tailwindcss-animate plugin exists).
## Verification
- oxlint 0 errors, oxfmt clean, `tsc --noEmit` clean at stack top
- Full studio suite at stack top: 1189 tests pass
## Stack
PR 1/7 of the studio UX-review fixes. Merges bottom-up; the stack top is fully green (tsc + 1189 tests). Some shared-file edits span PRs, so intermediate branches may not typecheck in isolation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## 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)
The timing compiler scanned raw HTML with tag regexes that weren't
comment-aware, so a comment or script merely mentioning `<video>`/`<audio>`
was rewritten as a real element — injecting id/data-start/data-hf-auto-start
into the comment text. That phantom attribute then tripped the probe stage's
substring check (`html.includes("data-hf-auto-start")`), launching an
unnecessary browser probe on every render with an unexplained empty reasons list.
- Mask comments, <script>, and <style> regions before the tag scan, then
restore them verbatim (compileTimingAttrs, extractResolvedMedia).
- Replace the probe's substring match with a DOM query
(video[data-hf-auto-start]) and add "auto-start video(s)" to the reasons list.
* feat(lint): flag dir="rtl" on <html> as a confirmed silent render failure
Two independent reports diagnosed the same exact bug: dir="rtl" (or any
non-ltr value) on <html> renders correctly in preview/snapshot but
produces a fully blank/black video from render, with no other
lint/validate/inspect check catching it - output file size (far smaller
than expected) was the only tell for both reporters. Both independently
confirmed the same fix: drop dir from <html>, keep lang, and scope
direction: rtl to individual text-containing elements via CSS instead.
Could not empirically verify the render pipeline's own root cause in this
session (headless Chrome screenshot capture is unreliable in this
sandboxed environment - even a baseline, non-RTL capture timed out), so
this ships the safe, already-confirmed advisory rather than guessing at a
runtime fix. Both reporters explicitly asked for exactly this: "deserves
a lint rule or render-time warning."
* fix(lint): only flag valid non-ltr html dir values
* 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.
* fix(parsers,sdk,studio-server,studio): unify hf-id space across preview, disk, and SDK session
Root-causes the setTiming element_not_found resolver-shadow divergence class:
timeline edits carry hf-ids read from the live preview DOM, but the preview
minted ids AFTER rewriting attributes (and never persisted them for sub-comps),
while the SDK session mints from the raw file — content-keyed minting then
yields different ids for the same element. Template-based comps were worse:
the SDK excluded the whole <template> subtree, so the session had zero
elements and every edit diverged.
- parsers: ensureHfIds now descends into <template> subtrees (linkedom's
querySelectorAll does not), minting and pinning inner ids
- sdk: buildRoots/buildElement treat <template> as a transparent container,
and resolution (resolveScoped, animation-id map) searches template subtrees
via querySelectorAllDeep — template comps now model, resolve, and edit
- studio-server: the sub-comp preview route persists hf-ids to the raw file
BEFORE the rewrite pipeline (mirrors the main route), pinning one id space
across served DOM, disk, and SDK session
- studio: resolver-shadow skips structurally-empty sessions (no event, no
attempt) and tags fail-open emissions with sourceReadFailed so read errors
are distinguishable from unwired readers in telemetry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(parsers,sdk,studio-server,studio): scope template descent, guard persist route
Addresses the 10 verified findings from the PR #1981 review:
- Restrict template transparency to COMPOSITION templates
(<template data-composition-id>) everywhere — ensureHfIds, SDK
buildChildren, querySelectorAllDeep. A plain <template> (runtime
clone-source) keeps its old fully-excluded behavior: stamping its
interior would duplicate one persisted id across every runtime clone,
and modeling it would show phantom timeline clips.
- Guard the sub-comp persist: only .html files (the wildcard route can
serve any project path — stamping an SVG corrupted it on disk),
try/catch the read (file-removed race becomes 404, not 500), salt the
etag (v2) so pre-fix cached clients don't 304 past the id pin, and
thread the stamped content into buildSubCompositionHtml so served ids
match the mint even when the disk write is skipped.
- Rewrite querySelectorAllDeep as a document-order DOM walk — appending
template matches after top-level matches made duplicate-id tiebreaks
disagree with the preview's unwrapped DOM (wrong-element edits).
- Recurse sourceMutation.querySelectorAllWithTemplates so server-side
ops resolve ids at any template depth, matching SDK resolution.
- Replace the empty-session silent skip with ONE tagged session_empty
event per session — silence would blind the tripwire to exactly the
modeling-gap class that exposed the template bug. Attempts stay
uncounted (an unmodelable comp can't cut over).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio-server): close TOCTOU in sub-comp hf-id persist (CodeQL js/file-system-race)
Replace the route-level stat/read/persist sequence with stampFileHfIds:
validation (fstat), read, mint, and write-back all go through ONE open
file descriptor (O_NOFOLLOW where supported), so the path cannot be
swapped between validation and write. Falls back to read-only stamping
when the file isn't writable — content-keyed minting means the SDK
derives the same ids from the same bytes even without the disk write.
Addresses miguel-heygen's blocking review on PR #1981.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio-server): linear-time template-attr match (CodeQL js/polynomial-redos)
promoteTemplateCompositionId's single-pattern regex backtracked
polynomially on crafted input. Two-step match: grab each <template>
open tag linearly, then find data-composition-id within that short
tag text. Same semantics (first template carrying the attr wins).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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>
* 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>
Miguel's fix (f999b40d) added the require/__filename/__dirname shims to the
Lambda handler bundle after #1932 crashed every render at import with
"__dirname is not defined in ES module scope" (wawoff2's emscripten build
reads __dirname at module scope; it's inlined via producer -> fontCompression).
The accompanying test only grepped build-zip.ts for the banner literals, so it
passes even if the shim is renamed, reordered into a broken form, or if a new
inlined CJS dep needs a global the banner doesn't provide.
Replace it with a behavioral test: extract the banner to _handlerBanner.ts
(build-zip.ts self-executes on import, so it can't be imported directly),
bundle a fixture that touches __dirname/__filename/require with the real
banner, and import the output under real Node -- not the bun test runtime,
which defines __dirname in ESM and would mask a missing shim. The import
faithfully reproduces Lambda's Node ESM environment and fails with the exact
#1932 error when any shim is dropped.
Handler bundle output is unchanged (identical banner string).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exhaustive audit of every MDX file in docs/ against skill references
and package source code. Every API signature, default value, flag,
and technical claim verified against ground truth.
recordAnimationResolverParity reported a false animation_not_found divergence
for any tween whose selector doesn't currently CSS-match a live DOM element,
because it only checked el.animationIds (DOM-gated). The real server-side op
it shadows resolves purely from the parsed script. Adds
Composition.getAllAnimationIds() as a DOM-independent id set and checks it
too, matching the server's actual resolution behavior.
On Windows, resolveSpawnCommand routes `npx` through node + npm's
npx-cli.js (avoiding the un-spawnable npx.cmd), locating that CLI via
npm_execpath. When the script is run directly with `node audio.mjs`
instead of through npm/npx, npm_execpath is unset, so resolution returns
null and spawnP short-circuited to `{status:-1}` — silently. With
stdio:"ignore" hiding everything, callers just reported "TTS failed -
omitted" for every single line, giving no hint that the real cause was
an unresolvable npx. Debugging required reading the source.
Fix: when spawnP hits that null-resolution path, emit a clear one-time
diagnostic naming npm_execpath and the remedy (run via npx/npm, or export
npm_execpath) before returning {status:-1}. One-shot latch so a batch of
lines logs it once, not per line. Behavior is otherwise unchanged — still
returns {status:-1} and spawns nothing.
Test: new tts.spawn.test.mjs case — two consecutive win32 npx calls with
npm_execpath unset both return {status:-1}, nothing is spawned, and the
diagnostic (mentioning npm_execpath) is emitted exactly once. Existing
spawn tests unchanged (7/7 pass).
pipInstall() spawned a bare "pip" binary. Many Homebrew/system Python
installs expose only python3/pip3 on PATH, so the spawn silently ENOENTs
and the documented "auto-installed on demand" local MusicGen path never
actually installs anything - the failure is invisible since spawnSync's
status just comes back non-zero like any other install failure.
Switched to `python3 -m pip install`, matching this same file's own
pyOk() convention of always invoking python3 explicitly. This also
closes a second latent bug: a bare pip/pip3 could resolve to a different
Python installation than the python3 binary pyOk() probes against if
more than one is on PATH, so `-m pip` guarantees the install lands in the
exact interpreter being checked.
Manually verified `python3 -m pip --version` succeeds in this
environment. No automated test added - this is a literal command-array
swap with no new branching logic, and spawnSync is a named import from
node:child_process with no clean mocking path available without a
larger refactor disproportionate to the fix's size.