From 4b778034a4f604ee11fa86e2b68c569af1b2300d Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sat, 10 Oct 2026 02:50:22 +0800 Subject: [PATCH 01/10] fix(media-use): serialize concurrent preference records --- .../lib/prefs-store.concurrent.test.mjs | 173 ++++++++++++++++++ .../cli/src/media-use/lib/prefs-store.mjs | 72 +++++--- skills-manifest.json | 2 +- skills/media-use/scripts/lib/prefs-store.mjs | 72 +++++--- 4 files changed, 262 insertions(+), 57 deletions(-) create mode 100644 packages/cli/src/media-use/lib/prefs-store.concurrent.test.mjs diff --git a/packages/cli/src/media-use/lib/prefs-store.concurrent.test.mjs b/packages/cli/src/media-use/lib/prefs-store.concurrent.test.mjs new file mode 100644 index 00000000000..8a272459ffc --- /dev/null +++ b/packages/cli/src/media-use/lib/prefs-store.concurrent.test.mjs @@ -0,0 +1,173 @@ +import assert from "node:assert/strict"; +import { fork } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + utimesSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; +import { projectPrefsPath, recordPreference, userPrefsPath } from "./prefs-store.mjs"; + +async function inSandbox(task) { + const root = mkdtempSync(join(tmpdir(), "mu-prefs-concurrent-")); + const previousHome = process.env.HYPERFRAMES_MEDIA_HOME; + process.env.HYPERFRAMES_MEDIA_HOME = join(root, "home"); + const project = join(root, "project"); + mkdirSync(project); + try { + await task({ root, project }); + } finally { + if (previousHome === undefined) delete process.env.HYPERFRAMES_MEDIA_HOME; + else process.env.HYPERFRAMES_MEDIA_HOME = previousHome; + rmSync(root, { recursive: true, force: true }); + } +} + +function worker(root, source, args = []) { + const path = join(mkdtempSync(join(root, "worker-")), "worker.cjs"); + writeFileSync(path, source); + const child = fork(path, args, { silent: true, execArgv: [] }); + let stderr = ""; + child.stderr.setEncoding("utf8").on("data", (data) => (stderr += data)); + const ready = new Promise((resolve, reject) => { + child.once("message", resolve); + child.once("error", reject); + child.once("exit", () => reject(new Error(`worker exited before ready: ${stderr}`))); + }); + const closed = new Promise((resolve, reject) => { + child.once("error", reject); + child.once("close", (code) => resolve({ code, stderr })); + }); + return { child, ready, closed }; +} + +for (const tier of ["project", "user"]) { + test(`record re-reads the ${tier} tier after another process releases its lock`, async () => { + await inSandbox(async ({ root, project }) => { + const path = tier === "project" ? projectPrefsPath(project) : userPrefsPath(); + mkdirSync(dirname(path), { recursive: true }); + const held = worker( + root, + `const fs = require("node:fs"); + const path = process.argv[2]; + fs.writeFileSync(path + ".lock", "held", { flag: "wx" }); + process.send("locked"); + setTimeout(() => { + fs.writeFileSync(path, JSON.stringify({version: 1, + preferences: {aspect: {value: "1080x1920", confirmed_in: ["other"]}}, + sightings: {aspect: {"1080x1920": ["other"]}}})); + fs.rmSync(path + ".lock"); + process.disconnect(); + }, 300);`, + [path], + ); + try { + await held.ready; + recordPreference({ projectDir: project, key: "destination", value: "youtube-shorts" }); + const result = await held.closed; + assert.equal(result.code, 0, result.stderr); + const file = JSON.parse(readFileSync(path, "utf8")); + assert.equal(file.preferences.aspect?.value, "1080x1920"); + if (tier === "project") { + assert.equal(file.preferences.destination?.value, "youtube-shorts"); + } else { + assert.deepEqual(file.sightings.destination?.["youtube-shorts"], ["project"]); + assert.deepEqual(file.sightings.aspect?.["1080x1920"], ["other"]); + } + } finally { + held.child.kill(); + await held.closed; + } + }); + }); +} + +test("parallel records preserve every project key and promote every shared user sighting", async () => { + await inSandbox(async ({ root, project }) => { + const secondProject = join(root, "second-project"); + mkdirSync(secondProject); + const modulePath = fileURLToPath(new URL("./prefs-store.mjs", import.meta.url)); + const choices = { + destination: "youtube-shorts", + aspect: "1080x1920", + language: "en", + flow: "automation", + }; + const children = [project, secondProject].flatMap((projectDir) => + Object.entries(choices).map(([key, value]) => + worker( + root, + `const {pathToFileURL} = require("node:url"); + import(pathToFileURL(process.argv[2])).then(({recordPreference}) => { + process.send("ready"); + process.once("message", () => { + try {recordPreference(JSON.parse(process.argv[3]));} + catch (error) {console.error(error.message); process.exitCode = 1;} + process.disconnect(); + }); + });`, + [modulePath, JSON.stringify({ projectDir, key, value })], + ), + ), + ); + try { + await Promise.all(children.map((child) => child.ready)); + for (const child of children) child.child.send("record"); + const results = await Promise.all(children.map((child) => child.closed)); + for (const result of results) assert.equal(result.code, 0, result.stderr); + for (const projectDir of [project, secondProject]) { + const file = JSON.parse(readFileSync(projectPrefsPath(projectDir), "utf8")); + assert.deepEqual(Object.keys(file.preferences).sort(), Object.keys(choices).sort()); + for (const [key, value] of Object.entries(choices)) + assert.equal(file.preferences[key].value, value); + assert.deepEqual(readdirSync(dirname(projectPrefsPath(projectDir))), ["preferences.json"]); + } + const user = JSON.parse(readFileSync(userPrefsPath(), "utf8")); + for (const [key, value] of Object.entries(choices)) { + assert.equal(user.preferences[key].value, value); + assert.deepEqual(user.sightings[key][value].sort(), ["project", "second-project"]); + } + assert.deepEqual(readdirSync(dirname(userPrefsPath())), ["preferences.json"]); + } finally { + for (const child of children) child.child.kill(); + await Promise.all(children.map((child) => child.closed)); + } + }); +}); + +for (const tier of ["project", "user"]) { + test(`an abandoned ${tier} lock never permits an unlocked preference write`, async () => { + await inSandbox(async ({ project }) => { + const path = tier === "project" ? projectPrefsPath(project) : userPrefsPath(); + mkdirSync(dirname(path), { recursive: true }); + const original = JSON.stringify({ version: 1, preferences: {}, sightings: {} }); + writeFileSync(path, original); + const lock = `${path}.lock`; + writeFileSync(lock, "abandoned"); + const past = new Date(Date.now() - 60_000); + utimesSync(lock, past, past); + const record = () => recordPreference({ projectDir: project, key: "language", value: "en" }); + if (tier === "project") { + assert.throws(record, (error) => error.code === "HF_SETTINGS_LOCKED"); + assert.equal(existsSync(userPrefsPath()), false); + } else { + assert.equal(record().promoted, false); + assert.equal( + JSON.parse(readFileSync(projectPrefsPath(project), "utf8")).preferences.language.value, + "en", + ); + } + assert.equal(readFileSync(path, "utf8"), original); + assert.equal(readFileSync(lock, "utf8"), "abandoned"); + assert.equal(existsSync(`${path}.${process.pid}.tmp`), false); + }); + }); +} diff --git a/packages/cli/src/media-use/lib/prefs-store.mjs b/packages/cli/src/media-use/lib/prefs-store.mjs index a8f84896979..77ab81516a6 100644 --- a/packages/cli/src/media-use/lib/prefs-store.mjs +++ b/packages/cli/src/media-use/lib/prefs-store.mjs @@ -1,5 +1,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import * as fs from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; +import { withFileLock } from "./config-lock.mjs"; import { globalMediaDir } from "./media-home.mjs"; /** @@ -70,10 +72,23 @@ function readPrefsFile(path) { /** Atomic write (tmp + rename) so a crash never leaves a torn file. */ function writePrefsFile(path, file) { + const tmp = `${path}.${process.pid}.tmp`; + try { + writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`); + renameSync(tmp, path); + } finally { + fs.rmSync(tmp, { force: true }); + } +} + +function updatePrefsFile(path, update) { mkdirSync(dirname(path), { recursive: true }); - const tmp = `${path}.tmp`; - writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`); - renameSync(tmp, path); + return withFileLock(`${path}.lock`, fs, () => { + const file = readPrefsFile(path); + const result = update(file); + writePrefsFile(path, file); + return result; + }); } /** `style_preset` entries are stored per workflow as `style_preset.`. */ @@ -112,16 +127,18 @@ function dedupe(list) { * provenance over (the old confirmations vouched for the old value). */ function recordProjectTier(projectDir, fullKey, value, projectName, now) { - const path = projectPrefsPath(projectDir); - const file = readPrefsFile(path); - const previous = file.preferences[fullKey]; - const keepProvenance = validEntry(previous) && previous.value === value; - const confirmedIn = keepProvenance - ? dedupe([...(Array.isArray(previous.confirmed_in) ? previous.confirmed_in : []), projectName]) - : [projectName]; - file.preferences[fullKey] = { value, confirmed_in: confirmedIn, updated_at: now }; - writePrefsFile(path, file); - return confirmedIn; + return updatePrefsFile(projectPrefsPath(projectDir), (file) => { + const previous = file.preferences[fullKey]; + const keepProvenance = validEntry(previous) && previous.value === value; + const confirmedIn = keepProvenance + ? dedupe([ + ...(Array.isArray(previous.confirmed_in) ? previous.confirmed_in : []), + projectName, + ]) + : [projectName]; + file.preferences[fullKey] = { value, confirmed_in: confirmedIn, updated_at: now }; + return confirmedIn; + }); } /** @@ -129,21 +146,20 @@ function recordProjectTier(projectDir, fullKey, value, projectName, now) { * the same value has been confirmed in PROMOTE_AT distinct projects. */ function recordUserSighting(fullKey, value, projectName, now) { - const path = userPrefsPath(); - const file = readPrefsFile(path); - const keySightings = isRecord(file.sightings[fullKey]) ? file.sightings[fullKey] : {}; - const seenIn = dedupe([ - ...(Array.isArray(keySightings[value]) ? keySightings[value] : []), - projectName, - ]); - keySightings[value] = seenIn; - file.sightings[fullKey] = keySightings; - const promoted = seenIn.length >= PROMOTE_AT; - if (promoted) { - file.preferences[fullKey] = { value, confirmed_in: seenIn, updated_at: now }; - } - writePrefsFile(path, file); - return promoted; + return updatePrefsFile(userPrefsPath(), (file) => { + const keySightings = isRecord(file.sightings[fullKey]) ? file.sightings[fullKey] : {}; + const seenIn = dedupe([ + ...(Array.isArray(keySightings[value]) ? keySightings[value] : []), + projectName, + ]); + keySightings[value] = seenIn; + file.sightings[fullKey] = keySightings; + const promoted = seenIn.length >= PROMOTE_AT; + if (promoted) { + file.preferences[fullKey] = { value, confirmed_in: seenIn, updated_at: now }; + } + return promoted; + }); } /** diff --git a/skills-manifest.json b/skills-manifest.json index 868bc89db1d..05a66a5cb73 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -54,7 +54,7 @@ "files": 1 }, "media-use": { - "hash": "c8800249da2d1ce6", + "hash": "47ef3ad90f4f73ac", "files": 109 }, "motion-graphics": { diff --git a/skills/media-use/scripts/lib/prefs-store.mjs b/skills/media-use/scripts/lib/prefs-store.mjs index a8f84896979..77ab81516a6 100644 --- a/skills/media-use/scripts/lib/prefs-store.mjs +++ b/skills/media-use/scripts/lib/prefs-store.mjs @@ -1,5 +1,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import * as fs from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; +import { withFileLock } from "./config-lock.mjs"; import { globalMediaDir } from "./media-home.mjs"; /** @@ -70,10 +72,23 @@ function readPrefsFile(path) { /** Atomic write (tmp + rename) so a crash never leaves a torn file. */ function writePrefsFile(path, file) { + const tmp = `${path}.${process.pid}.tmp`; + try { + writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`); + renameSync(tmp, path); + } finally { + fs.rmSync(tmp, { force: true }); + } +} + +function updatePrefsFile(path, update) { mkdirSync(dirname(path), { recursive: true }); - const tmp = `${path}.tmp`; - writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`); - renameSync(tmp, path); + return withFileLock(`${path}.lock`, fs, () => { + const file = readPrefsFile(path); + const result = update(file); + writePrefsFile(path, file); + return result; + }); } /** `style_preset` entries are stored per workflow as `style_preset.`. */ @@ -112,16 +127,18 @@ function dedupe(list) { * provenance over (the old confirmations vouched for the old value). */ function recordProjectTier(projectDir, fullKey, value, projectName, now) { - const path = projectPrefsPath(projectDir); - const file = readPrefsFile(path); - const previous = file.preferences[fullKey]; - const keepProvenance = validEntry(previous) && previous.value === value; - const confirmedIn = keepProvenance - ? dedupe([...(Array.isArray(previous.confirmed_in) ? previous.confirmed_in : []), projectName]) - : [projectName]; - file.preferences[fullKey] = { value, confirmed_in: confirmedIn, updated_at: now }; - writePrefsFile(path, file); - return confirmedIn; + return updatePrefsFile(projectPrefsPath(projectDir), (file) => { + const previous = file.preferences[fullKey]; + const keepProvenance = validEntry(previous) && previous.value === value; + const confirmedIn = keepProvenance + ? dedupe([ + ...(Array.isArray(previous.confirmed_in) ? previous.confirmed_in : []), + projectName, + ]) + : [projectName]; + file.preferences[fullKey] = { value, confirmed_in: confirmedIn, updated_at: now }; + return confirmedIn; + }); } /** @@ -129,21 +146,20 @@ function recordProjectTier(projectDir, fullKey, value, projectName, now) { * the same value has been confirmed in PROMOTE_AT distinct projects. */ function recordUserSighting(fullKey, value, projectName, now) { - const path = userPrefsPath(); - const file = readPrefsFile(path); - const keySightings = isRecord(file.sightings[fullKey]) ? file.sightings[fullKey] : {}; - const seenIn = dedupe([ - ...(Array.isArray(keySightings[value]) ? keySightings[value] : []), - projectName, - ]); - keySightings[value] = seenIn; - file.sightings[fullKey] = keySightings; - const promoted = seenIn.length >= PROMOTE_AT; - if (promoted) { - file.preferences[fullKey] = { value, confirmed_in: seenIn, updated_at: now }; - } - writePrefsFile(path, file); - return promoted; + return updatePrefsFile(userPrefsPath(), (file) => { + const keySightings = isRecord(file.sightings[fullKey]) ? file.sightings[fullKey] : {}; + const seenIn = dedupe([ + ...(Array.isArray(keySightings[value]) ? keySightings[value] : []), + projectName, + ]); + keySightings[value] = seenIn; + file.sightings[fullKey] = keySightings; + const promoted = seenIn.length >= PROMOTE_AT; + if (promoted) { + file.preferences[fullKey] = { value, confirmed_in: seenIn, updated_at: now }; + } + return promoted; + }); } /** From 059e4abd2fd2647272aafc23daea2dd57cf90212 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Wed, 14 Oct 2026 04:00:40 +0800 Subject: [PATCH 02/10] chore: prepare conflict resolution against current main --- skills-manifest.json | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 05a66a5cb73..960e5a88ac8 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -2,35 +2,35 @@ "source": "heygen-com/hyperframes", "skills": { "embedded-captions": { - "hash": "643579aed5919847", - "files": 144 + "hash": "1dc7ec2fd4a6e3fc", + "files": 145 }, "faceless-explainer": { - "hash": "1f10a69e0f879698", - "files": 28 + "hash": "bb2225ebaf7f9622", + "files": 29 }, "figma": { "hash": "f8cf9f2c78ea8374", "files": 2 }, "general-video": { - "hash": "7194a47a82647a88", - "files": 5 + "hash": "17dfbb1d34efb012", + "files": 6 }, "hyperframes": { - "hash": "589ae40d15628917", - "files": 29 + "hash": "9787b91e4c16bf7f", + "files": 31 }, "hyperframes-animation": { - "hash": "9370b856259f4586", + "hash": "a5d09375a50f71c8", "files": 122 }, "hyperframes-audio": { - "hash": "e96de47ed2e6097a", - "files": 7 + "hash": "780c9d4d8d23ab40", + "files": 8 }, "hyperframes-cli": { - "hash": "c053e99be2d209b1", + "hash": "56a46bc15c308d98", "files": 11 }, "hyperframes-core": { @@ -54,11 +54,11 @@ "files": 1 }, "media-use": { - "hash": "47ef3ad90f4f73ac", - "files": 109 + "hash": "c4c3c723aa04bb7d", + "files": 112 }, "motion-graphics": { - "hash": "6db77031ad839f6a", + "hash": "7e4af0881390ac2e", "files": 23 }, "music-to-video": { @@ -66,12 +66,12 @@ "files": 169 }, "pr-to-video": { - "hash": "88f4c68788eaba9e", - "files": 34 + "hash": "d377b802ef54bc46", + "files": 35 }, "product-launch-video": { - "hash": "76149a9f3514626e", - "files": 36 + "hash": "965b9b93bf19b173", + "files": 37 }, "remotion-to-hyperframes": { "hash": "d02be65b7f3f4a63", @@ -82,7 +82,7 @@ "files": 2 }, "talking-head-recut": { - "hash": "9aac076ceddd2499", + "hash": "4b2cf6d7b6e336b4", "files": 28 } } From 9a574f747175857bf327a343d01fd817eed6c6f9 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Wed, 14 Oct 2026 04:00:48 +0800 Subject: [PATCH 03/10] fix(media-use): serialize concurrent preference records --- .../cli/src/media-use/lib/prefs-store.mjs | 4 +- .../src/media-use/lib/prefs-store.test.mjs | 56 +++++++++++++++++++ skills-manifest.json | 2 +- skills/media-use/references/memory.md | 4 ++ skills/media-use/scripts/lib/prefs-store.mjs | 4 +- 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/media-use/lib/prefs-store.mjs b/packages/cli/src/media-use/lib/prefs-store.mjs index 77ab81516a6..87e3707dd12 100644 --- a/packages/cli/src/media-use/lib/prefs-store.mjs +++ b/packages/cli/src/media-use/lib/prefs-store.mjs @@ -77,7 +77,9 @@ function writePrefsFile(path, file) { writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`); renameSync(tmp, path); } finally { - fs.rmSync(tmp, { force: true }); + try { + fs.rmSync(tmp, { force: true }); + } catch {} } } diff --git a/packages/cli/src/media-use/lib/prefs-store.test.mjs b/packages/cli/src/media-use/lib/prefs-store.test.mjs index 4c13e5fa24c..de6be127ec4 100644 --- a/packages/cli/src/media-use/lib/prefs-store.test.mjs +++ b/packages/cli/src/media-use/lib/prefs-store.test.mjs @@ -1,6 +1,8 @@ import { strict as assert } from "node:assert"; import { test } from "node:test"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import fs from "node:fs"; +import { syncBuiltinESMExports } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -39,6 +41,60 @@ function withSandbox(fn) { } } +for (const code of ["EPERM", "EBUSY"]) { + test(`temporary-file cleanup ${code} does not fail a successful preference record`, (t) => { + const remove = fs.rmSync; + t.mock.method(fs, "rmSync", (path, options) => { + if (path.endsWith(".tmp")) throw Object.assign(new Error("cleanup failed"), { code }); + return remove(path, options); + }); + syncBuiltinESMExports(); + t.after(() => { + t.mock.restoreAll(); + syncBuiltinESMExports(); + }); + + withSandbox(({ projectA, projectB }) => { + recordPreference({ projectDir: projectA, key: "language", value: "en" }); + const second = recordPreference({ projectDir: projectB, key: "language", value: "en" }); + assert.equal(second.promoted, true); + for (const path of [ + projectPrefsPath(projectA), + projectPrefsPath(projectB), + userPrefsPath(), + ]) { + assert.equal(JSON.parse(readFileSync(path, "utf8")).preferences.language.value, "en"); + } + }); + }); + + test(`temporary-file cleanup ${code} preserves the original preference write error`, (t) => { + const remove = fs.rmSync; + const write = fs.writeFileSync; + const writeError = Object.assign(new Error("disk full"), { code: "ENOSPC" }); + t.mock.method(fs, "rmSync", (path, options) => { + if (path.endsWith(".tmp")) throw Object.assign(new Error("cleanup failed"), { code }); + return remove(path, options); + }); + t.mock.method(fs, "writeFileSync", (path, ...args) => { + if (path.endsWith(".tmp")) throw writeError; + return write(path, ...args); + }); + syncBuiltinESMExports(); + t.after(() => { + t.mock.restoreAll(); + syncBuiltinESMExports(); + }); + + withSandbox(({ projectA }) => { + assert.throws( + () => recordPreference({ projectDir: projectA, key: "language", value: "en" }), + (error) => error === writeError, + ); + }); + }); +} + test("record writes the project tier and get merges it as source=project", () => { withSandbox(({ projectA }) => { const result = recordPreference({ projectDir: projectA, key: "destination", value: "x-feed" }); diff --git a/skills-manifest.json b/skills-manifest.json index 960e5a88ac8..95735f55b0d 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -54,7 +54,7 @@ "files": 1 }, "media-use": { - "hash": "c4c3c723aa04bb7d", + "hash": "95acf615ab0b96af", "files": 112 }, "motion-graphics": { diff --git a/skills/media-use/references/memory.md b/skills/media-use/references/memory.md index 5c24b861a19..366a0227b88 100644 --- a/skills/media-use/references/memory.md +++ b/skills/media-use/references/memory.md @@ -12,6 +12,10 @@ node /scripts/prefs.mjs record --hyperframes . --key style_preset --v Only what the user actually confirmed gets recorded — never an inferred or defaulted value. How workflows consume these (a remembered value becomes the recommended default with a receipt, and never skips a question) is the brief contract's rule: `hyperframes/references/brief-contract.md` § 2, Remembered defaults. +Preference records serialize writes with `preferences.json.lock` beside each tier's file. If a process stops while holding that lock, later records leave it in place and report "Settings are locked…". After confirming no HyperFrames process is recording preferences, delete the named lock file and retry. A locked personal tier does not fail the project record; personal promotion waits for a later record. + +Keep `.media/preferences.json` committed, and add `.media/*.lock` to the project's `.gitignore` so transient locks are never shared with the team. + ## Recipes — frozen video bundles The heavyweight tier of user memory: one approved run frozen as a named, versioned bundle — `frame.md`, the storyboard skeleton (structure kept, content blanked to per-frame fill-ins), the brief skeleton (from `BRIEF.md` when the project has one — reusable frontmatter kept, run-shape and prose blanked), and the confirmed brief values. Same two tiers: project `.media/recipes//` (committed) and `~/.media/recipes//` (a freeze is already a confirmed bundle, so it promotes immediately — no two-project rule). Re-freezing a name bumps `version` and archives the old folder as `@v`. diff --git a/skills/media-use/scripts/lib/prefs-store.mjs b/skills/media-use/scripts/lib/prefs-store.mjs index 77ab81516a6..87e3707dd12 100644 --- a/skills/media-use/scripts/lib/prefs-store.mjs +++ b/skills/media-use/scripts/lib/prefs-store.mjs @@ -77,7 +77,9 @@ function writePrefsFile(path, file) { writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`); renameSync(tmp, path); } finally { - fs.rmSync(tmp, { force: true }); + try { + fs.rmSync(tmp, { force: true }); + } catch {} } } From e4c6891f77beb65ce4a84cbf8fd281ebcf98b390 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Wed, 14 Oct 2026 04:15:57 +0800 Subject: [PATCH 04/10] docs(media-use): clarify project preference file guidance --- skills-manifest.json | 2 +- skills/media-use/references/memory.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 95735f55b0d..ec06fe7b446 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -54,7 +54,7 @@ "files": 1 }, "media-use": { - "hash": "95acf615ab0b96af", + "hash": "9321c2222a697048", "files": 112 }, "motion-graphics": { diff --git a/skills/media-use/references/memory.md b/skills/media-use/references/memory.md index 366a0227b88..4d325afa330 100644 --- a/skills/media-use/references/memory.md +++ b/skills/media-use/references/memory.md @@ -14,7 +14,7 @@ Only what the user actually confirmed gets recorded — never an inferred or def Preference records serialize writes with `preferences.json.lock` beside each tier's file. If a process stops while holding that lock, later records leave it in place and report "Settings are locked…". After confirming no HyperFrames process is recording preferences, delete the named lock file and retry. A locked personal tier does not fail the project record; personal promotion waits for a later record. -Keep `.media/preferences.json` committed, and add `.media/*.lock` to the project's `.gitignore` so transient locks are never shared with the team. +Keep the project's preference file committed, and add `.media/*.lock` to the project's `.gitignore` so transient locks are never shared with the team. ## Recipes — frozen video bundles From ca307ce5ad4d686ae4f99ee102519e12bca2d5dc Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sat, 24 Oct 2026 12:40:29 +0800 Subject: [PATCH 05/10] chore: prepare conflict resolution against current main --- skills-manifest.json | 78 +++++++------------------------------------- 1 file changed, 11 insertions(+), 67 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index ec06fe7b446..e6442718b8a 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -1,89 +1,33 @@ { "source": "heygen-com/hyperframes", "skills": { - "embedded-captions": { - "hash": "1dc7ec2fd4a6e3fc", - "files": 145 - }, - "faceless-explainer": { - "hash": "bb2225ebaf7f9622", - "files": 29 - }, "figma": { - "hash": "f8cf9f2c78ea8374", + "hash": "af57c9b6411c81f5", "files": 2 }, - "general-video": { - "hash": "17dfbb1d34efb012", - "files": 6 - }, "hyperframes": { - "hash": "9787b91e4c16bf7f", - "files": 31 - }, - "hyperframes-animation": { - "hash": "a5d09375a50f71c8", - "files": 122 - }, - "hyperframes-audio": { - "hash": "780c9d4d8d23ab40", - "files": 8 + "hash": "8fc4b05db245cbbb", + "files": 77 }, "hyperframes-cli": { - "hash": "56a46bc15c308d98", - "files": 11 - }, - "hyperframes-core": { - "hash": "010a65915fc7b45e", - "files": 11 - }, - "hyperframes-creative": { - "hash": "8d0601ce26adc560", - "files": 79 - }, - "hyperframes-keyframes": { - "hash": "a3d43b98955b1366", - "files": 3 + "hash": "d82c156c1b2069d1", + "files": 13 }, "hyperframes-registry": { - "hash": "d7c1885fb4cce2cf", - "files": 12 - }, - "hyperframes-studio": { - "hash": "f5e0b44d68152c12", - "files": 1 + "hash": "221af9e7fbffe732", + "files": 13 }, "media-use": { - "hash": "9321c2222a697048", - "files": 112 - }, - "motion-graphics": { - "hash": "7e4af0881390ac2e", - "files": 23 - }, - "music-to-video": { - "hash": "b775f27b16efb1f2", - "files": 169 - }, - "pr-to-video": { - "hash": "d377b802ef54bc46", - "files": 35 - }, - "product-launch-video": { - "hash": "965b9b93bf19b173", - "files": 37 + "hash": "1082bd1c4e3f033b", + "files": 116 }, "remotion-to-hyperframes": { - "hash": "d02be65b7f3f4a63", + "hash": "3bf96d306215e384", "files": 77 }, "slideshow": { - "hash": "efc3c0be622fa9a1", + "hash": "c8a591e2aa7a6d86", "files": 2 - }, - "talking-head-recut": { - "hash": "4b2cf6d7b6e336b4", - "files": 28 } } } From f072486a464db40d2ecb732bb56c221385746d24 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sat, 24 Oct 2026 12:40:38 +0800 Subject: [PATCH 06/10] fix(media-use): refresh preference records against main --- docs/guides/media.mdx | 12 ++++++++++++ skills-manifest.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/guides/media.mdx b/docs/guides/media.mdx index 3e13dae9ed5..80398c5d2fe 100644 --- a/docs/guides/media.mdx +++ b/docs/guides/media.mdx @@ -53,6 +53,18 @@ make later revisions much easier for both people and agents. The original and prepared file should remain easy to distinguish. +## Remember confirmed preferences + +The media-use skill records confirmed choices in `.media/preferences.json`. +Concurrent records preserve each choice, including confirmations that promote a +preference to your personal defaults after use in two projects. + +Add `.media/*.lock` to the project's `.gitignore`, while keeping +`preferences.json` committed. If recording reports "Settings are locked…", +confirm that no HyperFrames process is recording preferences before deleting +the named lock file and retrying. A locked personal tier leaves the project +record intact and postpones personal promotion until a later record. + ## Check the final timeline Watch once from beginning to end and check: diff --git a/skills-manifest.json b/skills-manifest.json index e6442718b8a..8dbd6fa4e1a 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "1082bd1c4e3f033b", + "hash": "2424002b1640673a", "files": 116 }, "remotion-to-hyperframes": { From ac1ac4d89d8bf708713753163a6cad6784481332 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sun, 25 Oct 2026 09:48:20 +0800 Subject: [PATCH 07/10] chore: prepare conflict resolution against current main --- skills-manifest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 8dbd6fa4e1a..75aabb96626 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 2 }, "hyperframes": { - "hash": "8fc4b05db245cbbb", + "hash": "63994723e5533606", "files": 77 }, "hyperframes-cli": { @@ -18,11 +18,11 @@ "files": 13 }, "media-use": { - "hash": "2424002b1640673a", + "hash": "6550641984b19843", "files": 116 }, "remotion-to-hyperframes": { - "hash": "3bf96d306215e384", + "hash": "b585361591366636", "files": 77 }, "slideshow": { From 6acf696ba9244cb7418895df779f070d3ea116d2 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sun, 25 Oct 2026 09:48:27 +0800 Subject: [PATCH 08/10] chore: merge main and refresh preferences skill manifest --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index 75aabb96626..399dc14039d 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "6550641984b19843", + "hash": "5d9f0b1354c7efee", "files": 116 }, "remotion-to-hyperframes": { From 7dc50639b6c18e62c5fe3d723353d4626eef268a Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Mon, 26 Oct 2026 02:25:07 +0800 Subject: [PATCH 09/10] chore: prepare conflict resolution against current main --- skills-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 399dc14039d..a1564a8b5ff 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 2 }, "hyperframes": { - "hash": "63994723e5533606", + "hash": "76c914f47480032c", "files": 77 }, "hyperframes-cli": { @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "5d9f0b1354c7efee", + "hash": "0f1969abb1722e1a", "files": 116 }, "remotion-to-hyperframes": { From 4bc67d37439d0bec852f46f2fafac52bba197efc Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Mon, 26 Oct 2026 02:25:13 +0800 Subject: [PATCH 10/10] chore: merge main and refresh preferences skill manifest --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index a1564a8b5ff..377abb4ec3b 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "0f1969abb1722e1a", + "hash": "d411285e7a8fdc60", "files": 116 }, "remotion-to-hyperframes": {