Reuse now hands the semantic judgment to the coding agent instead of a
string heuristic, while keeping the deterministic normalize-exact match as
an automatic dedup floor. No LLM/embedding call enters resolve; it stays
offline-capable.
- lib/match.mjs: shared matchTokens + typesMatch (extracted from adopt.mjs
and resolve.mjs so the icon<->image equivalence and token rules can't
drift between the do-path and the look-path); adds tokenOverlap ranker.
- resolve --candidates: side-effect-free listing of reusable assets across
the project manifest AND the global ~/.media cache, ranked by lexical
overlap, capped per scope, --json or human table. Never hard-filters on
zero overlap (that would pre-empt the agent's judgment); the agent decides.
- resolve --reuse <sha>: import a specific global-cache asset by content
sha/prefix (from --candidates) into the project via importFromCache,
marked source=reused-explicit / provenance.reused_by=agent.
- Adherence nudge: on a resolve that misses the exact floor and is about to
fetch, print a one-line stderr hint when similar cached assets exist,
pointing at --candidates. Offline, stderr (safe under --json), never
auto-reuses a fuzzy match.
- cache.mjs: export readGlobalManifest; add findGlobalBySha (prefix resolve
with ambiguity/miss handling).
- Telemetry: media_use_candidates event + reused-explicit source on
media_use_resolve (type/scope/counts only, no intent text or paths).
- SKILL.md: 'Reuse before you resolve' guidance + trust guardrail
(prefer-fresh-when-unsure, entity-exact for brand, cross-project bleed).
- Tests: lib/candidates.test.mjs (ranking, no-hard-filter, cap/truncation,
icon<->image, sha resolution, formatter); adopt.mjs refactor covered by
existing lib/adopt.test.mjs.
Full media-use suite green; verified e2e against the live catalog
(cross-project resolve->candidates->reuse; hint fires on miss).
Two defects in the resolve cascade that made cache and asset-reuse
misbehave in practice:
- Prompt matching was byte-exact and case-sensitive. findByPrompt and
cacheGet compared provenance.prompt with ===, so "Calm piano" and
"calm piano" re-searched and re-downloaded instead of reusing the
cached asset (same project and cross-project). Add normalizePrompt
(trim + lowercase + collapse whitespace) and key both lookups on it;
the raw prompt is still stored for audit.
- findExistingAsset matched with name.includes(intent) ||
intent.includes(name), which silently returned the WRONG local file:
intent "whoosh" grabbed a stray who.mp3, and a one-letter filename
matched every intent. Require a shared word token (>= 3 chars, minus
stopwords) so a false negative just falls through to a catalog search
rather than shipping the wrong asset.
Adds lib/adopt.test.mjs and extends manifest.test.mjs. Full media-use
suite green; verified e2e against the live catalog (case-variant
cross-project resolve now reuses; whoosh no longer grabs who.mp3).
Swap execSync(<shell-string>) → execFileSync(file, [argv]) in probe.mjs, heygen-search.mjs, and eval.mjs so hostile filenames / queries / manifest metadata can't inject shell. Adds probe.test.mjs regression guard and a CI Test (skills) job so it actually runs. Closes the media-use High/Critical scanner alert.
* feat(media-use): core infrastructure — manifest, cache, adopt, probe
Foundation for media-use — the media resolution layer for HyperFrames.
- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)
* fix(media-use): oxfmt formatting + cap freeze download size
Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(media-use): resolve engine + all providers + brand from frame.md
- resolve.mjs: cheapest-first cascade
- BGM/SFX via heygen --headers, Image/Icon via heygen asset search
- Brand tokens from frame.md / design.md (local, no API)
- SKILL.md: full agent docs + hyperframes.dev/design redirect
- Router skill + workflow skill references
* fix(media-use): oxfmt formatting for resolve + providers
Format brand/heygen-search/providers/sfx providers + resolve.mjs
(CI oxfmt --check gate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(media-use): align providers with the real heygen CLI surface (v0.1.6)
Verified live against the official Go `heygen` CLI v0.1.6 with a valid key:
- Caller attribution: pass `--headers 'X-HeyGen-Client-Source: media-use'`
(the allowlisted flag the CLI added for media-use in v0.1.6). The old
`--x-source media-use` was never a real flag and broke every call.
- Command is `asset search` (the `list` leaf was dropped in v0.1.6), not
`asset search list`.
- `--min-score` is sent server-side: honored by `audio sounds list`, but the
`asset search` backend rejects it and returns no score field, so only the
audio providers pass it (image/icon don't).
- Drop hardcoded `ext` so resolve.mjs derives it from the URL: catalog icons
are .png (not .svg), some BGM is .wav (not .mp3).
Also: surface CLI/auth failures on stderr instead of swallowing them as
'no results', carry icon width/height through, and document the heygen CLI
install + >= v0.1.6 requirement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(media-use): core infrastructure — manifest, cache, adopt, probe
Foundation for media-use — the media resolution layer for HyperFrames.
- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)
* fix(media-use): oxfmt formatting + cap freeze download size
Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>