Commit Graph

1198 Commits

Author SHA1 Message Date
heygen-rui-bot 3539d72ddf codegen: resync cloud client from EF 65ee22fc 2026-09-08 03:06:08 +03:00
Miguel Ángel 30d6f43bdb chore: release v0.8.31 (#3747)
* chore: release v0.8.31

* docs(release): describe the range fix on its own terms
2026-09-07 12:12:35 -04:00
Xuanru Li c59fd162aa fix(check): key content_overlap and text_occluded collapse by pair, and bill a clipped text box once (#2801)
Combines two fixes to the layout audit:

- `staticIssueKey` no longer includes `issue.text` for `content_overlap`
  and `text_occluded`; both builders now emit a unique selector for both
  ends, so the selector pair is the identity. An animating subject
  (count-up, typewriter, rotating word) no longer splits one held
  collision into transient groups.
- A clipping box that is its own nearest constraint reports
  `clipped_text` only, instead of also reporting `text_box_overflow`.
  A non-clipping self-constraint still reports.
2026-09-06 18:54:18 -07:00
Xuanru Li 39ad2721b3 fix(check): keep auditing when a motion spec is partly unusable (#2805)
Combines two fixes to the motion sidecar path:

- An ambiguous selector no longer aborts the whole spec. It is reported
  as a finding, the assertions that depend on it are skipped and named,
  and every other assertion still evaluates. `assertionTargets` is now
  exhaustive over the assertion kinds so a new kind cannot silently fall
  back to an arbitrary first match.
- A sidecar that will not parse is reported as a spec finding instead of
  ending the run, so the composition is still audited.
2026-09-06 18:02:21 -07:00
Miguel Ángel 3874990449 chore: release v0.8.30 (#3733) 2026-09-05 23:14:12 -04:00
Miguel Ángel 672ea844a5 fix(cli): clarify publish visibility and claim links (#3730)
* fix(cli): clarify publish visibility and claim links

* style(cli): apply oxfmt to publish visibility test

* chore(skills): regenerate skills manifest for updated references

* fix(cli): stop an in-place re-publish claiming it made the project private

* test(cli): pin in-place visibility copy to the plain re-publish route
2026-09-05 23:10:42 -04:00
Miguel Ángel 1fa31d5dfb feat(cli): keep every icon a site declares and headline the bare mark (#3727)
A brand band shows logo artwork on a transparency checker, where a badge (a
mark knocked out of a full-bleed disc or square) reads as a solid blob. The
capture kept only the first icon that downloaded, so which of a site's icons
survived was decided by ranking alone, and nothing recorded what the survivor
actually looked like.

Keep them all. Each declared icon is written as assets/icon-<rel>-<sizes>.<ext>
and classified as bare-mark, badge, or unknown, then the best one is copied onto
the historical assets/favicon.<ext> stem so a stem match still finds it.

Classification is deterministic and cheap. SVG is read from markup, not
rendered: these files routinely fill via CSS custom properties behind a
prefers-color-scheme query, and librsvg resolves neither, rasterising one real
favicon to a fully transparent image while Chrome draws it correctly. Raster
icons are sampled at the midpoint of each edge rather than the corners, because
a full-bleed disc has four transparent corners and would otherwise read as a
bare mark.

The headline prefers a bare mark, then falls back to the existing ranking.
Preference is binary on purpose: an "unknown" is not promoted above a badge,
or an undecodable .ico would outrank a good SVG purely for being unexaminable.

extracted/icons-manifest.json records every icon, its shape and the evidence
for that shape, plus which file became the headline, its rank, and why. A
choice whose losers are invisible is indistinguishable from having had no
choice, which is how a substituted icon went unnoticed in the first place.

Worth noting for consumers: sites often declare no bare mark at all. An
apple-touch-icon is composited onto an opaque tile per Apple's spec, so it is
a badge, not a mark. The manifest says so rather than implying the preference
was satisfied.
2026-09-05 22:35:45 -04:00
James Russo 97fde27df7 fix(cli): pin Studio bundle, signature and runtime file reads (#3728)
* fix(cli): read Studio bundle files through checked descriptors

* fix(cli): pin Studio signature and runtime artifact reads
2026-09-05 22:23:15 -04:00
Miguel Ángel 7a07ea9ac3 fix(cli): fetch a page's assets as the same agent that loaded the page (#3726)
A capture is one session with two halves: Chrome navigates the page with a
browser User-Agent, then Node fetches the assets that page referenced. Those
halves sent three different identities — "HyperFrames/1.0" from the asset and
media downloaders, a bare "Mozilla/5.0" from the stylesheet inliner, and the
real Chrome UA from the navigation itself.

An origin is free to answer those differently, and anti-bot edges do. Capturing
one large site, GET /favicon.svg answers 403 text/html to "HyperFrames/1.0" and
200 image/svg+xml to the UA the very same capture had just navigated with. The
favicon ranker had already picked that SVG as the best declared icon; the 403
discarded it and the downloader fell through to the next candidate, so the icon
written to assets/ was chosen by the CDN's bot rules rather than by the ranker.
The capture reported it as one "unavailable" drop and carried on.

Hoist the navigation UA into CAPTURE_USER_AGENT and use it for every
out-of-band fetch the capture makes: favicons, images, og:image, fonts,
stylesheets, Lottie JSON and videos. One constant is what stops the two halves
drifting apart again.

Verified end to end against that site: before, assets/favicon.png (the
apple-touch icon) plus one unavailable drop; after, assets/favicon.svg, byte
identical to the file the site itself serves.
2026-09-05 22:20:15 -04:00
James Russo 41551c7acb fix(cli): read caption images through checked file descriptors (#3724) 2026-09-05 20:43:36 -04:00
James Russo cfdaccb7e6 fix(cli): publish cached synthesis script exclusively (#3722) 2026-09-05 20:05:15 -04:00
James Russo b49fba8dea fix(cli): preserve concurrent scaffold config creation (#3721)
* fix(cli): preserve concurrent package metadata during init

* fix(cli): preserve concurrent project config creation
2026-09-05 19:35:41 -04:00
James Russo e6d2816e1d fix(cli): publish capture metadata without overwriting files (#3720)
* fix(cli): create capture metadata exclusively

* fix(cli): publish capture metadata without following links
2026-09-05 18:47:53 -04:00
miga-heygen ae3d80c30f chore: release v0.8.29 (#3690) 2026-09-04 21:50:46 -04:00
miga-heygen 00c575d23b fix(studio): prevent preview hang on burst external file rewrites (#3648)
* fix(studio): prevent preview hang on burst external file rewrites

Two interacting bugs caused Studio to freeze when multiple processes
(generator, check, snapshot) burst-wrote index.html within seconds:

1. SSE listener leak: the /api/events handler added a watcher listener
   per client connection but never removed it on disconnect. Reconnects
   accumulated dead listeners, each triggering readFileSync on every
   file change and writing to closed streams.

2. Generation starvation: processChange incremented generationRef and
   awaited drainPendingChanges. A second event arriving mid-drain bumped
   the generation, causing the first drain to bail at the generation
   check. With rapid writes, no drain ever completed and Studio stayed
   frozen on stale content.

Fix 1: use stream.onAbort() to remove the watcher listener when the
SSE connection closes.

Fix 2: gate processChange with a draining ref. While a drain is in
progress, stash the latest event. On completion, process the stashed
event — the last write in a burst always completes its reload.

Closes #3646

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

* fix(studio): align coordinator tests with drain serialization

Update existing test to expect the new behavior: when two events
fire in quick succession, the first drain completes and triggers a
reload (previously it was silently discarded). The stashed event
then starts a second drain.

Also fix the burst-write test to use the drains array pattern and
explicit act() flushes for stashed event processing.

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

* fix(studio): stash events with allowDuplicate and harden listener cleanup

Address Rames's review findings:

- Stash with allowDuplicate: true so re-dispatched events are not
  swallowed by the duplicate guard (the identity was already written
  on the way in, so the stashed event matched itself on re-entry).
- Wrap SSE keepalive loop in try/finally so the listener is removed on
  both abort and throw, not just abort.
- Restore stale-completion guard test coverage lost in the rename.
- Use await act(async () => {...}) for burst dispatches so assertions
  depend on the stash guard rather than scheduling.

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

* fix(studio): simplify drain serialization and restore SSE cleanup

Restructure processChange into intake + drain loop:

- processChange is now synchronous — validates, dedupes, checks own
  echoes, enqueues the accepted payload, and starts the drain loop
- startDrainLoop runs while the pending slot is non-null, draining
  one event per iteration via drainOnePending
- No recursive void processChange(...) from finally, so no
  allowDuplicate escape hatch needed — stashed events never re-enter
  intake guards

SSE listener: restore stream.onAbort alongside try/finally. Hono's
sleep() never throws, so finally alone doesn't fire on disconnect.
Both paths call removeListener (Set.delete is idempotent).

Tests: remove stale-drain test that contaminated subsequent tests by
emptying the shared roots array mid-test. Use sync act() for burst
dispatches — the stash decision is synchronous.

All 10 coordinator tests pass locally (NODE_ENV=test).

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-09-04 21:44:25 -04:00
Miguel Ángel 64ce9fdf1f chore: release v0.8.28 (#3689) 2026-09-04 21:01:08 -04:00
James Russo 9ed50028c4 fix(capture): sample computed shadows on ordinary boxes (#3686)
Co-authored-by: Xuanru Li <157947275+xuanruli@users.noreply.github.com>
2026-09-04 20:28:17 -04:00
James Russo a7cdd583e7 fix(capture): retain opaque and glass button styles (#3685)
Refresh the capture fixes proposed in #1880 with browser-backed regression coverage.

Co-authored-by: Xuanru Li <157947275+xuanruli@users.noreply.github.com>
2026-09-04 20:28:07 -04:00
James Russo 44208ceada docs(cli): document current publish workflow (#3674)
Refresh the README guidance from #1950 for current auth, visibility and updates.

Co-authored-by: yoma <yingwaizhiying@gmail.com>
2026-09-04 19:52:54 -04:00
James Russo 36ec028d5f fix(cli): forward GIF and frame-format options in batch renders (#3666)
Port Dustin Persek’s fix and command regression from #1833 onto current main.

Co-authored-by: Dustin Persek <dustin.persek@protonmail.com>
2026-09-04 19:30:50 -04:00
James Russo d2741b3a28 fix(studio-server): preserve binary file writes and versions (#3653)
* fix(studio-server): preserve binary file writes and versions

* fix(studio-server): rely on exclusive file creation for POST

* test(studio-server): create race fixture atomically
2026-09-04 17:44:19 -04:00
Miguel Ángel 19a512c05a fix(cli): count font axes as motion in the sweep fingerprint (#3107)
The layout audit fingerprints each element by rect and opacity, plus an
8x8 pixel hash for canvas and video. A composition whose only change over
the timeline is font-variation-settings touches none of those, so all six
samples hash identically and the audit reports sweep_static on a timeline
that is visibly animating.

Duplexed variable fonts make it unavoidable rather than unlikely. Recursive
holds one advance width across its whole weight axis by design, so even the
element rect stays fixed while the letterforms change.

Fold the computed font-variation-settings into the per-element fingerprint.
It reads normal on every element that does not use the property, so no
existing composition's fingerprint changes.

This is the third instance of this failure class in the same function,
after the canvas-repaint and opacity-reveal cases whose regression tests
already sit alongside these. Two more join them: a scene where only the
axes move must change the fingerprint, and a scene where nothing moves
must hash identically twice. The second is the load-bearing one, since a
fingerprint that varies on its own would make sweep_static unfireable and
every green layout verdict meaningless.
2026-09-04 18:07:32 +00:00
Miguel Ángel e147613542 fix(cli): upgrade project wrapper pins (#3042) 2026-09-04 18:06:51 +00:00
Miguel Ángel 709d70c675 fix(cli): render SVG selector proof strips (#3537) 2026-09-04 18:06:49 +00:00
Miguel Ángel 2dc4bbfa75 feat(cli): forward a reported catalog gap to the feedback channel (#3204)
A search miss already reached PostHog, where nobody was watching it. It now
also goes to the same channel a CLI rating goes to, by the same best-effort
POST, so a move the catalog is missing is read rather than queried.

The forward is bounded and swallowed, and the ack prints before it, so a gap
report can never fail or delay the command that sent it.
2026-09-04 18:06:36 +00:00
Miguel Ángel faaafbb251 fix(cli): split Kokoro text on token overflow (#3041) 2026-09-04 18:06:15 +00:00
Miguel Ángel a3954aa1cb fix(cli): persist what the server answered for a captured page (#3553)
* fix(cli): persist what the server answered for a captured page

A page that renders is not a page that succeeded. An error page has a title,
a palette, typefaces and a DOM, so every extractor downstream reads it happily
and produces a design system belonging to whoever wrote the error page rather
than to the site's owner.

The status was already read, once, to feed `detectBlockedPage`, and then
dropped. That helper cannot stand in for it: it decides whether the rendered
document LOOKS like a protection wall, over a minimal-DOM heuristic that a
rich error page passes. "Was this blocked?" and "what did the server answer?"
are two questions, and widening the first to carry the second would leave a
heuristic owning a fact.

So the response status is persisted plainly, as its own record, and every
consumer decides for itself what a non-success response means for its product.
Written before the blocked-page check runs, so the record's absence means
"navigation never produced a response" — a third state distinct from a status
of 404 and from a status of null.

* fix(cli): report the response status in the capture command's JSON output

The status reached `CaptureResult` and was then dropped at the CLI boundary,
which is the same read-once-and-discard that made the error page harvestable
in the first place. `--json` is the documented programmatic surface, and an
agent reading `ok: true` off a capture of a 404 has no way to see it there.
2026-09-04 18:03:35 +00:00
WaterrrForever c8300c69a1 fix(cli): install the published skill set, not every SKILL.md in the repo (#3636)
Bare `hyperframes skills` passed `--skill '*'` to the upstream installer, which discovers every SKILL.md in the clone, so a full install put 26 skills on disk against the 20 the manifest publishes (the six repo-internal ones under .claude/skills / .agents/skills). It now resolves the canonical manifest and installs its names, the same path `skills update` uses; offline it warns and degrades to the pinned core set instead of the wildcard. Docs point the full set at `npx hyperframes skills` and say what `skills add --all` really pulls.
2026-09-04 23:25:43 +08:00
WaterrrForever c5224ac7e3 feat(cli): snapshot --against pairs each frame with a reference video (#3637)
Rebuilding footage means checking the render against the source at the same timestamps. `--against <video>` saves the reference frame at every captured time (frame-exact seek: -ss after -i) plus a labelled render|reference pair sheet built with the existing contact-sheet helper. Reference frames and pair sheets are derived artifacts, kept out of the snapshot list like contact-sheet.jpg. No new dependencies.
2026-09-04 23:25:36 +08:00
Miguel Ángel e0007b86e0 fix(render): allow resolution scaling for alpha output (#3634) 2026-09-04 10:28:56 -04:00
Miguel Ángel 19ab83f929 chore: release v0.8.27 (#3608) 2026-09-03 00:27:31 -04:00
Miguel Ángel 7dc31bd2cc fix(cli): score apple-touch-icon-precomposed at the 180px default (#3607)
declaredSize() matched the rel token "apple-touch-icon" exactly, so the
legacy "apple-touch-icon-precomposed" spelling (one token, not two) fell
through to 0 instead of the 180px Apple default, even though the selector
already collects it. Same page, one spelling apart, opposite winner inside
tier 1 — it never drops a candidate and never beats the .ico tier.

Switch to a startsWith check on the token. Exact-token matching stays for
mask-icon, where a longer rel really would be a different asset.
2026-09-03 00:21:05 -04:00
Miguel Ángel 5a5e841c5d fix(cli): pick the highest-quality favicon and keep its transparent background (#3606)
* fix(cli): capture the best declared favicon, not the first one

Pages routinely declare a legacy 16px .ico first and the good asset (an SVG,
or a 180x180 apple-touch PNG) after it. The capture's page evaluate kept only
{rel, href} and the download loop took whichever candidate fetched first, so
the .ico won on every such page. The dropped sizes/type attributes are the only
evidence of quality: page.html on disk does not keep the <link> tags and only
the winner's bytes are fetched, so the choice was unrecoverable downstream.

Keep sizes and type, and rank candidates before downloading: SVG first, then
the largest declared size (an unsized apple-touch-icon counts as 180), then
.ico. The loop still falls through to the next candidate when one fails to
download, so the ranking changes which icon wins, never whether one lands.

Ranking is a pure function over the declared attributes, unit-tested against
the link shapes three sites actually publish.

* fix(cli): rank a pinned-tab mask-icon below real favicons

`link[rel*="icon"]` also matches `rel="mask-icon"`, Safari's pinned-tab
asset: a single-colour silhouette drawn in a browser-chosen tint, not the
site mark. It is served as an SVG, so ranking by format alone promoted the
outline above the page's actual colour favicon.

Give mask-icon its own lowest tier rather than dropping it, so a page that
declares nothing else still lands an icon instead of none.
2026-09-02 23:53:45 -04:00
Miguel Ángel ae7e530f2d feat(cli): capture reports why a referenced asset is not in the folder (#3598)
Capture drops assets for four reasons and reported none of them, so a folder
with thirty images and a folder truncated to thirty images were the same
object. Every drop site was a bare `continue`, `break`, `return null` or an
empty `catch`, and the only signals downstream were two hand-written warning
strings that fired when the budget was already gone before a download pass
started, which is the one case where the pass could not say how much it lost.

`downloadAssets` and `downloadAndRewriteFonts` now return an `AssetDropCounts`
tally beside their result, incremented at the single line that performs each
drop. `capture --json` carries it as `dropped`; the human summary prints a
`Dropped:` line when it is non-zero.

Four reasons, three decisions and one failure:

  size-floor        fetched, then judged too small to be a real asset
  budget-exhausted  the post-navigation clock ran out before this one
  cap-reached       30 inline SVGs, 30 fonts, or 6 faces per family
  unavailable       the request or the write failed

A break now counts everything it did not reach rather than the one it stopped
on, because "how many did we lose" is the question and one is never the answer.

The two budget warnings are gone. Both existed only to cover the case where
the budget ran out before a pass was called, so both passes are now called
unconditionally: a zero budget makes each loop break on its first item and
record `budget-exhausted` for the rest, which costs no network and produces a
real number instead of the word "some". The single remaining warning is derived
from the tally, so the prose and the count cannot disagree.

Measured on a live capture of a large marketing site:

  default budget    232 kept, 91 dropped (39 size-floor, 20 cap-reached,
                    32 unavailable)
  15s budget         30 kept, 299 dropped (279 budget-exhausted, 20 cap-reached)

Same page, same command, and until now both runs described themselves the same
way.
2026-09-02 23:24:59 -04:00
Miguel Ángel 84ed587f33 chore: release v0.8.26 (#3597) 2026-09-02 10:36:01 -04:00
Miguel Ángel 6b360f56f7 chore: release v0.8.25 (#3595) 2026-09-02 01:29:06 -04:00
James Russo aceaaebd68 chore: release v0.8.24 (#3593) 2026-09-01 21:54:42 -04:00
Miguel Ángel 6cbe3fbe90 chore: release v0.8.23 (#3586) 2026-09-01 13:58:14 -04:00
WaterrrForever 8eea3c913f fix(capture): let vision captioning authenticate the way a server can (#3561)
* fix(capture): let vision captioning authenticate the way a server can

Three defects in one phase, all of which end with a capture that reports
"Captioned N/N images" and then "0 images captioned with Gemini" — a
successful-looking run that hands the agent nothing to see by.

1. Credential. The captioner only accepted an API key. A server deployment
   holds a service account, not a key, and a rejected key is indistinguishable
   from an unset one here: every request returns empty text and no error. Vertex
   is now a first-class provider, ranked above the bare key and below an explicit
   OPENROUTER_API_KEY opt-in, configured by HYPERFRAMES_VERTEX_SERVICE_ACCOUNT +
   HYPERFRAMES_VERTEX_PROJECT_ID (region via HYPERFRAMES_VERTEX_LOCATION). It
   carries its own model default because the Gemini API's flash-lite preview id
   is not resolvable on Vertex.

2. Empty captions. Thinking tokens are drawn from maxOutputTokens, so a model
   left free to think can spend the whole budget and return no text — a
   successful request with no caption. thinkingBudget is pinned to 0; a one-line
   factual caption needs no reasoning.

3. Native abort. Rasterizing a batch of SVGs concurrently drove up to SVG_BATCH
   simultaneous librsvg renders through libvips and corrupted the heap:
   `free(): unaligned chunk detected in tcache 2` (SIGABRT) during this phase,
   twice in fourteen days, losing the whole capture each time. A native abort
   cannot be caught, so the concurrency is removed rather than handled —
   rasterization is serialized and libvips' worker pool is bounded, while the
   vision requests, which are the slow leg, stay parallel. Throughput barely
   moves: 225 captions across three real captures, 0 failures, 13-25s each.

* test(capture): pin the rasterization loop to one render at a time

The serialization fix shipped without a regression test on the grounds that native
heap corruption is not unit-testable. The corruption is not, but the property that
prevents it is: `sharp` is mocked to record how many renders are in flight, and a
six-SVG batch must never reach two. A deliberately slow caption stub makes
overlapping renders the faster path, so a future refactor that "optimises" the loop
back to `Promise.all` fails here instead of aborting in production.

Also covered: `sharp.concurrency(1)` is applied — serializing the loop while leaving
libvips' pool at the host core count still fans one render across every core — and an
unrasterizable SVG is skipped without breaking serialization for its siblings.

Verified as a real guard: reverting only contentExtractor.ts to origin/main fails 7 of
the 22 cases in this file.


* fix(capture): tell the truth in the asset-descriptions header when Vertex captioned

The provider gate in `contentExtractor` accepts Vertex when a project and a
service account are both set -- which is the configuration a server
deployment actually has. The header written next to the captions still
tested only for an API key, so a capture whose captions Vertex had just
generated was labelled "GEMINI_API_KEY not set -- descriptions below are
catalog-derived".

That header is not cosmetic: it travels into the context the template
editor reads, telling it to distrust captions that are real.

Mirror the same two variables here, and name every provider in the fallback
text instead of only the API key.

* fix(capture): hand libvips' worker pool back after the renders

`sharp.concurrency(1)` is process-global and was set once, for the whole life of
the process. The bound is right for the rasterize loop -- a native abort in
libvips cannot be caught, so the renders must not overlap -- but its scope was
every later sharp caller in the process, none of which asked for captioning, all
of them pinned to one thread from then on.

Now the host's value is read first and restored in a `finally` around the
rasterize loop, so a skipped SVG cannot cost the process its threads either. The
vision requests below are network work and gain nothing from a pinned pool.

The mock had to grow the getter half of sharp's API -- `concurrency()` with no
argument reports the current value -- since save-and-restore is untestable
without it. Verified as a real guard: dropping only the restore fails both new
cases.

Raised by Rames Jusso in review of #3561 and concurred by Magi.
2026-09-01 23:34:59 +08:00
Xuanru Li 45cc343525 fix(cli): flag caption-zone by DOM box overlap (#3580)
A card centered at y=.860 can still cover the painted V2A pill. Intersect the element's getBoundingClientRect with the keepout instead of testing whether its center sits inside the band.
2026-09-01 04:41:57 +00:00
Miguel Ángel 38e356fba4 chore: release v0.8.22 (#3575)
* chore: release v0.8.22

* docs: include encoder retry in v0.8.22 notes

---------

Co-authored-by: James <james.russo@heygen.com>
2026-08-31 22:54:13 -04:00
heygengenesis[bot] 9097d539b1 fix(cli): hide Windows child process consoles (#3529)
Rebase #3529 onto current main. Preserve all 16 issue-scoped Studio server, lint, and CLI child-process windowsHide options, including main's PowerShell null guards and stderr suppression in orphanCleanup.

Regression tests continue to assert windowsHide at each scoped spawn site. #3476 and #3430 remain out of scope.

Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
2026-08-31 18:11:20 -04:00
miga-heygen 73aa71c9ec fix(cli): guard PowerShell process queries against exited PIDs (#3571)
## Summary

`processIdentity` and `processParentPid` call `Get-CimInstance Win32_Process` to look up process metadata on Windows. When the target process has already exited, `Get-CimInstance` returns null and calling `.CreationDate.ToFileTimeUtc()` or `.ParentProcessId` on it throws `InvokeMethodOnNull`. The try/catch handles it, but PowerShell writes the error to stderr, which pollutes the test runner's output and causes spurious exit code 1 on Windows CI.

Two fixes per call site:
- Null-check the CimInstance before accessing properties (`$p = ...; if ($p) { $p.Property }`)
- `-ErrorAction SilentlyContinue` + `stdio: ["pipe", "pipe", "ignore"]` to suppress any residual stderr

Fixes the recurring `Tests on windows-latest` flake on main.

## Test plan

- [x] All 196 CLI test files pass locally
- [ ] Windows CI should no longer exit 1 from PowerShell stderr noise

— Miga
2026-08-31 17:15:39 -04:00
Miguel Ángel f3099dcb27 chore: release v0.8.21 (#3570) 2026-08-31 15:16:22 -04:00
Miguel Ángel 724796e2f0 chore: release v0.8.20 (#3555) 2026-08-30 00:31:08 -04:00
Miguel Ángel 0fd70b1d21 chore: release v0.8.19 (#3551) 2026-08-29 13:58:33 -04:00
Miguel Ángel 5cc2f1bef5 chore: release v0.8.18 2026-08-29 15:38:26 +00:00
Miguel Ángel d99eeef0b2 fix(cli): honor authored playback rate in snapshots (#3536) 2026-08-29 02:48:33 +00:00
Miguel Ángel e4dabf830c fix(cli): prevent keyframe shots overwriting sources (#3534) 2026-08-29 02:43:37 +00:00
Miguel Ángel b28747df0f fix(cli): respect timeline-free static compositions (#3533) 2026-08-29 02:43:33 +00:00