The ENAMETOOLONG regression writes 150 real clip files and the mixer
existence-checks each one: ~58ms on Linux, but past vitest's 5s default on
the Windows lane. packages/engine sets no global testTimeout, so heavy
tests here carry an explicit one.
On timeout its abandoned async work kept calling the shared runFfmpegMock
after afterEach cleared it, so the next test saw 5 calls instead of 3 and
lost its queued once-implementations to the leak. mockReset stops an
aborted test from handing leftovers to the next one.
* docs(changelog): weekly digest 2026-08-24–2026-08-31
Rewrites the generated draft to publish quality: grouped by theme,
commit and PR links kept, review marker removed.
Generated and edited by Rames.
* docs(changelog): embed the Aug 24-31 weekly video
Adds the rendered weekly changelog video to the "Week of August 24, 2026"
digest entry, matching the DocsVideo embed shape used by the six prior weeks.
Video: 1080x1080, 50.0s, Annie VO, built with the changelog-video skill.
Gates: hyperframes check 0 errors (contrast 82/82 WCAG AA), seam gate 0 fail
across 6 seams, captions verified on rendered frames at every scene midpoint.
Signed-off-by: Rames Jusso
---------
Signed-off-by: Rames Jusso
* fix(media-use): repoint the dead videogen tier, demote past unusable models
`LOCAL_MODELS.videogen`'s `large` tier named `dgrauet/ltx-2.3-mlx-bf16`, which
returns HTTP 401 and cannot be downloaded at all. It was not a dormant entry:
`rankedByPreference` sorts by descending `needs.ramMB` when no `rank` is set,
so the largest fitting tier is tried FIRST by design. Any machine clearing
32 GB *available* RAM selected the dead entry, `ltxVideoGenerate` caught the
failure and returned a bare `null`, and since `ltx.local` is last in
`["heygen.video", "ltx.local"]` and network providers are skipped under
`--local-only` (`registry.mjs:206`), local video generation failed outright
instead of falling back to the tier that works.
It survived review because the table landed with "live verification on a 24GB
M-series Mac" - and a 24 GB machine cannot select a 32 GB tier, so that entry
was unreachable on the only machine that validated it. The unit fixtures
inherit the same ceiling (`fittingSpecs` is 20000MB), so every existing test
exercised the medium tier alone.
Two changes:
1. Repoint to `dgrauet/ltx-2.3-mlx-q8` (reachable) and correct `sizeMB` from
45000 to 28800. Measured against the HF API: the q8 repo totals 87.5 GB,
and the registry's own targeted `--include` subset is 28.76 GB. That
matches the sibling q4 entry's convention (`sizeMB: 20000` vs a measured
19.48 GB subset), so 45000 was wrong under either reading. `--low-ram` is
added because the entry's own note calls it required at this tier's 32 GB
floor, and the invoke omitted it.
2. A repoint alone is one bad URL from a repeat, so add the missing recovery.
`selectModelLadder` returns every fitting model best-first;
`selectModel`'s pick is now defined as that list's head. All three sites
that previously selected exactly one model and failed terminally walk the
ladder instead, demoting past a tier that cannot run here - gated weights,
runner off PATH, an OOM at a tier that nominally fits:
- `ltx-video-provider.mjs` (videogen, the reported failure)
- `mflux-provider.mjs` (imagegen - same shape, and its 32 GB/64 GB tiers
are equally unverifiable on a 24 GB machine)
- `local-run.mjs` (tts/asr/upscale - `fish-speech` missing should still
get you Kokoro)
Every demotion is logged rather than silent, so a quietly smaller model is
never mistaken for the tier the machine nominally qualified for.
Also fixes the `install` string both videogen entries share: it ended at
`uv sync --all-extras`, which leaves the entry point in `.venv/bin`, so the
"`ltx-2-mlx` not on PATH" hint named a command that following the instruction
would not put on PATH.
The q8 tier is NOT live-verified - no 32 GB+ Apple Silicon machine was
available - and its notes say so. Shipping it unverified is safe precisely
because of change 2: a wrong tier now costs one failed attempt, not the whole
local path.
- Rames Jusso
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(media-use): report the real videogen download size, disclose it, discard failed partials
Addresses review feedback on #3509 (CHANGES_REQUESTED at 90df164a), plus the
follow-on ask to tell the user what a download costs before they accept it.
1. `sizeMB` described a targeted `--include` subset that no run ever gets.
Both videogen invokes pass a repo id to `--model`, and upstream
`resolve_model_dir()` (`ltx_pipelines_mlx/utils/_orchestration.py:35-40`)
calls `snapshot_download(repo)` with no `allow_patterns`, so the full repo
lands regardless of what was pre-fetched. Corrected to measured repo
totals: q8 87500 (87,511,991,375 B) and q4 59700 (59,686,429,583 B). q4 was
wrong the same way at 20000, so both are fixed together rather than leaving
one convention on each side.
My earlier claim that 28800 "matches the sibling q4 entry's convention" was
wrong in the way that matters: the convention itself described a subset the
runner does not honor. The file's own comment already said "blind
snapshot-downloads the lot (60 GB q4, 88 GB q8)" three lines above the
fields that contradicted it, and the original report measured it too ("the
q4 cache ended at 56 GB and q8 at 82 GB"), which reconciles exactly once
read as GiB: 59.69 GB = 55.6 GiB, 87.51 GB = 81.5 GiB. So the download is
the complete repo both times, not a partial fetch.
Removed the `--include` recipe rather than repairing it: it is ineffective
(the runner refetches at generate time) and insufficient (`--two-stage` is
"dev model + CFG at half-res, upscale, distilled LoRA refine" per upstream's
own help text, so it needs transformer-dev AND transformer-distilled AND
spatial_upscaler_x2; `--distilled` needs an upscaler too). The q4 tier
verified on a 24 GB Mac only worked BECAUSE the download is unfiltered.
2. Nothing told the user what they were agreeing to before a tool started
pulling tens of GB. `describeDownload()` in `specs.mjs` names the size and
the directory the weights land in, and checks free space with `statfs`
against that directory rather than cwd, since the weights do not land in
cwd. A tier that will not fit is still offered, with a plain statement that
it will not fit: hiding it would make a machine that could free up space
look like it has no large tier. Unknown free space reports as unknown, not
as zero. Wired into both providers' install hints, the `runLocalModel`
install payload (now carrying `sizeMB`), and `describeModelLadder`.
3. Each retry attempt mints its own timestamped temp path, so a partial
artifact from a failed tier was orphaned rather than overwritten, and a
lower tier then succeeding hid it. Both providers discard the partial before
demoting, guarded so a file that cannot be removed never masks the generate
failure it came from. Video is the material case: a partial mp4 is large.
`local-run.mjs` is deliberately unchanged here. Its `out` is caller-provided
and identical across attempts, so a partial is overwritten rather than
orphaned, and unlinking a path the caller named would be a footgun. The rule
the two providers follow is: clean up what you allocate.
Tests: 553/553 across `skills/**/*.test.mjs` (+15). New coverage pins the
cleanup (failed tier's partial removed, returned artifact survives, one discard
per attempt on the all-fail path, an unremovable partial still surfaces the
real failure) and the disclosure (cache-dir precedence, statfs walk-up to the
deepest existing ancestor, unknown-vs-zero, and the will-not-fit wording).
Every new guard mutation-tested: removing any one of them turns tests red.
- Rames Jusso
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grouped audio shipped and its three canaries were deleted rather than raised.
Solo and the group meter shipped and were removed in the same week, so the
digest says so. Notes that the v0.8.0 minor bump marks two catalog component
removals rather than the week's headline.
Generated with bun run changelog:weekly, then rewritten for publication.
Every sha was machine-verified: 40 chars, prefix-matched, an ancestor of main,
and inside the 2026-08-17..2026-08-25Z window. npx mint validate passes from docs/.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
seam-gate verify --project has failed with "preview server exited early" since #3310, which made `hyperframes preview` pick its launch mode from the TTY. The gate spawns with stdio [ignore,pipe,pipe], so interactive is false and it silently took the background path: the launcher exits 0 before the server is serving (read as a dead server), and the detached server escapes the gate's process-group cleanup (leaking a preview per run).
Pass the CLI's own --foreground opt-out in the default --server-cmd. Both skill mirrors updated.
— Rames
* docs(changelog): weekly digest 2026-08-10–2026-08-17
Rewritten from `git log --no-merges` grouped by type and scope rather than
polished from the generator draft.
The window overlaps last week's digest. 106 non-merge commits land inside
2026-08-10T00:00:00Z..2026-08-18T00:00:00Z, but five were already published
in the Aug 3-10 entry (#3148, #3149, #3150, #3089, #3151, all dated Aug 10).
Those are excluded by set-subtracting the 109 SHAs that entry cites, so this
covers 101 commits and cites 89 PRs. The release range is corrected the same
way: last week claimed v0.7.90 through v0.7.105, and both the v0.7.104 and
v0.7.105 release commits fall inside this window, so this week is v0.7.106
through v0.7.109.
Verified at HEAD rather than from commit bodies:
- The audio FX rack is behind the `audio-fx-rack` canary at percentage 0 in
canaryRegistry.ts, and the gate site is real: PropertyPanelFlat.tsx reads
isCanaryEnabled("audio-fx-rack"). It is described as staged, not shipped.
The flag gates the authoring surface only, so a composition that already
carries data-fx-chain still plays and renders, and that is stated.
- The video primitive moves were added, reverted twenty minutes later
because their previews did not deploy, then relanded with the workflow
fixed. Only the end state is announced.
- The CLI canary route was added and reverted inside the window, so the
revert is what gets reported.
- The preview volume control landed after v0.7.109, so it is called out as
shipping in the next release rather than as available now.
Every SHA is emitted by a script that refuses to write unless it resolves to
40 chars, prefix-matches its abbreviation, is an ancestor of origin/main,
falls inside the window, and was not cited by the previous digest. Six
negative controls confirm each check rejects, and a positive control confirms
valid input still writes. No SHA was hand-typed.
* docs(changelog): embed the weekly video in the Aug 10-17 digest
45.1s square film built from the changelog-video skill. Uploaded to
static.heygen.ai and the CloudFront path invalidated and verified serving.
* feat(engine): stamp rendered files with hidden renderer provenance
* fix(engine,producer): re-assert provenance at every container writer
Review found that a no-audio MOV render still shipped untagged. The concat
step is the last container write on that path (mux is skipped without audio,
and applyFaststart only copies mov/webm), and the concat demuxer does not
carry the chunks' container metadata through.
The same hole applies to no-audio WebM, and to the in-process chunked encode
in chunkEncoder, not just the distributed assemble path. mp4 was masked
throughout because applyFaststart re-runs ffmpeg for that format and re-tagged
the output.
Tags the four remaining writers: the chunked-encode concat, and assemble's
single-chunk remux, concat and cfr re-encode.
Also corrects the trust claim. These are unsigned, freely writable keys, so a
present tag means the file claims to be HyperFrames output, not that
HyperFrames wrote it. Documented as an unauthenticated diagnostic hint rather
than an authenticity or attribution boundary.
Tests assert on the assembled file through the real assemble() path for both
mov and webm; both fail without the concat fix.
* test(engine): pin provenance through the in-process chunked concat
Review noted the distributed writers are mutation-pinned but the
encodeFramesChunkedConcat fix had no real-file regression of its own.
Encodes 70 frames at a 30-frame chunk size so the concat step actually runs,
then asserts the tags on the resulting no-audio mov. Fails without the concat
fix, passes with it.
A family resolving through FONT_ALIAS_MAP could emit @font-face rules drawn from two
unrelated typefaces under one font-family name, split by weight and style. The
supplementation fetch was passed the authored name, so for a cross-typeface alias
(helvetica -> inter) it asked Google for the very typeface the alias exists to replace.
Diagnosed, reported and fixed by Akshay Kumar Sharma (@akzarma) in #3083 / #3085. This
PR carries that work because the fix requires re-recorded regression baselines, which
are LFS objects we cannot push to a fork's LFS store.
Baselines re-recorded for style-15-prod and style-3-prod, each verified text-only
before acceptance. All 9 regression shards pass.
Closes#3083.
Co-authored-by: Akshay Kumar Sharma <25038017+akzarma@users.noreply.github.com>
* fix(skills): stop embedded-captions shipping author-only paths
The skill is distributed via `hyperframes skills` (--copy, so the installed
bundle matches the published tree), but three shipped files pointed at
directories that only exist on the original author's machine.
- references/test-set.md: deleted. Nothing in the skill referenced it (zero
inbound links across all 140 files), and its corpus lives at
~/Downloads/heygen_relevant_videos/, so it was neither reachable nor runnable.
- SKILL.md / dna/README.md: the retired 54-template archive was stated as
living at a ~/Downloads path. Keeps the fact, drops the false location.
- themes/PORTING.md: marked maintainer-only. It authors new theme DNAs rather
than using the skill, nothing links it, and its inputs (the cap_fx3 demos,
the frame corpora, CONTRACT.md) are not distributed. The paths stay as the
author's original layout, now explicitly labelled as names for the inputs.
The 10 references to ~/Downloads/hyperframes are untouched: those are the
documented last resort in a real chain (HYPERFRAMES_ROOT -> in-repo -> that
path), and every file carrying it also carries the env var.
Regenerates skills-manifest.json, which hashes the whole skill directory.
Closes#3219
— Rames Jusso (James's assistant)
* fix(skills): delete themes/PORTING.md instead of marking it internal
Nothing in the skill referenced it, its inputs are not distributed, and git
history keeps it recoverable. Marking a dead procedure still ships the dead
procedure.
— Rames Jusso (James's assistant)
* fix(skills): remove duplicated media-cleanup block in slideshow
The media-cleanup and global-nav-mute sections appeared twice in
skills/slideshow/SKILL.md. The two copies were not identical: the later one
was missing the paragraph requiring custom media controls to sync through
media events. Deletes the incomplete copy, keeps the complete one.
Originally authored in #3218. Carried here re-signed because the org requires
signed commits and both changes regenerate skills-manifest.json, which would
otherwise conflict between the two PRs.
Co-authored-by: Lance Curtis <69400491+imprimisxo@users.noreply.github.com>
* chore(skills): regenerate manifest for slideshow + embedded-captions
— Rames Jusso (James's assistant)
* chore(skills): record the two embedded-captions deletions in the guard
check-no-main-deletions.mjs deliberately has no blanket override, so each
intentional deletion is named with its reason and shows up in review.
— Rames Jusso (James's assistant)
---------
Co-authored-by: Lance Curtis <69400491+imprimisxo@users.noreply.github.com>
* docs(changelog): weekly digest 2026-08-03–2026-08-10
Rewritten from `git log --no-merges` grouped by type and scope, not polished
from the generator draft. The generator's picks were a 10-item list that put
two internal tooling commits in Highlights, split the router story across
three bullets that supersede each other, and covered none of the week's 70
fixes or 76 docs commits. This cites 109 commits and 45 PRs.
Verified at HEAD rather than from commit bodies:
- The DE parallel router is default-on for every install. The canary entry is
gone from canaryRegistry.ts and render.ts no longer gates it. Framed as a
restoration, since the canary had cut fleet exposure to 0.13 percent.
- Catalog meaning search is opt-in behind a 33 MB consented download, so it
is not described as default behaviour.
- The Reference Project landed and was removed inside the same window, so it
is reported as both rather than announced as new.
- 30 Days really does carry thirty distinct films. Examples now has 23, not
the 19 its commit subject claims, so no count is published for it.
Every SHA is expanded from a token by a script that refuses to write unless
it resolves to 40 chars, prefix-matches, is an ancestor of origin/main and
falls inside the window. No SHA was hand-typed.
* docs(changelog): correct release range to v0.7.90 in weekly digest
v0.7.91 was never published. Its artifacts were folded into v0.7.92, so
the sequence goes v0.7.90 -> v0.7.92 and the window's first release is
v0.7.90. Still fifteen releases.
* docs(changelog): embed the weekly video in the Aug 3-10 digest
45.1s square film built from the changelog-video skill. Uploaded to
static.heygen.ai and the CloudFront path invalidated.
`hyperframes skills update` deleted skills that the same command had just
installed, from every agent directory on the machine, and reported them as
"no longer published".
`skills add --skill '*'` installs every skill in the repo — including the
repo-native ones under `.claude/skills/` and `.agents/skills/` — and the
upstream lock attributes all of them to `heygen-com/hyperframes`. The published
manifest is generated from `<repoRoot>/skills` only (gen-skills-manifest.ts), so
it never lists those. detectRemoved read that silence as "removed upstream" and
pruned them, so `check || update` could not converge: `add` reinstalled them and
the next `update` deleted them again.
Scope removed-detection to skills the manifest is actually authoritative for,
using the lock's `skillPath` — the only field that separates a skill installed
from `skills/` from one installed out of the same repo's other skill roots
(`source` is identical for both). An entry with no `skillPath` is treated as not
covered: this is a delete path, so unknown provenance fails safe.
Also resolve the prune's manifest canonically. Its notion of "still published"
could otherwise come from any `skills-manifest.json` within 16 parent
directories of cwd, which — since HyperFrames' own manifest declares
`source: heygen-com/hyperframes` — matches lock attribution and drives deletion.
The install-side check already did this (#2176); the deleting path did not, and
the comment claiming that was deliberate and "tested separately" had no such
test. An explicit `--source` still wins.
Verified end to end against the real CLI in a sandboxed HOME. Before: `add`
installed 25 skills, `update` printed "Removing 6 skill(s) no longer published:
captions-overlay, changelog-video, cut-the-curve, motion-doctrine,
oversized-cursor, seam-craft" and deleted all six (27 dirs -> 21). After: no
removal line, 27 -> 27. Both new regression tests fail on the pre-fix source.
Fixes#3111
* fix: bound HDR and video extraction resources
* fix: trim negative video extraction preroll
* fix: skip invisible video extraction windows
* fix: preserve negative-start loop and held tails
* fix: cap finite video slots to source duration
* fix: bound held-tail frame extraction
* fix: plan from playable video duration
* fix: preserve open-ended held video tails
* fix: resolve held tails from decoded frames
* fix: normalize final-frame probe timestamps
* fix: handle unseekable final-frame sources
* fix: dedupe final-frame probes per render
* refactor: clarify output dynamic range contract
## What
Refactor distributed planning around one shared local execution-plan builder:
- `buildLocalExecutionPlan()` now owns compile/probe/extract/audio/freeze.
- Legacy `plan()` remains a deprecated v1 transport wrapper.
- Plan v2 calls the shared builder directly and publishes through the existing manifest/CAS contract.
- Add neutral `createPlanV2FromExecutionPlan()`, `publishPlanV2FromExecutionPlan()`, `getPlanV2ExecutionPlanHash()`, and `PLAN_PROTOCOL_V1` names.
- Retain deprecated v1-named exports and wire aliases.
- Recommend explicit Plan v2 opt-in for new producer, Lambda, and Cloud Run integrations.
## Why
Plan v2 previously looked like it invoked a v1 planner even though v1 and v2 share the same frozen local execution representation. This removes that migration-era coupling while preserving the public minor-version compatibility contract.
## How
The shared builder returns neutral internal execution-plan fields. The v1 wrapper maps those fields back to the existing `PlanResult`; the v2 publisher consumes them directly.
Compatibility is intentional and covered by exact shape tests:
- omitted `planProtocol` still serializes/selects `"v1"`;
- v1 layouts, descriptor-less decoding, event unions, workflow branches, and exports remain;
- the v1 descriptor JSON is byte-identical and `CURRENT_PLAN_PROTOCOL` is an identity-preserving alias;
- v2 manifest bytes, key order, hash framing, and `sourcePlanV1Hash` wire key remain unchanged;
- no enumerable neutral hash field was added to manifests or returned result objects;
- v1/v2 result objects, cloud event payloads, and SDK handle key sets remain unchanged.
## Test plan
- Focused Plan v1/v2/protocol/export/size compatibility: 141 passed
- `@hyperframes/core`: 1,419 passed
- `@hyperframes/producer` unit lane: 990 passed
- `@hyperframes/aws-lambda`: 140 passed
- `@hyperframes/gcp-cloud-run`: 101 passed
- Producer, Lambda, and Cloud Run typechecks
- Repository-wide lint, format check, workspace/package-subpath checks
- Full workspace build
- `git diff --check`
- [x] Unit tests added/updated
- [ ] Manual testing performed
- [x] Documentation updated (if applicable)
Adds three test cases pinning the code-vocabulary section that frame-packets.mjs appends to code frames. Deleting codeVocabularySection outright left all 455 skills tests green before this change; the only assertion touching it was a doesNotMatch that passes trivially when the section is empty.
## What
- enforce a finite, validated `meta/videos.json` contract shared by Plan v1 and Plan v2
- preserve authored finite ends and source-derived trim-aware ends; bound any still-open end at the validated composition end
- fail distributed planning when any declared video source did not extract instead of publishing a blank-capable plan
- make the v1 chunk reader reject malformed/null video timing before frame injection
- route deterministic video-source/metadata failures as non-retryable in AWS and GCP while retaining retries for transient extraction failures
## Why
An open-ended video whose remote source could not be resolved retained `Infinity` through planning. Plan v2 correctly rejected that value, while Plan v1 serialized it as `null`; the v1 frame lookup could then suppress injected frames and silently produce incorrect output.
The invariant belongs at the shared metadata boundary. Both protocols must receive identical finite timing, and unavailable sources must fail closed before plan publication.
## Test plan
- [x] producer distributed planning, metadata, v1 chunk boundary, Plan v2 conversion/materialization, and public exports
- [x] core runtime media semantics (authored slots, natural duration, looping, non-looping hold)
- [x] engine video extraction and frame lookup
- [x] AWS Lambda/CDK/SAM and GCP Cloud Run error normalization/retry classification
- [x] producer, core, engine, AWS, and GCP typechecks/builds
- [x] formatting, oxlint, tracked-artifact, fallow, and commit hooks
- [x] exact incident composition replayed through the AWS Lambda handler's Lambda-local path in a Lambda-like container; Plan v1 and Plan v2 both fail closed as `VIDEO_SOURCE_UNRENDERABLE` during planning, before plan publication
- [x] full PR CI, including all nine regression shards and Windows render/tests
No production flags or deployment/release workflows are changed.
## What
Extracts the reliability-critical telemetry **delivery layer** — the in-memory event queue, async `flush()`, and the exit-time detached-child `flushSync()` — out of `packages/cli/src/telemetry/client.ts` into a new `transport.ts`.
`client.ts` stays the CLI-facing **policy** layer:
- `shouldTrack()` opt-out checks (dev mode, `DO_NOT_TRACK`, `HYPERFRAMES_NO_TELEMETRY`, config)
- `trackEvent()` system-metadata enrichment
- `showTelemetryNotice()` first-run disclosure
…and re-exports `flush` / `flushSync`, so `events.ts`, `index.ts`, and the `cli.ts` exit handlers keep importing from `./client.js` **unchanged**.
## Why
This is the code path that had the process-exit data-loss bug fixed in #2105 — render telemetry was ~6× undercounted and geographically US-skewed because the old drain-first flush emptied the queue before delivery confirmed, and the render command's `process.exit()` teardown killed the in-flight request. Isolating the delivery mechanism into its own focused, dependency-light module (only `./config` + node builtins) keeps that subtle, reliability-critical path in one place and reduces `client.ts` to just policy.
Follow-up to the render-telemetry-gap investigation. A delivery-health canary was also added to the [CLI Observability dashboard](https://us.posthog.com/project/356858/dashboard/1634055) — `render_complete ÷ successful render commands`, which should sit ~1.0 and would surface any regression of this class immediately.
## How
Pure code motion — **no behavior change, public API identical**. `transport.ts` owns the queue and stamps each event's dedup `uuid` + ISO timestamp in a new `enqueue()`; `trackEvent()` enriches with system metadata then calls `enqueue()`. `buildPayload`/`flush`/`flushSync` bodies are moved verbatim.
## Test plan
- [x] `vitest run src/telemetry/client.test.ts src/telemetry/events.test.ts` → **38/38 pass** (client.test.ts still validates queue-retention, uuid idempotency, and the detached-child flushSync path through the public API — unchanged)
- [x] `oxlint` clean, `oxfmt --check` clean
- [x] `tsc --noEmit` — no new type errors in `telemetry/`
- [x] `bun run build` succeeds
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What
Adds the weekly digest entry for July 20 to July 27, 2026 to `docs/weekly-updates.mdx`.
Generated with `bun run changelog:weekly --from 2026-07-20 --to 2026-07-27 --write`, then rewritten to publish quality: entries grouped under Features, Fixes, and Docs, prose rewritten to describe user impact, and the `<!-- TODO: review -->` marker removed.
## Why
Keeps the public weekly-updates page current. The digest is the RSS-backed summary readers get between versioned releases.
## How
- Ran the generator over the July 20 to July 27 range.
- Rewrote the new `<Update>` block: 11 Features, 15 Fixes, 3 Docs bullets.
- Every commit and PR link the generator produced for these entries is retained. No link was invented; all references trace back to `updates/weekly/2026-07-27.md`.
- Headline items: professional color grading across core, Studio, and the CLI; a versioned distributed plan protocol with direct S3 and GCS publishing; the Studio keyframe ease editor; data-driven registry caption components.
- `renderStretch` (#2676) is deliberately omitted, since it was reverted in the same window by #2730 and is not present at HEAD.
- CI-only performance and internal refactor entries are omitted as not user-facing.
## Test plan
Docs-only change. No runtime code touched.
- [ ] Unit tests added/updated
- [x] Manual testing performed
- [x] Documentation updated (if applicable)
Verification performed:
- `git diff origin/main --stat` shows only `docs/weekly-updates.mdx`, 49 insertions and 0 deletions (purely additive, the prior week's entry is untouched).
- Confirmed the TODO marker is gone from the file.
- Confirmed every commit SHA and PR number in the new block appears in the generator's own draft, and that each link label matches its href.
- Confirmed the block contains no em-dashes and no stray JSX-unsafe characters outside inline code.
- `node scripts/check-tracked-artifacts.mjs` passes.
* ci(regression): compute the shard matrix from recorded fixture timings
* ci(regression): refresh shard timings from a green post-PSNR run
* fix(ci): close two silent-skip holes in the shard schedule contract
* ci(regression): schedule the new static-volume-future-set fixture
* test(producer): regenerate static-volume-future-set golden in the pinned container
* fix(producer): accept partial color metadata in plan v2
* test(engine): make partial color probe hermetic
* fix(producer): validate plan v2 sentinels in fallback mode
## What
Allow Studio Preview to serve an asset reached through a project-local symlink whose target is in a shared directory outside the project, including browser-hostile video assets that need an authoring proxy.
## Why
Preview rejected these assets with a 404 while the renderer accepted the same path. The initial static-route fix still failed for HEVC, ProRes, AV1, and VP9 assets because the proxy transcoder rejected the external target.
## How
Use lexical project-root containment for the read-only static asset route and proxy source request. The transcoder canonicalizes the target for ffmpeg and includes that identity in its cache key, while keeping the proxy cache inside the project. Composition source paths retain canonical containment because preview can persist their data-hf-id values.
## Test plan
- [x] Unit tests added/updated
- [x] `bun run --cwd packages/studio-server test` (397 tests)
- [x] Studio Server typecheck, oxlint, and oxfmt
- [x] External-symlinked hostile-video proxy route regression
- [x] Static-route traversal regression
- [ ] Documentation updated (not applicable)
Chrome 112+ / Firefox 117+ / Safari 16.5+ support native CSS Nesting.
A nested rule like '.title { … }' inside '[data-composition-id="intro"]
{ … }' resolves at match time to '<parent> .title' via the implicit
'&' prefix.
'scopeCssToComposition' walks every rule via 'root.walkRules' and re-
scopes selectors, but it did so for nested rules too — producing
'[…scope…] .title' inside '[…scope…] { … }', which nesting then
prepends AGAIN to '[…scope…] […scope…] .title'. Since the composition
root only appears once in the DOM, the doubly-scoped selector never
matches — the nested rule appears 'just ignored' as the reporter
described (#2721).
Reproduced on 0.7.66 with the reporter's exact composition. Fix: add
'isNestedInsideAnotherRule' predicate — mirrors the existing
'isInsideGlobalAtRule' — and skip nested rules in the walkRules
callback. Top-level rules still get scoped; their nested descendants
inherit scope naturally via CSS Nesting at match time.
Added two focused tests:
- 'preserves nested-rule selectors so CSS Nesting inheritance works
(#2721)' — asserts nested '.title' and 'h2' selectors stay verbatim
while top-level rules keep scoping.
- 'preserves deeply-nested CSS Nesting rules (#2721)' — same rule at
depth 3.
All 37 existing scopeCssToComposition tests still pass.
Fixes#2721.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
#2606 taught the video-coverage gate that a non-looping short video holds
its final decoded frame across the tail, so the delivered source frames
are enough to cover the authored slot. But that fix gated the credit on
'!video.loop' — a looping short video was still measured as
unique-source-frames / slot-frames and aborted at ratio << threshold.
Reproduced on 0.7.64 with the reporter's exact composition (3s clip in a
10s slot, loop attribute): render aborts with 'captured 90 of expected
300 frames (coverage 30.0%)'. Same source without loop renders clean via
#2606's freeze credit. This is the mainline 'loop a short clip to fill a
longer scene' case, the reason loop exists.
Fix: extend #2606's source-credit to loops symmetrically — the delivered
set (all N source frames) covers every repeat within the slot, so
expectedFrames = min(slotFrames, sourceFrames) for both hold and loop.
Fail-loud preserved for a genuinely-broken loop (extractor truncated
below its own source): a 60/90 delivery still aborts at 66.7% < 95%
because the delivered set no longer covers the full source period the
loop reuses. Missing extractions still require the full slot.
Test updates:
- 'still requires the full authored slot for looping clips' locked in
the buggy behavior; replaced with 'credits a looping short clip
against the source portion' which asserts the correct 90/90/1.0.
- Added 'still fails when a looping clip's source extraction is
truncated' as the new fail-loud floor.
Fixes#2665. Regression window: 0.7.60 (#2606's original ship)
through 0.7.67 (current).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* docs(skills): make captions non-optional in changelog-video
The Jul 13-20 build shipped without captions because the pipeline had two soft
failure modes:
- Empty vo-words.json from the TTS provider was silently accepted, so
align-captions.mjs had nothing to align. captions.json was never produced.
- Step 5 tolerated the master-skeleton's LINES array staying empty. The build
lint-checked and validated clean, but the caption rail rendered no text.
This adds three hard gates on top of the existing scaffold:
- Step 4 adds a whisper forced-alignment fallback so a missing TTS timestamp
payload no longer breaks the caption pipeline. Whisper only supplies timings;
captions still use the DISPLAY layer from script-tokens.json.
- Step 5 flags an empty LINES array as a shipped bug and shows the exact
IIFE-input shape it expects.
- Step 6 adds gate 5: sample 3-4 frames across the VO window and confirm visible
caption text on each. If any spoken window renders no caption, the build is
red.
Anti-patterns table gets two rows covering the empty-LINES and
missing-word-timings failure modes. .claude and .agents mirrors stay
byte-identical.
Follow-up to #2669 (pre-build gate); paired with the v5 rebuild that
retrofitted captions onto the shipped Jul 13-20 video.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(skills): bump changelog caption size to 32px for phone-screen legibility
25px reads too small at 1080² when the video is viewed on a phone without
zoom. Bumped to 32px (font-size 25→32, top 1002→990, height 40→52, alpha
.92→.94) so captions carry across small-screen playback. Change is CSS-only
on the master-skeleton; existing per-scene chrome and layouts are untouched.
Retrofitted the Jul 13-20 v6 build with the same values (identical CSS
block) so the shipped video and the future default match. .claude and
.agents mirrors stay byte-identical.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(skills): sync caption position refs to top: 990 across SKILL.md + build-spec
Miga caught: the master-skeleton bump to top: 990 / 32px / height: 52 was
not reflected in the SKILL.md prose or build-spec.md. All three files now
name the same values so the doc, gate, and code stay consistent. Also
clarifies build-spec.md's caption-rail line to point at SKILL.md step 5 as
the source of the LINES-populated requirement.
.claude and .agents mirrors byte-identical.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(skills): sync script-voice.md caption rail to top: 990 / 32px
Third and final stale reference to top: 1002 / 25px — script-voice.md was
still stating the pre-bump values. Now matches master-skeleton.html,
build-spec.md, and SKILL.md. Also bumps the 'keep critical small text out'
guidance from ~80px to ~100px to reflect the slightly taller caption rail.
Full-directory sweep confirms zero remaining occurrences of 1002 or 25px
across .claude/ and .agents/ skill mirrors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* docs(changelog): weekly digest 2026-07-13–2026-07-20
* docs(changelog): embed the weekly video in the Jul 13-20 digest
Adds the Jul 13-20 changelog video (produced via the changelog-video
skill, hosted at static.heygen.ai) at the top of the Update block using
Mintlify's <Frame> component with a native <video controls>.
Video URL: https://static.heygen.ai/hyperframes/changelog-videos/weekly-changelog-jul13-20.mp4
Slack thread: C0ACCNHLG3U · 1784565857.520599