fix(skills): make hyperframes package version resolution non-fatal and overridable#1825
Conversation
…d overridable Global skill installs (e.g. ~/.claude/skills) have no hyperframes package.json in the loader's ancestor chain, so readBundledHyperframesVersion returns null and hyperframesPackageSpec threw. Because the spec is passed eagerly as an argument to importPackagesOrBootstrap, it threw even when @hyperframes/producer was already installed and no bootstrap was needed. Resolution order is now: HYPERFRAMES_SKILL_PKG_VERSION env override first, then the bundled/in-repo version, then a non-throwing @latest fallback that warns to stderr. @latest satisfies the pinned-spec guard so bootstrap still installs, and already-installed packages import fine; the eager argument is now harmless. In-repo resolution is unchanged (same pinned version). Applied to both package-loader.mjs copies (animation + creative) and documented the env var in animation-map.mjs and contrast-report.mjs usage. Adds a focused test covering override wins, in-repo pin, and unresolvable @latest fallback.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at HEAD cbc16d06. Read-only pass on the two package-loader.mjs copies, the two usage-header docs, the new test, and the manifest. Cross-checked against packages/cli/scripts/build-copy.mjs (skills are bundled into dist/skills/... in the published @hyperframes/cli, which is the global-install shape this PR targets — so the fix propagates via the CLI publish pipeline automatically).
🟢 Strengths
- Correct diagnosis. PR body walks
readBundledHyperframesVersion()→ eager-arg-eval → throw-before-import-check cleanly. Verified the throw was in fact eager: bothanimation-map.mjs:29andcontrast-report.mjs:28buildnpmPackages: [hyperframesPackageSpec(...)]as an argument literal, so any throw insidehyperframesPackageSpecshort-circuits beforeimportPackagesOrBootstrapgets to tryresolvePackageEntryon the already-installed package. Removing the throw lets the resolver do its job. - Symmetric fix across both loaders. Diff'd
hyperframes-animation/scripts/package-loader.mjsandhyperframes-creative/scripts/package-loader.mjsat HEAD — byte-identical. Two copies is the pre-existing shape (not this PR's problem), and the PR keeps them in sync. - Fallback observability is on the failure path. The
@latestfallback emits a two-line stderr message naming the env var override BEFORE returning. This is exactly the "warn on band-aid path" shape — no silent swallow. @latestsatisfiesassertPinnedPackageSpecs. VerifiedhasVersionSpec("@hyperframes/producer@latest")returnstruevia the L182-188 check (second@after the slash), so the pinned-spec guard downstream still passes without a signature change. Nice reuse of the existing invariant; PR body's claim holds.- Test isolation is thoughtful. Test (c) uses
spawnSyncinto a temp-dir subprocess so the ancestor-package.json lookup can't accidentally resolve the repo's ownhyperframes— that's the right way to prove "no resolvable version" behavior without touching the parent process'scwd.
🟡 Questions / follow-ups
- Test file exists only for the animation copy.
skills/hyperframes-animation/scripts/package-loader.test.mjsis new;skills/hyperframes-creative/scripts/has no sibling. The loaders are byte-identical today, so functional coverage is fine, but nothing prevents the two copies from silently drifting in a future PR (the "two loader copies kept in sync manually" shape is fragile). Cheapest guard: a one-line assertion in the animation test that reads both files andassert.equals their contents — or, better long-term, symlink one to the other, or hoist to a sharedskills/_shared/package-loader.mjs. Not blocking; worth surfacing before the shape ossifies. HYPERFRAMES_SKILL_PKG_VERSIONisn't documented inSKILL.mdfor either skill. The env var lives in the usage headers ofanimation-map.mjs(L11-15) andcontrast-report.mjs(L15-19), which is the right place for CLI users. But the skill-agent-visibleskills/hyperframes-animation/SKILL.mdandskills/hyperframes-creative/SKILL.mddon't mention it. If an agent hits the warn and wants to know how to pin,SKILL.mdis where they'll look. Low-effort follow-up (one sentence per SKILL.md).@latestversion-skew risk in the global-install fallback path. When a globally-installed user with no ancestorpackage.jsontriggers the fallback, they get the newest published@hyperframes/producer— which may drift from the CLI's own bundled version over months. The warning + env-var escape hatch is a reasonable answer, but I'd note this loader ships INSIDE the published@hyperframes/clipackage (perpackages/cli/scripts/build-copy.mjs:89copyingskills/<pkg>→dist/skills/<pkg>). That means at bundle-time you do know the CLI's own version — the same package.json thattsupreads atpackages/cli/tsup.config.ts:5. A future refinement would be to snapshot the CLI's version at build time (baked into a generated constant in the copied loader) and fall back to^<major>.<minor>.xinstead of@latest. Not blocking for this fix; wanted to flag while the shape is fresh.- Warn fires per-call, not per-process. Only matters if a script calls
hyperframesPackageSpec()multiple times — currently 1 call in each of the two callers, so no spam today. If future callers loop, consider aSet<packageName>-keyed once-per-process gate. Nit. - Env var value passes through unvalidated.
.trim()handles whitespace, butHYPERFRAMES_SKILL_PKG_VERSION="not-a-version"gets embedded verbatim into the spec string. Since the install path uses argv-arrayspawnSyncwith--ignore-scripts --no-save, it's not shell-interpolated (safe), and npm rejects invalid semver — so worst case is a confusing npm error rather than an early "invalid version" one. Nit; would be a one-liner to/^[@\w./~^*<>=|,\s-]+$/-guard and emit a clearer message. - Test coverage gap: env-var precedence-over-resolvable-in-repo. Test (a) proves env wins in the no-ancestor case (via async-import from within the repo but with env set — verified by inspection, but the test doesn't isolate cwd, so it's implicitly relying on the module-level import's ancestor-scan being suppressed by the early env check). Test (b) proves in-repo wins when env is unset. Neither test isolates "env set AND in-repo package.json reachable → env wins." Given the code shape is trivial (early return on env before
readBundledHyperframesVersion), risk is low; a one-line assertion in test (a) that the returned version equals"9.9.9"(which it already does) is sufficient — that IS the precedence proof, so this is actually already covered. Withdrawing on second read; leaving here as note for anyone else reading.
Ship criteria
CI green (all required checks passing at HEAD), no peer reviews yet, mergeable: true / mergeStateStatus: BLOCKED on the standard reviewer-required gate. Fix is contained, well-tested, and observable. Leaving as COMMENT — stamp routes through the OG Rames Jusso lane.
— Review by Rames D Jusso
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R2 @ 71c398c5 — verifying against my R1 at cbc16d06.
Findings verification
-
✅ R1 🟡 (1) Test asymmetry — resolved.
skills/hyperframes-creative/scripts/package-loader.test.mjsexists at HEAD, and both test files are byte-identical (git blob7b36a3ceb3for both). Sibling loaders also byte-identical (063bf4fe0c). All 3 test cases (env-override wins, in-repo resolves,@latestfallback via isolated-tmpdir spawnSync) present on both sides. Coverage propagates; drift risk is now caught by whichever side runs first in CI.- Follow-up nit (non-blocking, deferred is fine): two byte-identical test files is a smaller version of the same "manually kept in sync" shape as the loaders themselves. If a future PR hoists the loader to a shared module, the tests should follow. Not this PR's job.
-
✅ R1 🟡 (2) Env-var doc-visibility gap — resolved. Both
skills/hyperframes-animation/SKILL.md(L75+) andskills/hyperframes-creative/SKILL.md(L52+) now documentHYPERFRAMES_SKILL_PKG_VERSION. Sentences are symmetric (only the entrypoint filename differs:animation-map.mjsvscontrast-report.mjs). Doc is placed in the discoverable "Scripts" section next to the script listing, not buried at bottom. Explains purpose ("bootstrap the bundled HyperFrames package version"), when to set it ("running the skill outside the bundled CLI/skill install"), and the precedence intent ("pin that bootstrap version explicitly").- The two
.mjsscript usage headers (animation-map.mjs:11-15,contrast-report.mjs:15-19) now also mention the env var, with a symmetric block that names the fallback semantics ("falls back to @latest with a warning otherwise"). Nice — CLI-users and skill-agents both have a reachable pointer.
- The two
-
↩️ R1 🟡 nit (
@latestversion-skew) — acknowledged-and-deferred. Not addressed in R2; explicitly flagged as future refinement in R1. Fine.
Adjacent-file coherence
- Loader body change is symmetric across both copies (same 18-line delta at
hyperframesPackageSpec— override check first, thenreadBundledHyperframesVersion, then stderr-warn@latestfallback). Both blobs at063bf4fe0c— verified viagh api contents?ref=71c398c5. skills-manifest.jsonregenerated:hyperframes-animationhash57458f4308708e21→f789d7f95d9ad2fe, files 115 → 116;hyperframes-creativehashbb248c1bc5cc28b5→8573b34712e14ab1, files 67 → 68. Both bumps == +1 file (the new test), consistent with the diff.- No divergence introduced between animation and creative sides — the "byte-identical siblings" invariant that made R1(1) worth flagging is preserved across all four affected files (loader, loader-test, SKILL.md addition, script-header addition).
Ship state
- HEAD:
71c398c5(stable through review, verified viagh pr view --json headRefOid). - Mergeable:
MERGEABLE/BLOCKED(standard reviewer-required gate; not a merge conflict). - Draft: no.
- CI:
Test: skills✅,Skills: manifest in sync✅, all required checks green or SKIPPED (path-filter). CodeQL still IN_PROGRESS but that's advisory/NEUTRAL historically on this repo. - Peer reviews since R1: none. No inline comments.
Verdict
All R1 substantive findings resolved cleanly. Fix is symmetric, tested on both sides, documented on both surfaces. Leaving as COMMENT — stamp lane routes through the OG Rames Jusso.
— Review by Rames D Jusso
Problem
hyperframes-animation/scripts/animation-map.mjs(andhyperframes-creative/scripts/contrast-report.mjs) fail for users who installed the skills globally:This happens even when
@hyperframes/produceris already installed and no bootstrap install is needed.Root cause
hyperframesPackageSpec()inpackage-loader.mjscallsreadBundledHyperframesVersion(), which only walks the ancestor chain of the script location and cwd for a hyperframespackage.json. A global skill install (~/.claude/skills,~/.agents/skills) has no such ancestor, so it returnsnulland the function throws.The call sites evaluate the spec eagerly as an argument to
importPackagesOrBootstrap([...], { npmPackages: [hyperframesPackageSpec(...)] }), so it throws up-front, before the loader ever checks whether the package is already resolvable. Result: it throws even when nothing needs installing.Reproduce
Run the loader from a temp dir whose ancestor chain (and cwd) has no hyperframes
package.json:hyperframesPackageSpec("@hyperframes/producer")throws "Could not determine ...".@hyperframes/producer@0.7.22).Fix
Version resolution is now non-fatal and overridable, with order:
HYPERFRAMES_SKILL_PKG_VERSIONenv override (honored first) so users/CI can pin the version explicitly.@latestfallback that prints a one-line stderr warning naming the env override.@latestsatisfies the existing pinned-spec guard, so a bootstrap install still proceeds and already-installed packages import fine.Because the fallback never throws, the eager argument evaluation is now harmless, so no signature change to
importPackagesOrBootstrapwas needed. Applied to both identicalpackage-loader.mjscopies (animation + creative) and documented the env var in the two call-site scripts' usage headers. Regeneratedskills-manifest.json.Verification
@latest.skills/hyperframes-animation/scripts/package-loader.test.mjs): (a) env override wins, (b) resolvable in-repo pin, (c) unresolvable ->@latestfallback with no throw.node --test skills/**/*.test.mjs); oxfmt + oxlint clean; skills-manifest in sync; commit passed all pre-commit gates.