Commit Graph
4600 Commits
Author SHA1 Message Date
Miguel Angel Simon Sierra d71663ccf1 fix(studio): remove stale timeline model inputs 2026-09-21 10:45:53 -04:00
Miguel Angel Simon Sierra a4d7195933 fix(studio): restore automation disclosure state 2026-09-21 10:45:49 -04:00
Miguel Angel Simon Sierra 21f14338d1 fix(studio): remove inline keyframe expansion rows 2026-09-21 10:45:46 -04:00
Miguel Angel Simon Sierra 0757ed24d8 fix(studio): resolve provider rebase without clip auto expansion 2026-09-21 10:45:43 -04:00
Miguel Angel Simon Sierra e05aea6099 test(studio): remove obsolete expanded fixture assertions 2026-09-21 10:45:39 -04:00
Miguel Angel Simon Sierra d072ba17a3 fix(studio): restore audio disclosure state 2026-09-21 10:45:38 -04:00
Miguel Angel Simon Sierra 64d24108df refactor(studio): keep audio lanes while removing clip expansion 2026-09-21 10:44:36 -04:00
Miguel Angel Simon Sierra e5576faa65 fix(studio): keep automation lanes mounted without disclosure 2026-09-21 10:44:35 -04:00
Miguel Angel Simon Sierra 5f7263d1fe fix(studio): remove stale timeline test helpers 2026-09-21 10:44:34 -04:00
Miguel Angel Simon Sierra 079d50e4f3 test(studio): update timeline removal expectations 2026-09-21 10:44:33 -04:00
Miguel Angel Simon Sierra ec57f1da39 fix(studio): clear fallow findings in timeline cleanup 2026-09-21 10:41:44 -04:00
Miguel Angel Simon Sierra a4dea2e324 fix(studio): satisfy removal and format guards 2026-09-21 10:41:43 -04:00
Miguel Angel Simon Sierra 53a5a58685 test(studio): assert nested row identity by key 2026-09-21 10:41:42 -04:00
Miguel Angel Simon Sierra 5073869ab2 test(studio): remove expansion lane assertions 2026-09-21 10:41:42 -04:00
Miguel Angel Simon Sierra 369b4f3c9e test(studio): type nested timeline fixture broadly 2026-09-21 10:41:41 -04:00
Miguel Angel Simon Sierra ff4f966679 refactor(studio): remove expansion telemetry and prove stable nested rows 2026-09-21 10:41:40 -04:00
Miguel Angel Simon Sierra 044d91e49f fix(studio): keep timeline keyboard actor mounted 2026-09-21 10:41:39 -04:00
Miguel Angel Simon Sierra 69c603cf0d refactor(studio): keep legacy fixture state out of rendered rows 2026-09-21 10:41:39 -04:00
Miguel Ángel 0641921558 feat(studio): expose composable timeline parts (#4229)
* feat(studio): expose composable timeline parts

* refactor(studio): split timeline overlay parts

* fix(studio): resolve timeline fallow findings

* fix(studio): clarify timeline part contracts

* style(studio): trim timeline drag comment
2026-09-21 09:03:50 -04:00
Miguel Ángel 952e9228b5 fix(cli): bound capture runs and degrade WebGL navigation (#4215)
* fix(cli): bound capture runs and degrade WebGL navigation

* fix(cli): keep partial capture on deadline

* fix(cli): persist partial capture artifacts

* fix(cli): reduce capture phase complexity

* chore(cli): tighten capture comments

* fix(cli): restore post-capture phase calls

* fix(cli): parse extracted scripts before filtering

* fix(cli): remove next data through dom filter

* refactor(cli): move capture attempt into module

* refactor(cli): extract capture phases into modules

* refactor(cli): split navigation checks

* refactor(cli): deduplicate capture phase modules

* fix(cli): preserve bounded capture output on deadline

* fix(cli): secure capture artifact writes

* fix(cli): close remaining capture file alerts

* fix(cli): mark deadline captures unsuccessful

* refactor(cli): centralize capture phase schema

* style(cli): format capture watchdog test

* docs(cli): condense capture attempt header
2026-09-21 02:52:54 -04:00
Vance Ingalls c797f54542 chore: release v0.8.58 (#4242) v0.8.58 2026-09-20 20:14:03 -07:00
Vance IngallsandClaude Code 5a8ee663f7 fix(studio): report a rejected read as a network reason, not an open failure (#4240)
* fix(studio): report a rejected read as a network reason, not an open failure

`stage: "open"` is documented as `openComposition` threw — an unparseable
composition, or OOM. Every event in that bucket on 0.8.56 and 0.8.57 carries a
fetch-rejection message instead: "Failed to fetch" (28 users / 11 users), "Load
failed", "NetworkError when attempting to fetch resource.".

The cause is one missing catch. `readProjectFileOptional` awaits `fetch` with no
try/catch, so a rejected request escapes the function and is caught by the
effect's outer `.catch` — the handler written for `openComposition`. The read
path has a five-member reason union precisely so this class of failure is
diagnosable, and the one case that never produces a response was the case
outside it.

That made the largest SDK-session failure class in Studio (2.9% of session users
on 0.8.56, 2.3% on 0.8.57) unaddressable: you cannot fix a network failure while
it is reported as a composition that failed to parse.

Now `readProjectFileOptional` catches its own rejection and returns
`reason: "network"`, so it lands in `stage: read` with everything else, and
`stage: open` means only what its comment says.

Telemetry only. Nothing branches on the stage and no user-facing behaviour
changes. Fifth split of this shape, after `ineligible_operation` (v0.8.49),
`stage: read` (v0.8.51) and `block_detail` (#4214).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* test(docs): point the catalog tab-row test at the page gutter #4238 moved the inset to

`catalog-detail.test.ts` asserted the exact strings that #4238 deleted:
`.hf-ve-tabs-row { margin: 20px 0 0 16px; }` and `.hf-ve-body { padding: 2rem 0 0 16px; }`.
That PR removed both insets deliberately — its own comment says "the page gutter is
the page's job, and a second 16px here put everything below the stage on a
different left edge from the title, install block and stage above it" — and
restored the gutter on `#content-area` in `docs/custom.css` instead. The test was
left asserting the old mechanism, so main has been red on it since, unnoticed
because the `Test` workflow does not run for a docs-only change.

The invariant did not change: this content is still inset from the page edge. Only
the mechanism did, so the test now asserts both halves of it — the two rules carry
no inset of their own, and the gutter standing in for them is still present, at both
its values. Asserting only the first would keep passing if the gutter were deleted
outright, which is the regression the test exists to catch; both mutations were
checked and both fail.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-20 19:40:06 -07:00
Miguel Ángel c09594fef8 fix(docs): restore the sidebar gutter on catalog pages (#4238)
* fix(docs): restore the sidebar gutter on catalog detail pages

* fix(docs): align the catalog tab row and panes with the page gutter
2026-09-20 21:17:09 -04:00
Miguel Ángel b4787082b2 fix(studio): keep narrow timeline headers readable (#4232)
* fix(studio): hide narrow timeline header labels

* test(studio): cover narrow automation headers

* test(studio): cover narrow automation headers

* test(studio): assert narrow lane tooltip

* style(studio): format narrow header changes

* test(studio): cover narrow speed lane labels

* fix(studio): truncate timeline header names

* test(studio): remove stale header room plumbing
2026-09-20 19:49:29 -04:00
Miguel Ángel e86a530f0b fix(cli): update Puppeteer browser dependencies (#4233)
Bump @puppeteer/browsers to ^3.2.2 and puppeteer-core to ^25.10.0 so Chrome spawns detached only off Windows (upstream puppeteer #15415). Lock regenerated on top of main. Fixes #4028.
2026-09-20 18:08:16 -04:00
Miguel Ángel f92d017b99 feat(cli): add timeline editing verbs (#4221)
* feat(cli): add timeline editing verbs

* chore: satisfy fallow complexity gate

* test(cli): cover snap rounding at known fps

* test(cli): cover snap rounding at known fps

* test(cli): cover snap rounding at known fps

* test(cli): cover snap rounding at known fps

* fix(cli): derive split ids from stable refs

* fix(cli): remove timeline gate blockers

* fix(cli): close freshness and output gate gaps

* fix(cli): close remaining timeline review findings

* refactor(cli): simplify mutation decision flow

* refactor(cli): split trim and apply paths

* refactor(cli): isolate trim validation

* fix(cli): satisfy timeline lint gate

* style: format timeline mutation files

* fix(cli): preserve mutation result types

* fix(cli): close timeline receipt and nested bounds gaps

* fix(studio): rollback writes that fail during write

* fix(cli): bound moved clips by end time
2026-09-20 18:04:13 -04:00
Miguel Ángel 4ff4435e1d feat(studio): export player handle for hosts (#4227) 2026-09-20 17:47:35 -04:00
Miguel Ángel 2fb6f6987e test(studio): close shadow reload regression gaps (#4223)
* test(studio): close shadow reload regression gaps

* chore: refresh follow-up checks

* test: keep follow-up free of unused exports

* test(studio): distinguish root timeline selection
2026-09-20 17:15:29 -04:00
Vance Ingalls f09ea7af3b chore: release v0.8.57 (#4231) v0.8.57 2026-09-20 14:13:17 -07:00
Miguel Ángel e48eac7f5e feat(studio): track preview time to first frame (#4226) 2026-09-20 17:10:41 -04:00
Vance IngallsandClaude Code f476e78cc8 feat(studio): say which project this Studio is serving when a read 404s (#4228)
A tab whose project the running Studio cannot resolve failed every composition
read with a 404 and said nothing. `useSdkSession` recorded the failure to
telemetry and returned, so the editor looked normal while every edit silently
took the server path instead of the SDK — nothing landed, and nothing said
why. The `out-fb1` timeline sat in that state for an hour and a half, through
a reload that passed mount validation.

The banner names both sides: "This Studio is serving X. This tab was opened
for Y." That is the whole truth in the host that produces these 404s. The
CLI-embedded Studio serves exactly one project, and `hyperframes preview`
reuses port 3002 when the folder matches, so starting it on a different
folder takes the port out from under an open tab — and the project the old
tab was pointed at is untouched on disk. "No longer available" would be
false: nothing was lost, and the same timeline shows the project return.

With several projects served (the Vite host) a rename cannot be told from a
deletion, so that wording claims no cause.

`reportReadFailure` answers which project a failed read identifies. Only a 404
does; a 5xx, a dropped request, an unexpected body or an empty file say nothing
about which project the server serves, so none of them claim it.

Also fixes a smaller lie on the same path: `useFileTree` parsed the 404's JSON
body without checking `r.ok`, so the failure took the success branch and
recorded an empty tree — and an empty provenance snapshot for a project that
was never read — which then travelled out in bug reports.

The 404s continue; this ends the silent part, not the failure.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-20 13:53:52 -07:00
Vance IngallsandClaude Code 74af8b4279 fix(studio): validate the hash on hashchange, and stop pushing dead ids (#4225)
The mount path has validated its hash since v0.8.52. The hashchange path did
not — it adopted whatever id the URL offered, so changing projects walked
straight around the mount fix and re-armed the same failure: every later
/api/projects/<id>/... request 404s for the life of the tab, including the
composition read that opens the SDK session.

Same tri-state as mount: ok adopts, unknown adopts (a transient check failure
must not rewrite a hash that is probably fine), missing falls back.

Three details that are not obvious from the diff:

- **The race guard reads the hash, not a counter.** This effect re-registers on
  every projectId change, so a `generation` in that closure resets and an
  in-flight verdict from the previous registration would read as current —
  adopting a project the user already navigated away from. The hash cannot go
  stale, so it is the ground truth to compare against after the await.

- **The fallback re-fetches the project list.** tryConnect has `data.projects`
  in scope; this path does not, and caching the mount value would be stale in
  exactly the long-lived-tab case this exists for.

- **Both rewrites use replaceState.** `location.hash =` pushes, leaving the dead
  id one Back away — and Back would then re-validate, fall back, and push again,
  forever. The mount fallback had this bug too; fixed here as well.

Also emits the hashchange verdict so the `unknown` branch — which deliberately
keeps the hash and is invisible from outside — is countable.

Verified: 7 mutants, 7 caught, including adopting without validating, collapsing
unknown into missing, trusting the verdict over the live hash, memoizing the
fallback list, and pushing on either rewrite. Full studio suite 5,323 passed.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-20 13:53:31 -07:00
Vance IngallsandClaude Code 7abbe49638 feat(studio): a per-page tab id, and the hash-validation verdict (#4224)
Two telemetry gaps, both pure instrumentation with no behaviour change.

`tab_id` — distinct_id identifies a browser, not a page. Several
investigations stalled on "two tabs, or one tab switching?", because a burst
of events alternating between two project hashes is indistinguishable from a
user moving between projects, and `session_start` fires once per page load so
counting it cannot tell them apart either. One id per page load, from one
shared module, attached by BOTH transports (`studio:*` and `studio_*`) — those
two families already drifted once on how `agent_runtime` is encoded.

Deliberately not persisted. Storage would survive a reload and quietly turn
this into a tab-session id, changing what every query means, and blocked or
cleared storage would make the property intermittently absent — which reads as
"old client" rather than "storage unavailable".

`project_hash_validated` — a tab that keeps its hash because the check itself
failed ("unknown") is currently indistinguishable in production from one that
never ran the check. Different causes, different fixes, one silent bucket.
Emits `stage` (mount | hashchange), `outcome` (ok | missing | unknown), and
the HTTP status when the server answered.

Verified: 8 mutants, 8 caught — including dropping tab_id from either
transport, regenerating it per read, persisting it to storage, emptying it in
the no-DOM fallback, and collapsing "unknown" into "missing". Full studio
suite 5,314 passed.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-20 13:30:35 -07:00
Miguel Ángel ab6cbb383e refactor(studio): compose timeline parts through TimelineProvider (#4222)
* refactor(studio): add timeline provider boundary

* fix(studio): preserve timeline context schema

* refactor(studio): own timeline composition context

* fix(studio): complete timeline context ownership

* refactor(studio): keep timeline helper exports in Timeline

* refactor(studio): extract timeline provider state builders

* refactor(studio): isolate timeline provider branches

* refactor(studio): extract timeline overlay state

* refactor(studio): extract timeline overlay state hook

* fix(studio): keep overlay internals out of context
2026-09-20 15:38:27 -04:00
Miguel Ángel 243ecd1d10 feat(studio): compose preview overlay guides (#4220)
* feat(studio): compose preview overlay guides

* fix(studio): sync late preview iframe mounts

* fix(studio): make preview iframe ownership explicit
2026-09-20 14:37:50 -04:00
Miguel Ángel 85a1a8f789 fix(studio): swap the preview via a shadow iframe instead of blanking on reload (#4207)
* fix(studio): reloading the preview swaps in a ready copy instead of blanking

* fix(player): report painted once assets are ready and the loader has faded out

assetsready fires when the loader fade starts, so a consumer promoting on it can
expose a raised or fading panel. painted fires once per document load after the
fade completes, with a getter for late listeners; it resets on each load.

* fix(studio): promote the shadow preview only when painted, under one wait budget

Studio waited for ready plus two frames, which can land while the player's loader
panel is still up or fading. It now also waits for the player's painted event.
The shadow no longer arms the 5s adapter give-up, so a slow but live load fails
only at the single 15s budget. Set-destination availability follows the live
iframe across a swap.
2026-09-20 13:04:23 -04:00
Miguel Ángel 0c158be165 feat(studio): edit a clip's speed ramp with a rate lane and presets (#4153)
* feat(studio): edit a clip's speed ramp with a rate lane and presets

* fix(studio): keep the live preview attribute in sync with what gets persisted

* fix(cli): snapshot picks the source frame through a rate lane and up to 10x

hyperframes snapshot clamped the constant playback rate at 5 and ignored a
rate lane in data-automation, so a sped-up or ramped clip showed the wrong
frame. It now reads the rate through the runtime's own reader and maps time
with the shared source-time function.

* docs(skills): teach speed ramps as a rate lane in one reference page

Adds the speed-ramps reference, corrects the 0.1 to 5 rate range to 0.1 to 10
and the 'speed ramps are not supported' lines in the skills, and regenerates
the skills manifest.

* docs(skills): say what fixes a ramped clip's length and how the render approximates pitch

* fix(studio): hide the speed preset until the clip has a known duration

A preset on a clip with no data-duration was stretched over a made-up one
second. The panel now offers presets only when the duration is known.

* docs(skills): fix the last stale ramp line in the editing recipes and the slice-cap wording
2026-09-20 12:21:47 -04:00
Miguel Ángel fa699b7d9b refactor(studio): move playback adapter resolution into its own module (#4199) 2026-09-20 12:07:53 -04:00
Miguel Ángel e45483aa0a chore: release v0.8.56 (#4218) v0.8.56 2026-09-20 10:57:46 -04:00
Miguel Ángel 41b7202bca refactor(studio): make timeline rows use manifest elements (#4213)
* refactor(studio): make timeline rows use manifest elements

* fix(studio): preserve timeline consumer prop names

* test(studio): update asset row expectation

* fix(studio): clear preflight lint nit

* chore: allow intentional timeline hook removal

* chore: refresh PR capture checks

* test(studio): align visibility gate with store rows

* test(studio): preserve canEdit literal type
2026-09-20 10:18:03 -04:00
Miguel Ángel 84e6531668 feat(studio): a host can make the preview read-only (#4200)
* feat(studio): a host can make the preview read-only

Gates the shared geometry commit funnel (offset/size/rotation), so the
inspector's manual X/Y/W/H/rotation fields refuse the same as canvas
drag/resize/rotate. The flat inspector's fields render disabled with a
host-supplied reason. Fixes a hooks-order crash on deselect found in review.

* refactor(studio): derive the read-only preview flag from context, not a synced store

EditorShell synced its readOnlyPreview prop into a zustand store via a raw
useEffect behind an eslint-disable of no-restricted-syntax. Replace it with
a PreviewReadOnlyProvider context computed fresh from the prop every render:
Tier-1 consumers (genuine EditorShell descendants) read it via useContext;
Tier-2 consumers (useAppHotkeys, useGestureCommit, the
useDomEditSession/useDomEditCommits/useDomGeometryCommits chain), which run
in App.tsx above EditorShell, take it as an explicit parameter threaded from
StudioApp's own prop instead. Adds a host-mount test proving EditorShell
gates its descendants from its own prop with no StudioApp/App.tsx involved.

* fix(studio): keep preview hotkeys context explicit

* fix(studio): close read-only interaction escapes

* fix(studio): preserve timeline ownership in read-only preview

* fix(studio): rollback read-only preview work

* fix(studio): keep hotkey ownership on the active surface

* fix(studio): format preview overlay and strengthen split test

* fix(studio): clear preview gate findings

* refactor(studio): share preview test fixtures
2026-09-20 10:10:57 -04:00
Miguel Ángel 9bd6da848f feat(studio): theme timeline colours with CSS tokens (#4211)
* feat(studio): the timeline's colours read CSS custom properties instead of literals

The timeline's surfaces, hairlines, ruler text and clip colours were hard-coded
hex/rgba, so a host with a light chrome saw a black slab. The 24 values move
into new --timeline-* tokens in theme.css (the same mechanism Button and Menu
already use), holding today's exact values, so a host themes the timeline by
overriding a token instead of the object staying opaque.

* fix(studio): route timeline clip colours through theme tokens

* chore(studio): bank the colour-literal baseline

* fix(studio): preserve timeline clip token defaults

* fix(studio): increase default timeline clip contrast

* fix(studio): keep timeline hover brighter

* test(studio): stabilize property panel flag coverage
2026-09-20 09:38:38 -04:00
Vance Ingalls 96369c8acd chore: release v0.8.55 (#4216) v0.8.55 2026-09-20 00:45:18 -07:00
Vance IngallsandClaude Opus 5 05d5abb926 feat(studio): a blocked GSAP edit records which of seven causes it hit (#4214)
`edit_blocked` is the largest interaction failure in Studio: 575 users in 14
days, 17% of everyone who touches an editing control, and 41% of them are
blocked on their first or second attempt. Only 30% ever commit anything
afterwards.

Almost all of it is one message. "This animation is computed at runtime. Edit
the animation in the Code tab." covers ~460 of those users and is thrown from
seven different places — runtime-dynamic provenance, unresolved keyframes, an
unresolved selector, a geometry commit over unresolved source, and three
live-tween-without-a-source-match cases across position and rotation. The
telemetry carried only that string, so the classes were indistinguishable.

The copy is also only literally true for the first. The rest are parser or
source-matching limits where the animation may be plain authored source, and
we send the user to the Code tab to rewrite something that could be fine.

Adds a `detail` beside the coarse reason, set at each of the seven origins and
emitted as `block_detail`. Telemetry only; nothing branches on it, and the
user-facing copy is unchanged. Which cause dominates decides whether the fix
is better copy, a parser gap, or an Unroll-style recovery path — and guessing
that without the split is what this is meant to avoid.

`directEditOutcomeForProperties` rebuilt blocked outcomes from `reason` alone,
so it dropped the detail for the three causes that flow through it; it now
carries it. Mutation testing caught that, not review.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:42:37 +00:00
Miguel Ángel c204b3c757 chore: release v0.8.54 (#4212) v0.8.54 2026-09-20 00:22:52 -04:00
Miguel Ángel 5262062e7b feat(studio): export timeline editing for a host outside EditorShell (#4201)
* fix(studio): undo waits for in-flight hand edits before consuming history

* docs(studio): trim comments to the 3-line house rule

* feat(studio): export timeline editing, undo/redo, conflict banner and TimelinePane for a host

* docs(studio): trim comments to the 3-line house rule

* refactor(studio): simplify exports pass

* docs(studio): trim comments to the 3-line house rule

* fix(studio): resolve canEdit targets against expanded timeline rows

* docs(studio): trim comment to the 3-line house rule

* fix(studio): guard razor-split-all and the audio-group attribute write

* fix(studio): gate the audio-group attribute for sub-composition-only members

* fix(studio): keep blocked timeline edits truthful

Refresh cached permission checks from the current host policy, gate auto-grouping before live patches, and restore optimistic attribute previews when a guarded commit is refused.

* fix(studio): publish desktop compatibility exports

Expose range selection and the canonical loader from the built Studio package while retaining timeline editing seams.

* fix(studio): gate timeline drop writes

Route asset, file, and composition drops through the shared edit permission gate before any side effect.

* fix(studio): preserve release-owned style exports

* fix(studio): clear file and Fallow gates

* fix(studio): keep the carve gate and range slice under the size budgets

* style(studio): format playerStore
2026-09-19 23:59:13 -04:00
Vance Ingalls d11907c325 chore: release v0.8.53 (#4210) v0.8.53 2026-09-19 20:30:29 -07:00
Vance IngallsandClaude Fable 5.1 83624d193e fix(studio): stop counting extension-leaked AbortError rejections as errors (#4209)
`studio:unhandled_promise_rejection` was dominated by two AbortError rows
("signal is aborted without reason", "The user aborted a request."), about
10,000 events across 265 users per week, every one with a stack rooted in
the thumbnail scheduler's `release()`.

The scheduler is correct: every promise on its abort signal is awaited, and
a Chrome 153 reproduction with the real bundle confirms that releasing a
lease mid-fetch produces no unhandled rejection on its own. The leak comes
from browser extensions that wrap `window.fetch` in the main world
(Requestly and Poper Blocker are two seen in field stacks) and derive a
promise from each request without a rejection handler. Chrome rejects that
derived promise with our abort reason, so the stack points at `release()`.
Injecting such a wrapper reproduces the field rows exactly. Among users
with both this leak and a "Failed to fetch" error, 69 of 72 have an
extension frame inside `window.fetch`; among the rest, 13 of 308.

An AbortError rejection is a cancellation we asked for, so record it as a
sampled `abort_rejection_filtered` counter instead of an error. The filter
sits before the ERROR_CAP check: a scroll burst previously spent the
50-event cap in under a second and dropped every real rejection for the
rest of the session (116 users hit the cap in the last 7 days).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 19:51:00 -07:00
Vance IngallsandClaude Opus 5 90e67ff5a8 fix(release): a stale local tag can no longer silently widen the changelog (#4208)
* fix(release): a stale local tag can no longer silently widen the changelog

`release:prepare` creates `vX.Y.Z` locally at the branch commit, then the
publish workflow creates the same tag at the merge SHA. After a squash merge
those are different commits and the local one is not an ancestor of main, so
`git describe` cannot reach it and falls back to the previous release without
saying so. `git fetch --tags` does not help: it refuses to move a tag that
already exists locally, and only `--force` does.

On the v0.8.52 cut this produced a `v0.8.50...v0.8.52` draft that re-listed
about a hundred already-shipped commits. The changelog is user-facing, and
nothing in the output said the baseline was wrong.

Adds a guard in the drafter: after resolving the baseline, fail if a stable
tag exists locally that sits strictly between it and the release. `describe`
only ever returns a reachable tag, so a tag in that range is proof it skipped
one. The message names the tags and prints `git fetch origin --tags --force`.
`--from` still overrides, for a deliberate baseline.

Also prints the captures-gate hatch with the release PR instructions. Every
release bumps packages/studio and packages/player, which
scripts/check-pr-captures.mjs watches, so it demands before/after media for a
version bump; its own `## No visible change` section is the intended answer
and applies to a 4-line package.json diff.

Verified end to end by moving a local tag onto the pre-squash commit of
v0.8.51: the drafter fails with the fix, and drafts normally once restored.
Six mutants of the range check are all caught by the tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(release): guard the baseline inside its resolver

`resolvePreviousTag` only runs when `--from` was not supplied, so testing
`!options.from` at the call site repeated a condition the `??` already
expressed. Folding the guard into the resolver drops that branch, which had
pushed `createDraft` to a CRAP score of exactly 30.0 and failed the audit.

The assert sits outside the try/catch on purpose: `fail` exits rather than
throwing today, and this keeps a catch from ever swallowing the guard if that
changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 17:14:51 -07:00
Miguel Ángel 2d84937e1f feat(studio): export the header logo, button sizes and a stable styles.css subpath (#4197)
A host mounting Studio's header reuses HyperframesLogo and buttonSizes for
matching chrome, and imports the compiled stylesheet as a fixed-name package
export instead of a hash that changes on every rebuild.
2026-09-19 19:06:01 -04:00