luckiest-co 1.0.46 → 1.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "name": "luckiest",
10
10
  "source": "./",
11
11
  "description": "Plan, go, finish, plus bundled free Luckiest skills that work in Claude Code, web chat, and Cowork.",
12
- "version": "0.1.45",
12
+ "version": "0.1.46",
13
13
  "author": { "name": "Luckiest" }
14
14
  }
15
15
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luckiest",
3
3
  "description": "Plan, go, finish. Guided planning, progress dashboards, and your luckiest.co tribe inside Claude Code.",
4
- "version": "0.1.45",
4
+ "version": "0.1.46",
5
5
  "author": { "name": "Luckiest" }
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luckiest-co",
3
- "version": "1.0.46",
3
+ "version": "1.0.47",
4
4
  "description": "Luckiest for Claude Code: plan, go, finish. Your luckiest.co skills and tribe, inside Claude.",
5
5
  "bin": {
6
6
  "luckiest-co": "./bin/install.js"
@@ -1,5 +1,9 @@
1
1
  # Changelog — luckiest-video-studio
2
2
 
3
+ ## 1.18.0 — 2026-10-13
4
+
5
+ - New QA gate `node qa.mjs logo <run-dir> [--svg path] [--max 8]`: for every logo scene it takes the clip's last frame, finds the mark's box, draws the logo SVG (default `brand/logo.svg`) into the same box on the same background with HyperFrames, and fails when they differ. Enforces the "resolve frame equals the master logo" rule in the style cards' Logo construction sections.
6
+
3
7
  ## 1.17.3 — 2026-10-13
4
8
 
5
9
  - Logo construction on the `mosaic`, `pixel` and `flat-vector` style cards: mosaic lays stones along the mark's outline then fill and grout; pixel draws the mark into the low-res grid then steps up to the crisp SVG; flat-vector flies in the mark's own shapes with overshoot. Each resolves to the flat logo SVG (a visible difference is a Block).
@@ -4,7 +4,7 @@ description: "Make product launch videos, product demos with motion design betwe
4
4
  license: See ATTRIBUTION.md
5
5
  allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion
6
6
  metadata:
7
- version: "1.17.3"
7
+ version: "1.18.0"
8
8
  listing_id: luckiest-video-studio
9
9
  author: luckiest
10
10
  ---
@@ -19,13 +19,13 @@ an MP4.
19
19
  ## Staying current
20
20
 
21
21
  On activation, call the Luckiest MCP `check_updates` tool with
22
- `{ listingId: "aff888e4-9ee9-4f73-af08-74d5d09d3c10", installedSemver: "1.17.3" }`. If it returns
22
+ `{ listingId: "aff888e4-9ee9-4f73-af08-74d5d09d3c10", installedSemver: "1.18.0" }`. If it returns
23
23
  `upToDate: false`, surface the `notice` to the user once, then continue. Do nothing
24
24
  further if `upToDate: true`. Never block on this check; if the tool is unavailable,
25
25
  proceed.
26
26
 
27
27
  When the skill's work is done, call the Luckiest MCP `report_usage` tool once with
28
- `{ listing_id: "luckiest-video-studio", skill_version: "1.17.3", matched: true, success: <true if the skill completed, false otherwise> }`.
28
+ `{ listing_id: "luckiest-video-studio", skill_version: "1.18.0", matched: true, success: <true if the skill completed, false otherwise> }`.
29
29
  Metadata only, never prompt text. Never block on it; if the tool is unavailable,
30
30
  skip silently.
31
31
 
@@ -313,6 +313,7 @@ node <skill-dir>/scripts/qa.mjs avsync <output-dir>/final.mp4 --expect <hero
313
313
  node <skill-dir>/scripts/qa.mjs facts <output-dir> # on-screen numbers not in storyboard facts, em dashes on screen
314
314
  node <skill-dir>/scripts/qa.mjs loop <output-dir>/final.mp4 # only for looping pieces: last frame matches the first
315
315
  node <skill-dir>/scripts/qa.mjs safe <output-dir> # text or logo edges in each format's platform-covered margins
316
+ node <skill-dir>/scripts/qa.mjs logo <output-dir> [--svg <logo.svg>] # logo scenes: last frame vs the logo SVG in the same box (default brand/logo.svg)
316
317
  node <skill-dir>/scripts/qa.mjs endhold <output-dir>/final.mp4 # end card held over 1.4 s, final shot over a quarter of the film
317
318
  node <skill-dir>/scripts/qa.mjs blankframes <output-dir>/final.mp4 # more than 3 frames of empty swatch in a row
318
319
  node <skill-dir>/scripts/qa.mjs hud <output-dir>/composition # HUD counters, timecodes, BPM/FPS, greetings, "label · $value"
@@ -20,13 +20,15 @@
20
20
  // node qa.mjs storyboard <run-dir> scene lengths, picture-only, text-led share, camera streaks, style id
21
21
  // node qa.mjs facts <run-dir> on-screen numbers not in storyboard facts, em dashes on screen
22
22
  // node qa.mjs safe <run-dir> [--max 0.004] [--stills] text or logo edges in the margins each format's platforms cover
23
+ // node qa.mjs logo <run-dir> [--svg brand/logo.svg] [--max 8] last frame of each logo scene must match the logo SVG drawn in the same box
23
24
  // node qa.mjs judge <new.mp4> <old.mp4> [--out dir] [--cmd "claude -p --model sonnet"] [--verdicts A,B]
24
25
  // new vs old, asked twice with the order swapped; the new one is kept only if it wins both
25
26
  //
26
27
  // A gate that has never failed proves nothing: qa.test.mjs runs each one on a control
27
28
  // built to fail it.
28
29
 
29
- import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, writeFileSync } from "node:fs";
30
+ import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
31
+ import { tmpdir } from "node:os";
30
32
  import { join, resolve, extname } from "node:path";
31
33
  import { spawnSync } from "node:child_process";
32
34
  import { fileURLToPath } from "node:url";
@@ -556,6 +558,65 @@ B: ${b}
556
558
  Open both images. Judge which reads better to a viewer: every card legible, the speaker's face never covered, visuals change with what is said, no empty or cluttered stretches, real evidence instead of filler, and motion that feels designed: elements arrive fast and settle instead of creeping in or popping from nothing, moves match each other in speed and easing, and nothing drifts while it is being read.
557
559
  Name the two biggest differences, then end with exactly one line: WINNER: A or WINNER: B`;
558
560
 
561
+ // ---------- logo resolve ----------
562
+
563
+ // The SVG drawn into `box` on a plain `bg` page at W x H, captured with HyperFrames (headless Chrome),
564
+ // the same snapshot path render-scenes.mjs --stills uses. The mark is fitted by its own bounding box.
565
+ export function rasterLogo({ svg, W, H, box, bg, out, version = "latest" }) {
566
+ const proj = mkdtempSync(join(tmpdir(), "qa-logo-"));
567
+ writeFileSync(join(proj, "index.html"), `<!doctype html><html><head><meta charset="UTF-8"><script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
568
+ <style>*{margin:0;padding:0}html,body{width:${W}px;height:${H}px;overflow:hidden;background:${bg}}#m{position:absolute;left:${box.x}px;top:${box.y}px;width:${box.w}px;height:${box.h}px}#m svg{width:100%;height:100%;display:block}</style></head>
569
+ <body><div id="root" data-composition-id="main" data-start="0" data-duration="1" data-width="${W}" data-height="${H}"><div id="m" class="clip" data-start="0" data-duration="1" data-track-index="0">${readFileSync(svg, "utf8")}</div></div>
570
+ <script>const s=document.querySelector("#m svg"),b=s.getBBox();s.setAttribute("viewBox",[b.x,b.y,b.width,b.height].join(" "));
571
+ window.__timelines={main:gsap.timeline({paused:true}).to({},{duration:1})};</script></body></html>`);
572
+ const hf = spawnSync("hyperframes", ["--version"]).error ? ["npx", `hyperframes@${version}`] : ["hyperframes"];
573
+ const r = spawnSync(hf[0], [...hf.slice(1), "snapshot", proj, "--at", "0.5", "--no-end", "-o", join(proj, ".snap"), "--describe", "false"], { encoding: "utf8" });
574
+ const png = existsSync(join(proj, ".snap")) && readdirSync(join(proj, ".snap")).find((f) => f.startsWith("frame-"));
575
+ if (r.status !== 0 || !png) throw new Error(`logo snapshot failed: ${(r.stderr ?? "").trim().split("\n").pop()}`);
576
+ ff(["-v", "error", "-y", "-i", join(proj, ".snap", png), "-vf", `scale=${W}:${H}`, out]);
577
+ rmSync(proj, { recursive: true, force: true });
578
+ return out;
579
+ }
580
+
581
+ // Each logo scene's last frame against the logo SVG drawn at the same size and position.
582
+ // The box is the ink that differs from the frame's corner color; any visible difference inside it is a Block.
583
+ // ponytail: box from ink, so a caption beside the mark widens it and fails; add a scene logoBox override when a run needs it.
584
+ export function logoResolve(runDir, { svg, max = 8, raster = rasterLogo, ink = 24 } = {}) {
585
+ const sb = JSON.parse(readFileSync(join(runDir, "storyboard.json"), "utf8"));
586
+ const logo = resolve(runDir, svg ?? sb.brand?.logo ?? "brand/logo.svg");
587
+ const targets = Array.isArray(sb.formats) ? sb.formats.map((f) => [f, ...FORMATS[f], join(runDir, "clips", f)]) : [[null, sb.format.width, sb.format.height, join(runDir, "clips")]];
588
+ const hits = [], tmp = mkdtempSync(join(tmpdir(), "qa-logo-"));
589
+ let checked = 0;
590
+ for (const [fmt, W, H, dir] of targets) {
591
+ const w = Math.round(W / 4), h = Math.round(H / 4);
592
+ sb.scenes.forEach((s, i) => {
593
+ if (s.kind !== "logo") return;
594
+ const name = `${String(i + 1).padStart(2, "0")}-${s.id}`, clip = join(dir, `${name}.mp4`), tag = { ...(fmt ? { format: fmt } : {}), scene: s.id };
595
+ if (!existsSync(clip)) return hits.push({ ...tag, missing: clip });
596
+ const last = join(tmp, `${fmt ?? "main"}-${name}.png`);
597
+ ff(["-v", "error", "-y", "-sseof", "-0.25", "-i", clip, "-update", "1", last]);
598
+ const [f] = grayFrames(last, w, h);
599
+ if (!f) return hits.push({ ...tag, missing: last });
600
+ let x0 = w, y0 = h, x1 = -1, y1 = -1;
601
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
602
+ if (Math.abs(f[y * w + x] - f[0]) > ink) { x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y); }
603
+ }
604
+ if (x1 < 0) return hits.push({ ...tag, blank: true });
605
+ const rgb = ff(["-v", "error", "-i", last, "-vf", "crop=1:1:0:0,format=rgb24", "-f", "rawvideo", "-"]).stdout;
606
+ const bg = `#${[...rgb.subarray(0, 3)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
607
+ const box = { x: x0 * 4, y: y0 * 4, w: (x1 - x0 + 1) * 4, h: (y1 - y0 + 1) * 4 };
608
+ const [ref] = grayFrames(raster({ svg: logo, W, H, box, bg, out: join(tmp, `${fmt ?? "main"}-${name}-ref.png`), version: sb.hyperframes }), w, h);
609
+ let sum = 0, n = 0;
610
+ for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { sum += Math.abs(f[y * w + x] - ref[y * w + x]); n++; }
611
+ const diff = Number((sum / n).toFixed(2));
612
+ checked++;
613
+ if (diff > max) hits.push({ ...tag, diff, box });
614
+ });
615
+ }
616
+ rmSync(tmp, { recursive: true, force: true });
617
+ return { ok: hits.length === 0, hits, checked, svg: logo };
618
+ }
619
+
559
620
  // ---------- CLI ----------
560
621
 
561
622
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
@@ -563,7 +624,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
563
624
  if (gate === "judge" && rest[0] && !rest[0].startsWith("--")) rest.unshift("--old", rest.shift());
564
625
  const opt = (n, d) => { const i = rest.indexOf(n); return i >= 0 ? rest[i + 1] : d; };
565
626
  const num = (n, d) => Number(opt(n, d));
566
- if (!gate || !target) { console.error("Usage: qa.mjs <tokens|crossfade|motion|legibility|pops|deadframes|presence|beatsync|beatgrid|face|avsync|loop|endhold|blankframes|hud|facts|safe|judge> <path> [options]"); process.exit(1); }
627
+ if (!gate || !target) { console.error("Usage: qa.mjs <tokens|crossfade|motion|legibility|pops|deadframes|presence|beatsync|beatgrid|face|avsync|loop|endhold|blankframes|hud|facts|safe|logo|judge> <path> [options]"); process.exit(1); }
567
628
  let result;
568
629
  if (gate === "tokens") {
569
630
  const tokens = opt("--tokens") ? JSON.parse(readFileSync(opt("--tokens"), "utf8")) : DEFAULT_TOKENS;
@@ -607,6 +668,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
607
668
  }
608
669
  else if (gate === "storyboard") result = lintStoryboard(JSON.parse(readFileSync(join(target, "storyboard.json"), "utf8")));
609
670
  else if (gate === "safe") result = safeAreas(target, { max: num("--max", 0.004), stills: rest.includes("--stills") });
671
+ else if (gate === "logo") result = logoResolve(target, { svg: opt("--svg"), max: num("--max", 8) });
610
672
  else if (gate === "facts") {
611
673
  const sb = JSON.parse(readFileSync(join(target, "storyboard.json"), "utf8"));
612
674
  const comp = join(target, "composition");
@@ -10,7 +10,7 @@ import {
10
10
  scanTokens, scanCrossfades, scanMotion, legibility, findPops, deadFrames, presence,
11
11
  beatSync, findAnchor, cutTimes, beatGrid, grayFrames, probe,
12
12
  faceCover, pairVerdict, parseWinner, audioOnsets, avSync, visibleText, scanFacts, loopCheck,
13
- fullFrameSelectors, safeAreas, endHold, blankRuns, scanHud, lintStoryboard,
13
+ fullFrameSelectors, safeAreas, endHold, blankRuns, scanHud, lintStoryboard, logoResolve,
14
14
  } from "./qa.mjs";
15
15
 
16
16
  const dir = mkdtempSync(join(tmpdir(), "qa-gates-"));
@@ -209,6 +209,34 @@ test("loop: last frame matching the first passes, a different end frame fails",
209
209
  assert.equal(cli("loop", ends).status, 1);
210
210
  });
211
211
 
212
+ test("logo: a sting ending on the logo passes, one ending on a different shape fails", () => {
213
+ // A dark mark at 120,60 (80x60) on paper; the stand-in rasterizer draws the "logo" as that filled box.
214
+ const run = (name, end) => {
215
+ const r = join(dir, name);
216
+ mkdirSync(join(r, "clips"), { recursive: true });
217
+ const g = spawnSync("ffmpeg", ["-v", "error", "-y", "-f", "lavfi", "-i", "color=c=0xf3efe6:size=320x180:rate=30:duration=1",
218
+ "-vf", `drawbox=x=120:y=60:w=80:h=60:color=0x1a1a1a:t=${end},format=yuv420p`, "-c:v", "libx264", "-crf", "12", join(r, "clips", "01-sting.mp4")]);
219
+ assert.equal(g.status, 0, g.stderr?.toString());
220
+ writeFileSync(join(r, "storyboard.json"), JSON.stringify({ format: { width: 320, height: 180, fps: 30 }, scenes: [{ id: "sting", kind: "logo", duration: 1 }] }));
221
+ return r;
222
+ };
223
+ const raster = ({ W, H, box, bg, out }) => {
224
+ const g = spawnSync("ffmpeg", ["-v", "error", "-y", "-f", "lavfi", "-i", `color=c=${bg}:size=${W}x${H}`, "-vf", `drawbox=x=${box.x}:y=${box.y}:w=${box.w}:h=${box.h}:color=0x1a1a1a:t=fill`, "-frames:v", "1", out]);
225
+ assert.equal(g.status, 0, g.stderr?.toString());
226
+ return out;
227
+ };
228
+ const same = logoResolve(run("logo-same", "fill"), { raster });
229
+ assert.equal(same.ok, true, JSON.stringify(same));
230
+ assert.equal(same.checked, 1);
231
+ const off = logoResolve(run("logo-outline", 6), { raster });
232
+ assert.equal(off.ok, false);
233
+ assert.equal(off.hits[0].scene, "sting");
234
+ const missing = join(dir, "logo-missing");
235
+ mkdirSync(missing, { recursive: true });
236
+ writeFileSync(join(missing, "storyboard.json"), JSON.stringify({ format: { width: 320, height: 180, fps: 30 }, scenes: [{ id: "sting", kind: "logo", duration: 1 }] }));
237
+ assert.equal(cli("logo", missing).status, 1);
238
+ });
239
+
212
240
  test("endhold: a 2 s static end card fails, a short resolved hold and a one-shot film pass", () => {
213
241
  const long = video("endhold-long.mp4", [["testsrc", 2], ["0x202020", 2]]);
214
242
  const r = endHold(long);