@litfamily/litgrok 1.0.30 → 1.0.31

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.
@@ -203,7 +203,7 @@ An old agent name, prefixed or not, selects the renamed agent and prints that ro
203
203
 
204
204
  These rows are prompt guidance, not a router: old slash routes are not guaranteed; never create old skill directories or invent command metadata.
205
205
 
206
- <!-- litfamily:litgrok:start v=1.0.30 pre=0a -->
206
+ <!-- litfamily:litgrok:start v=1.0.31 pre=0a -->
207
207
  ## LitGrok ground rules
208
208
 
209
209
  Grok Build reads this file on every turn. These lines are the working agreement. The sections above still choose the skill.
@@ -15,6 +15,7 @@ Allowed files: html, js, mjs, css, svg, png, jpg, webp, gif, wav, woff, woff2, t
15
15
  stage directory.
16
16
  - Rasters are textures and stills, never a frame sequence: at most 24 raster images and 8 MB in
17
17
  total. Ten or more rasters of one size (a flipbook), or any animated GIF, APNG or WebP, is exit 17.
18
+ The one frame sequence a page may carry is a declared keyed clip (Clips).
18
19
  - Forbidden, exit 17 naming it: `<video>`, `<audio>`, `<iframe>`, `<object>`, `<embed>`, `<frame>`,
19
20
  `new Audio()`, `AudioContext`, `OfflineAudioContext`, Worker, SharedWorker, service workers,
20
21
  WebSocket, WebTransport, RTCPeerConnection.
@@ -25,6 +26,8 @@ Allowed files: html, js, mjs, css, svg, png, jpg, webp, gif, wav, woff, woff2, t
25
26
  <script src="/lit/stage-kit.js"></script>
26
27
  ```
27
28
 
29
+ A page with clips also loads `<script src="/lit/clips.js"></script>` (Clips).
30
+
28
31
  `/lit/fonts.css` declares the verified faces with `font-display: block`: `Archivo` (weights 400,
29
32
  700, 900; `font-stretch` 75 %, 100 %, 125 %), `Pretendard` (Hangul, 400 and 700), `VT323`,
30
33
  `Silkscreen` (400, 700), `Galmuri9` and `Meslo` (both need the pre-warm), and `JetBrains Mono`
@@ -88,11 +91,58 @@ LitStage.drawPath(svgPath, p); // stroke draw-on
88
91
  const shape = LitStage.morph(dA, dB); path.setAttribute('d', shape(p)); // single-subpath morph
89
92
  LitStage.clip(el, p, { from: 'left' }); // inset reveal; LitStage.circle(el, p, { x, y })
90
93
  const c = LitStage.mix('#0b1f33', '#7fd1ff', p); // colour mix
94
+ const show = LitStage.overlay({ parent, id, box }); // a declared keyed clip (Clips); show(t, { start })
91
95
  ```
92
96
 
93
97
  `morph` normalizes both paths to cubics, resamples them by arc length and picks the best rotation;
94
98
  paths with more than one subpath are not supported (split them into separate paths).
95
99
 
100
+ ## Clips
101
+
102
+ Real keyed footage (a reaction clip, a spinning-cat loop shot on green screen) joins the film as a
103
+ layer inside the page. Key the video outside the renderer into one 8-bit RGBA PNG per frame, declare
104
+ the frames, and the page picks the frame from film time. A declared clip leaves the flipbook and
105
+ count rule; every other raster keeps its limits. Nothing is encoded by hand: the page is captured
106
+ frame by frame as usual, so the flash audit, the determinism replay and the text checks run on the
107
+ final pixels.
108
+
109
+ 1. Copy each clip's frames into the run's stage folder as `stage/clips/<dir>/frame_0001.png`,
110
+ `frame_0002.png` and so on, with no gaps and no other image in the folder.
111
+ 2. Declare them in `stage/clips/clips.json`:
112
+
113
+ ```json
114
+ { "clips": [ { "id": "boom", "dir": "01-vine-boom", "count": 122, "fps": 30 } ] }
115
+ ```
116
+
117
+ `id` is the name the page uses, `dir` a folder directly under `clips/`, `count` the number of
118
+ frames and `fps` the clip's own rate, 1 to 30. A clip used twice is declared once.
119
+ 3. The page loads `<script src="/lit/clips.js"></script>` after the kit, registers each use once with
120
+ `LitStage.overlay({ parent, id, box })` and calls the returned `show(t, opts)` from `render(t)`.
121
+ (`LitStage.clip` is the inset reveal; it has nothing to do with clips.) `parent` is the element
122
+ the clip sits in; `box` is its place in pixels, the frame fitted inside it. `show` returns the
123
+ clip's wrapper element, or `null` while hidden; scale, fade or move that wrapper from `t` like any
124
+ other layer.
125
+
126
+ ```js
127
+ const boom = LitStage.overlay({ parent: stageEl, id: 'boom', box: { x: 1200, y: 300, w: 520, h: 511 } });
128
+ render(t) { boom(t, { start: 2.1 }); }
129
+ ```
130
+
131
+ The frame is `floor((t - start) * fps) + 1`, counted from `from`, so a cue is plain arithmetic: a
132
+ clip whose own sound starts at frame 85 and must land on a cue at 0.12 s has
133
+ `start = 0.12 - 84 / 30`. Options: `start` (film seconds, default 0), `from` and `to` (the frame
134
+ range, default the whole clip), `loop` (wrap by frame index) and `hold` (keep the last frame).
135
+ Before `start` and past `to`, without `loop` or `hold`, the clip is hidden.
136
+
137
+ The scan refuses, with exit 17 and the clip named: more than 12 clips, 360 frames in a clip or 1500
138
+ in all, 40 MB in a clip or 128 MB in all, a frame whose long side is above 720 px (a clip is an
139
+ overlay, not the film), an fps above 30, frames of mixed sizes, a missing or extra frame, a frame
140
+ that is not an 8-bit RGBA PNG or has no transparent pixel in the first, middle and last frame (an
141
+ opaque video is not keyed), an animated image, and a duplicate `id`. A folder that is not declared,
142
+ or any frame sequence beside a declared clip, is a flipbook as before. Cover mode takes no clips.
143
+ Keep clips clear of copy: the text checks read DOM text, so a clip laid over a line is yours to
144
+ catch on the stills. The manifest and the gate report list each clip.
145
+
96
146
  ## Serving and network
97
147
 
98
148
  The page is served from the synthetic origin `http://lit.stage/` with no listening socket. Only
@@ -0,0 +1,119 @@
1
+ // Declared keyed overlay clips on the stage path: stage/clips/clips.json names RGBA frame folders,
2
+ // scanClips checks every declared frame against its bytes, and clipsScript writes the table the page
3
+ // reads as window.LitClips (served at /lit/clips.js). See references/stage.md, Clips.
4
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { CLIP_LIMITS } from './constants.mjs';
7
+ import { decodePng } from './png.mjs';
8
+
9
+ export const CLIPS_DIR = 'clips';
10
+ export const clipFrame = (i) => `frame_${String(i).padStart(4, '0')}.png`;
11
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/;
12
+ const DIR = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
13
+ const MB = 1024 * 1024;
14
+
15
+ /** Header facts of a PNG: size, bit depth, colour type (6 = RGBA) and whether it is an APNG. */
16
+ function pngHeader(bytes) {
17
+ if (bytes.length < 33 || bytes.toString('latin1', 1, 4) !== 'PNG') return null;
18
+ let animated = false;
19
+ for (let o = 8; o + 8 <= bytes.length;) {
20
+ const type = bytes.toString('latin1', o + 4, o + 8);
21
+ if (type === 'acTL') animated = true;
22
+ if (type === 'IDAT' || type === 'IEND') break;
23
+ o += 12 + bytes.readUInt32BE(o);
24
+ }
25
+ return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20), bitDepth: bytes[24], colorType: bytes[25], animated };
26
+ }
27
+
28
+ /** The clips stage/clips/clips.json declares ([] without the file), checked for shape only. Throws Error. */
29
+ export function declaredClips(stageDir, limits = CLIP_LIMITS) {
30
+ const file = join(stageDir, CLIPS_DIR, 'clips.json');
31
+ if (!existsSync(file)) return [];
32
+ let doc;
33
+ try { doc = JSON.parse(readFileSync(file, 'utf8')); } catch (error) { throw new Error(`${CLIPS_DIR}/clips.json is not valid JSON (${error.message})`); }
34
+ if (!doc || !Array.isArray(doc.clips)) throw new Error(`${CLIPS_DIR}/clips.json needs { "clips": [ { "id", "dir", "count", "fps" } ] }`);
35
+ if (doc.clips.length > limits.clips) throw new Error(`${doc.clips.length} clips declared (at most ${limits.clips} clips)`);
36
+ const ids = new Set();
37
+ const dirs = new Set();
38
+ return doc.clips.map((c, n) => {
39
+ const at = `${CLIPS_DIR}/clips.json clip #${n + 1}`;
40
+ if (!c || typeof c !== 'object') throw new Error(`${at} is not an object`);
41
+ if (typeof c.id !== 'string' || !ID.test(c.id)) throw new Error(`${at}: id must be a plain token of letters, digits, - and _ (got ${JSON.stringify(c.id)})`);
42
+ if (ids.has(c.id)) throw new Error(`${at}: duplicate id ${c.id}`);
43
+ ids.add(c.id);
44
+ if (typeof c.dir !== 'string' || !DIR.test(c.dir) || c.dir.includes('..')) throw new Error(`clip "${c.id}": dir must be a folder name directly under ${CLIPS_DIR}/ (got ${JSON.stringify(c.dir)})`);
45
+ if (dirs.has(c.dir)) throw new Error(`clip "${c.id}": dir ${c.dir} is already declared by another clip`);
46
+ dirs.add(c.dir);
47
+ if (!Number.isInteger(c.count) || c.count < 1) throw new Error(`clip "${c.id}": count must be a whole number of frames (got ${JSON.stringify(c.count)})`);
48
+ if (!Number.isInteger(c.fps) || c.fps < 1 || c.fps > limits.fps) throw new Error(`clip "${c.id}": fps must be a whole number from 1 to ${limits.fps} (got ${JSON.stringify(c.fps)})`);
49
+ return { id: c.id, dir: c.dir, count: c.count, fps: c.fps };
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Check every declared clip against its bytes. Each clip needs exactly frame_0001.png..frame_<count>.png in clips/<dir>/, every one an
55
+ * 8-bit still RGBA PNG of one size within the overlay cap, a transparent pixel in the first, middle and
56
+ * last frame, and the per-clip and total caps. Throws Error naming the clip; returns
57
+ * [{ id, dir, count, fps, width, height, bytes, frames }] where `frames` are stage-relative paths.
58
+ */
59
+ export function scanClips(stageDir, { limits = CLIP_LIMITS } = {}) {
60
+ const decls = declaredClips(stageDir, limits);
61
+ let totalFrames = 0;
62
+ let totalBytes = 0;
63
+ const clips = decls.map((d) => {
64
+ if (d.count > limits.framesPerClip) throw new Error(`clip "${d.id}": ${d.count} frames (at most ${limits.framesPerClip} per clip)`);
65
+ const prefix = `${CLIPS_DIR}/${d.dir}/`;
66
+ const folder = join(stageDir, CLIPS_DIR, d.dir);
67
+ const have = new Set(existsSync(folder) ? readdirSync(folder, { withFileTypes: true }).filter((e) => e.isFile() && /\.(png|jpe?g|webp|gif)$/i.test(e.name)).map((e) => e.name) : []);
68
+ const frames = [];
69
+ for (let i = 1; i <= d.count; i += 1) {
70
+ if (!have.has(clipFrame(i))) throw new Error(`clip "${d.id}": ${prefix}${clipFrame(i)} is missing (declared ${d.count} frames, named frame_0001.png onward)`);
71
+ frames.push(`${prefix}${clipFrame(i)}`);
72
+ }
73
+ const stray = [...have].find((name) => !frames.includes(`${prefix}${name}`));
74
+ if (stray) throw new Error(`clip "${d.id}": ${prefix}${stray} is not one of the ${d.count} declared frames; raise count or remove it`);
75
+ let size = null;
76
+ let bytes = 0;
77
+ const read = (rel) => readFileSync(join(stageDir, ...rel.split('/')));
78
+ const headers = frames.map((rel) => {
79
+ const buffer = read(rel);
80
+ bytes += buffer.length;
81
+ const info = pngHeader(buffer);
82
+ if (!info) throw new Error(`clip "${d.id}": ${rel} is not a readable PNG`);
83
+ if (info.animated) throw new Error(`clip "${d.id}": ${rel} is an animated image; a clip is a folder of still frames`);
84
+ if (info.colorType !== 6 || info.bitDepth !== 8) throw new Error(`clip "${d.id}": ${rel} must be an 8-bit RGBA PNG with an alpha channel (a keyed frame), not an opaque image`);
85
+ if (Math.max(info.width, info.height) > limits.side) throw new Error(`clip "${d.id}": ${rel} is ${info.width}x${info.height}; a clip is an overlay of at most ${limits.side} px on the long side`);
86
+ if (size && (size.width !== info.width || size.height !== info.height)) throw new Error(`clip "${d.id}": ${rel} is ${info.width}x${info.height} but frame_0001.png is ${size.width}x${size.height}; one size per clip`);
87
+ size = { width: info.width, height: info.height };
88
+ return buffer;
89
+ });
90
+ if (bytes > limits.bytesPerClip) throw new Error(`clip "${d.id}": ${(bytes / MB).toFixed(1)} MB (at most ${(limits.bytesPerClip / MB).toFixed(0)} MB per clip)`);
91
+ for (const at of [0, Math.floor((frames.length - 1) / 2), frames.length - 1]) {
92
+ let pixels;
93
+ try { ({ pixels } = decodePng(headers[at])); } catch (error) { throw new Error(`clip "${d.id}": ${frames[at]} cannot be decoded (${error.message}); use a non-interlaced 8-bit RGBA PNG`); }
94
+ let keyed = false;
95
+ for (let i = 3; i < pixels.length && !keyed; i += 4) keyed = pixels[i] < 255;
96
+ if (!keyed) throw new Error(`clip "${d.id}": ${frames[at]} has no transparent pixel; a clip is keyed, not a full-frame video`);
97
+ }
98
+ totalFrames += d.count;
99
+ totalBytes += bytes;
100
+ return { ...d, ...size, bytes, frames };
101
+ });
102
+ if (totalFrames > limits.framesTotal) throw new Error(`${totalFrames} clip frames in all (at most ${limits.framesTotal})`);
103
+ if (totalBytes > limits.bytesTotal) throw new Error(`${(totalBytes / MB).toFixed(1)} MB of clip frames in all (at most ${(limits.bytesTotal / MB).toFixed(0)} MB)`);
104
+ return clips;
105
+ }
106
+
107
+ /**
108
+ * The script served at /lit/clips.js: window.LitClips = { id: { src, count, fps, width, height } }, from
109
+ * clips.json and each first frame's header. null when the stage declares no clips, or one the scan refuses.
110
+ */
111
+ export function clipsScript(stageDir) {
112
+ try {
113
+ const table = Object.fromEntries(declaredClips(stageDir).map((c) => {
114
+ const info = pngHeader(readFileSync(join(stageDir, CLIPS_DIR, c.dir, clipFrame(1))));
115
+ return [c.id, { src: `${CLIPS_DIR}/${c.dir}`, count: c.count, fps: c.fps, width: info.width, height: info.height }];
116
+ }));
117
+ return Object.keys(table).length ? `window.LitClips = ${JSON.stringify(table)};\n` : null;
118
+ } catch { return null; }
119
+ }
@@ -195,6 +195,19 @@ export const STAGE = Object.freeze({
195
195
  determinismMax: 16,
196
196
  });
197
197
 
198
+ // Keyed overlay clips (references/stage.md, Clips): frame folders declared in stage/clips/clips.json
199
+ // are held to these caps instead of the raster limits above. A clip is a sticker on the film, never
200
+ // the film: 8-bit RGBA PNG frames with real transparency, at most `side` px on the long side.
201
+ export const CLIP_LIMITS = Object.freeze({
202
+ clips: 12,
203
+ framesPerClip: 360,
204
+ framesTotal: 1500,
205
+ bytesPerClip: 40 * 1024 * 1024,
206
+ bytesTotal: 128 * 1024 * 1024,
207
+ side: 720,
208
+ fps: 30,
209
+ });
210
+
198
211
  // Cover mode (references/cover.md): the README cover loop and the gate that reads its DECODED file.
199
212
  // Calibrated on 2026-10-20 against eight earlier painted covers, four slide-like negatives and
200
213
  // three pilot loops; a number moves only with a new calibration table. FLOOR fails a cover (every
@@ -137,7 +137,7 @@ function prepare({ out, env }) {
137
137
  const treatment = loadCoverTreatment(out);
138
138
  const dir = join(out, COVER_DIR);
139
139
  if (!existsSync(join(dir, 'index.html'))) throw new BlockedError(EXIT.STAGE_CONTRACT, `STAGE_CONTRACT_ERROR: ${join(dir, 'index.html')} is missing; write the cover page first (start from a recipe in recipes/)`);
140
- const scan = scanStage(dir);
140
+ const scan = scanStage(dir, { clips: false });
141
141
  const rename = (line) => line.replaceAll('stage/', `${COVER_DIR}/`);
142
142
  if (scan.network.length) throw new BlockedError(EXIT.STAGE_NETWORK, `STAGE_NETWORK_REQUEST: ${rename(scan.network[0])}; a cover loads only its own files and the /lit/ routes`);
143
143
  if (scan.contract.length) throw new BlockedError(EXIT.STAGE_CONTRACT, `STAGE_CONTRACT_ERROR: ${rename(scan.contract[0])}`);
@@ -124,6 +124,7 @@ export function stageReportText({ manifest, gate, withheld, perf, outDir, treatm
124
124
  lines.push(`path: stage · format ${manifest.format} (${manifest.resolution.join('x')}) · ${manifest.durationSec.toFixed(2)} s of ${treatment.durationSec} s target @ ${manifest.fps} fps`);
125
125
  lines.push(`sound: ${manifest.sound?.label ?? 'n/a'}${manifest.sound?.timbre ? ` (${manifest.sound.timbre}, ${manifest.sound.key}, ${manifest.sound.tempo} BPM, ${manifest.sound.lufs?.toFixed?.(1)} LUFS)` : ''}`);
126
126
  lines.push(`renderer: ${manifest.renderer}`);
127
+ if (manifest.clips?.length) lines.push(`clips: ${manifest.clips.map((c) => `${c.id} ${c.frames} frames at ${c.fps} fps`).join(', ')} (keyed overlays, chosen from film time)`);
127
128
  lines.push('');
128
129
  lines.push(`QA gate: ${gate.status}`);
129
130
  for (const r of gate.results) lines.push(` ${`${r.id}:`.padEnd(30)}${r.status} (${r.detail})`);
@@ -7,6 +7,7 @@ import { extname, join, sep } from 'node:path';
7
7
  import { STAGE } from './constants.mjs';
8
8
  import { launchStage } from './chrome.mjs';
9
9
  import { SKILL_ROOT } from './fonts.mjs';
10
+ import { clipsScript } from './clips.mjs';
10
11
 
11
12
  export const CONTENT_TYPES = Object.freeze({
12
13
  '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
@@ -78,6 +79,7 @@ export async function openStage({ executable, outDir, stageDir, width, height, f
78
79
  const network = [];
79
80
  const refused = [];
80
81
  const served = new Set();
82
+ const clipsJs = clipsScript(stageDir);
81
83
  const kit = existsSync(KIT_PATH) ? readFileSync(KIT_PATH) : Buffer.from('/* stage kit missing */');
82
84
  const respond = (requestId, status, body, type) => browser.send('Fetch.fulfillRequest', {
83
85
  requestId, responseCode: status,
@@ -95,6 +97,7 @@ export async function openStage({ executable, outDir, stageDir, width, height, f
95
97
  return;
96
98
  }
97
99
  if (url.pathname === '/lit/stage-kit.js') { served.add('/lit/stage-kit.js'); await respond(requestId, 200, kit, CONTENT_TYPES['.js']); return; }
100
+ if (clipsJs && url.pathname === '/lit/clips.js') { served.add('/lit/clips.js'); await respond(requestId, 200, clipsJs, CONTENT_TYPES['.js']); return; }
98
101
  if (url.pathname.startsWith('/lit/three/')) {
99
102
  const resolved = resolveThree(url.pathname);
100
103
  if (resolved.refused) { refused.push(resolved.refused); await respond(requestId, 403, 'refused'); return; }
@@ -6,6 +6,7 @@ import { createHash } from 'node:crypto';
6
6
  import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { extname, join, relative } from 'node:path';
8
8
  import { BlockedError, DIRECTOR, EXIT, STAGE } from './constants.mjs';
9
+ import { scanClips } from './clips.mjs';
9
10
  import { resolveChrome, stageArgs } from './chrome.mjs';
10
11
  import { fontPath, fontState, PINS, resolveFontKey } from './fonts.mjs';
11
12
  import { FlashDetector } from './flash.mjs';
@@ -67,17 +68,24 @@ export function rasterInfo(bytes, ext) {
67
68
  }
68
69
 
69
70
  // Static scan before any launch: forbidden elements (17), absolute or protocol-relative URLs and
70
- // preconnect/prefetch links (19), raster limits, flipbooks and animated rasters (17).
71
- export function scanStage(stageDir) {
71
+ // preconnect/prefetch links (19), raster limits, flipbooks and animated rasters (17). Frames of a clip
72
+ // declared in clips/clips.json are checked by scanClips instead; `clips: false` (cover mode) ignores the
73
+ // declaration, so a declared folder is held to the raster limits like any other.
74
+ export function scanStage(stageDir, { clips: allowClips = true } = {}) {
72
75
  const contract = [];
73
76
  const network = [];
74
77
  const warnings = [];
75
- if (!existsSync(join(stageDir, 'index.html'))) return { contract: ['stage/index.html is missing'], network, warnings, files: [], rasters: [] };
78
+ if (!existsSync(join(stageDir, 'index.html'))) return { contract: ['stage/index.html is missing'], network, warnings, files: [], rasters: [], clips: [] };
76
79
  const files = walk(stageDir);
77
80
  // A Blender plate and its preview stills are frames the runner wrote and plate.json lists by name
78
81
  // and size; they are not page rasters. Anything not listed is held to the raster limits.
79
82
  const plates = [readPlate(stageDir, 'plate'), readPlate(stageDir, 'plate-preview')].filter(Boolean);
80
83
  const listed = new Set(plates.flatMap((p) => p.frames.map((f) => `${p.dir}/${String(f.name)}`)));
84
+ let clips = [];
85
+ if (allowClips) {
86
+ try { clips = scanClips(stageDir); } catch (error) { contract.push(error.message); }
87
+ for (const clip of clips) for (const rel of clip.frames) listed.add(rel);
88
+ }
81
89
  const rasters = [];
82
90
  for (const file of files) {
83
91
  if (file.symlink) continue;
@@ -112,7 +120,7 @@ export function scanStage(stageDir) {
112
120
  const bySize = new Map();
113
121
  for (const r of rasters) { const key = `${r.width}x${r.height}`; bySize.set(key, (bySize.get(key) ?? 0) + 1); }
114
122
  for (const [size, count] of bySize) if (count >= STAGE.flipbookCount) contract.push(`${count} rasters of one size (${size}) form a flipbook; rasters are textures and stills, never a frame sequence`);
115
- return { contract, network, warnings, files: files.map((f) => f.rel), rasters };
123
+ return { contract, network, warnings, files: files.map((f) => f.rel), rasters, clips };
116
124
  }
117
125
 
118
126
  // ---- fonts -----------------------------------------------------------------------------------
@@ -424,6 +432,7 @@ export async function renderStage({ out, treatment, round = 1, stillsOnly = fals
424
432
  schemaVersion: 1, path: 'stage', format: treatment.format, resolution: [width, height], fps, durationSec: totalFrames / fps, targetDurationSec: treatment.durationSec,
425
433
  frames: totalFrames, seed, rung: masterRung, renderer: `${masterRenderer} on the ${masterRung} rung (stage path)`, softwareRenderer: masterRung === 'software' || isSoftwareRenderer(masterRenderer), chromeFlags: masterArgs.filter((arg) => !arg.startsWith('--user-data-dir') && !arg.startsWith('--window-size') && arg !== 'about:blank'), previewEncoder: preview.encoder,
426
434
  sound: sound.summary, round, warnings,
435
+ clips: scan.clips.map((c) => ({ id: c.id, frames: c.count, fps: c.fps, width: c.width, height: c.height, bytes: c.bytes })),
427
436
  };
428
437
  const stills = writeStills(out, { plan, frames: kept, width, height, path: 'stage', round, mode: 'full', poster: { bytes: posterBytes, frame: posterFrame }, treatmentSha256 });
429
438
  // The final render ships its storyboard too: the done-check wants the board viewed on the final set.
@@ -280,6 +280,49 @@
280
280
  };
281
281
  }
282
282
 
283
+ // A declared keyed clip (stage/clips/clips.json, served as window.LitClips by /lit/clips.js): one
284
+ // wrapper at `box` over the film, holding the clip's frame for t. The frame is a pure function of film
285
+ // time, floor((t - start) * fps) + 1 within from..to, so no <video> and no real clock is involved.
286
+ // Before `start` and after `to` the wrapper is hidden (null) unless `hold` keeps the last frame or
287
+ // `loop` wraps by frame index. The wrapper is what a page scales or fades; the <img> inside is
288
+ // replaced on a frame change, so the renderer waits for its decode before the capture.
289
+ function overlay(opts) {
290
+ var all = root.LitClips, parent = opts && opts.parent, id = opts && opts.id, box = opts && opts.box;
291
+ if (!all) throw new Error('LitStage.overlay: load <script src="/lit/clips.js"></script> before the page script; the stage declares no clips (clips/clips.json)');
292
+ var c = all[id];
293
+ if (!c) throw new Error('LitStage.overlay: no clip "' + id + '" in clips/clips.json (declared: ' + Object.keys(all).join(', ') + ')');
294
+ if (!parent || !box || !(box.w > 0) || !(box.h > 0)) throw new TypeError('LitStage.overlay: pass { parent, id, box: { x, y, w, h } }');
295
+ var doc = parent.ownerDocument || root.document;
296
+ var wrap = doc.createElement('div');
297
+ wrap.setAttribute('data-lit-clip', id);
298
+ var ws = wrap.style;
299
+ ws.position = 'absolute'; ws.left = box.x + 'px'; ws.top = box.y + 'px'; ws.width = box.w + 'px'; ws.height = box.h + 'px';
300
+ ws.display = 'none'; ws.pointerEvents = 'none';
301
+ parent.append(wrap);
302
+ var shown = 0, img = null;
303
+ return function (t, o) {
304
+ o = o || {};
305
+ var start = o.start == null ? 0 : o.start, from = o.from == null ? 1 : o.from, to = o.to == null ? c.count : o.to;
306
+ if (!(from >= 1 && to <= c.count && from <= to)) throw new RangeError('LitStage.overlay: from..to must lie within 1..' + c.count + ' (got ' + from + '..' + to + ')');
307
+ var n = to - from + 1, k = Math.floor((t - start) * c.fps + 1e-6);
308
+ var i = k < 0 ? 0 : k < n ? from + k : o.loop ? from + (k % n) : o.hold ? to : 0;
309
+ wrap.style.display = i ? 'block' : 'none';
310
+ if (!i) return null;
311
+ if (i !== shown) {
312
+ var next = doc.createElement('img');
313
+ next.src = c.src + '/frame_' + String(i).padStart(4, '0') + '.png';
314
+ next.alt = '';
315
+ var ns = next.style;
316
+ ns.display = 'block'; ns.width = '100%'; ns.height = '100%'; ns.objectFit = 'contain';
317
+ if (img) img.replaceWith(next);
318
+ else wrap.append(next);
319
+ img = next;
320
+ shown = i;
321
+ }
322
+ return wrap;
323
+ };
324
+ }
325
+
283
326
  function rgb(hex) {
284
327
  var h = String(hex).replace('#', '');
285
328
  if (h.length === 3) h = h.split('').map(function (c) { return c + c; }).join('');
@@ -472,6 +515,7 @@
472
515
  clip: clip,
473
516
  circle: circle,
474
517
  plate: plate,
518
+ overlay: overlay,
475
519
  mix: mix,
476
520
  rgb: rgb,
477
521
  hex: hex,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litgrok",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Grok Build skills, project rules, and bounded lifecycle hooks for evidence-first lit work.",
5
5
  "author": {
6
6
  "name": "LitGrok contributors"
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.0.31 — 2026-11-01
6
+
7
+ - The motion skill's stage path can carry keyed overlay clips: real footage keyed on green screen outside the renderer into one RGBA PNG per frame, declared in `stage/clips/clips.json` (`id`, `dir`, `count`, `fps`). The page registers a clip once with `LitStage.overlay` and shows it from film time, so a cue is plain arithmetic and the flash audit, the determinism replay and the text checks still run on the final pixels. The scan holds clips to 12 per film, 360 frames each and 1500 in all, 40 MB each and 128 MB in all, 720 px on the long side and 30 fps, and refuses opaque or mixed-size frames with exit 17; an undeclared frame folder is still a flipbook, and cover mode takes no clips.
8
+
5
9
  ## 1.0.30 — 2026-10-31
6
10
 
7
11
  - `lit-adhd` counts other work before it tells this session what to do. A paused frame here is not resumed, and this session's own run is not continued with `go`, while another session or a herdr agent is still working. A start-work approval is still offered. The session's own card does not show `lit continue` while that work runs.
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion-still.webp" /><source media="(prefers-reduced-motion: no-preference)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitGrok robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion-still.webp" /><source media="(prefers-reduced-motion: no-preference)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitGrok robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
2
2
 
3
3
  <h1 align="center">LitGrok</h1>
4
4
  <p align="center"><strong>Keep the work lit.</strong></p>
5
5
 
6
6
  LitGrok gives xAI's Grok Build the habit of planning before it edits, checking what it built, and leaving a handoff, so tomorrow's session starts where today's stopped. One npm command adds its skills, project rule and hooks to your project.
7
7
 
8
- **[Full README on GitHub](https://github.com/wjgoarxiv/litgrok#readme)** · [Guide](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide.md) · [All skills](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills.md) · [한국어](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md) · [Install](#install-in-30-seconds) · [Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/reference.md)
8
+ **[Full README on GitHub](https://github.com/wjgoarxiv/litgrok#readme)** · [Guide](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide.md) · [All skills](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills.md) · [한국어](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md) · [Install](#install-in-30-seconds) · [Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/reference.md)
9
9
 
10
10
  <p align="center">
11
- <a href="#install-in-30-seconds"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/badge-version.svg" alt="1.0.30" /></a>
12
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
11
+ <a href="#install-in-30-seconds"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/badge-version.svg" alt="1.0.31" /></a>
12
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
13
13
  </p>
14
14
 
15
- <p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/reference.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/lucide-book-open.svg" width="16" alt="" /> Docs</a> &nbsp; <a href="#install-in-30-seconds">Install</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion.webp"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/lucide-play.svg" width="16" alt="" /> Cover motion</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/lucide-shield-check.svg" width="16" alt="" /> MIT</a></p>
15
+ <p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/reference.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/lucide-book-open.svg" width="16" alt="" /> Docs</a> &nbsp; <a href="#install-in-30-seconds">Install</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion.webp"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/lucide-play.svg" width="16" alt="" /> Cover motion</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/lucide-shield-check.svg" width="16" alt="" /> MIT</a></p>
16
16
 
17
17
  Long Grok Build sessions get compacted, a new session starts without what the last one knew, and "done" often just means the model said so. LitGrok gives each session a routine to follow: a written plan before the first edit, checks against that plan, and a handoff file the next session can read. It ships 42 skills, 11 agents, one project rule, and twelve hook registrations. Grok Build still runs the session and chooses the model. It suits work that runs longer than one sitting, such as a feature spread over a few days or a report someone else will read.
18
18
 
@@ -24,7 +24,7 @@ You need Node.js and Grok Build. In an interactive terminal, from the project yo
24
24
  npm exec --yes --package @litfamily/litgrok@latest -- litgrok install
25
25
  ```
26
26
 
27
- The files land in `<project>/.grok/`. Add `--user` to install under `~/.grok/` instead, or use `--package @litfamily/litgrok@1.0.30` to pin this release. Add `--dry-run` to see every destination before anything is written.
27
+ The files land in `<project>/.grok/`. Add `--user` to install under `~/.grok/` instead, or use `--package @litfamily/litgrok@1.0.31` to pin this release. Add `--dry-run` to see every destination before anything is written.
28
28
 
29
29
  Trying a local package instead? Put its real absolute path in a variable, then run these two lines separately:
30
30
 
@@ -73,7 +73,7 @@ Beyond code, `lit-pptx` makes slide decks and `lit-docx` makes reports and Word
73
73
 
74
74
  ## What changes after install
75
75
 
76
- The installer copies the skills, agents, project rule, and hooks into `.grok/`. The twelve hook registrations in [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/hooks/hooks.json) run only in a trusted Git root, and they log the order of events in `.grok/litgrok/session-ledger/`. Everything runs inside your Grok session, and nothing runs in the background; when the session ends, the work waits for the next one.
76
+ The installer copies the skills, agents, project rule, and hooks into `.grok/`. The twelve hook registrations in [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/hooks/hooks.json) run only in a trusted Git root, and they log the order of events in `.grok/litgrok/session-ledger/`. Everything runs inside your Grok session, and nothing runs in the background; when the session ends, the work waits for the next one.
77
77
 
78
78
  If you'd like a status row showing the active LitGrok skill, the model and how much context is used, add it with `install --user --status-line`. It writes `[ui.status_line]` into `~/.grok/config.toml` and backs up an existing file first. Once a prompt starts a workflow skill, the row also shows the workflow stage, the tool running now and the last test result. Add `--notify` for Grok's terminal notification when a turn finishes.
79
79
 
@@ -112,6 +112,6 @@ If the status row is missing, add it with `install --user --status-line`. Grok r
112
112
 
113
113
  If LitGrok saves you some re-explaining, a [star on GitHub](https://github.com/wjgoarxiv/litgrok) helps other Grok Build users find it.
114
114
 
115
- [Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/reference.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/CHANGELOG.md) · [Privacy](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/privacy.md) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/LICENSE) · [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/TRADEMARK.md)
115
+ [Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/reference.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/CHANGELOG.md) · [Privacy](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/privacy.md) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/LICENSE) · [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/TRADEMARK.md)
116
116
 
117
117
  The cover is brand motion made with the LitFamily motion skill: animated artwork, with no Grok session recorded in it.
package/README_ko-KR.md CHANGED
@@ -1,18 +1,18 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion-still.webp" /><source media="(prefers-reduced-motion: no-preference)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 일곱 로봇 패널이 차례로 켜지고, LitGrok 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion-still.webp" /><source media="(prefers-reduced-motion: no-preference)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 일곱 로봇 패널이 차례로 켜지고, LitGrok 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
2
2
 
3
3
  <h1 align="center">LitGrok</h1>
4
4
  <p align="center"><strong>Keep the work lit.</strong></p>
5
5
 
6
6
  LitGrok은 xAI Grok Build가 코드를 고치기 전에 계획부터 세우고, 만든 결과를 확인하고, 인수인계 문서를 남기게 합니다. 그러면 내일 세션이 오늘 멈춘 자리에서 이어집니다. npm 명령 한 줄이면 스킬과 프로젝트 규칙, 훅이 프로젝트에 들어갑니다.
7
7
 
8
- **[GitHub에서 README 전체 보기](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md)** · [안내서](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide_ko-KR.md) · [전체 스킬](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills_ko-KR.md) · [English](https://github.com/wjgoarxiv/litgrok#readme) · [설치](#30초-설치) · [상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/reference_ko-KR.md)
8
+ **[GitHub에서 README 전체 보기](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md)** · [안내서](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide_ko-KR.md) · [전체 스킬](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills_ko-KR.md) · [English](https://github.com/wjgoarxiv/litgrok#readme) · [설치](#30초-설치) · [상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/reference_ko-KR.md)
9
9
 
10
10
  <p align="center">
11
- <a href="#30초-설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/badge-version.svg" alt="1.0.30" /></a>
12
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
11
+ <a href="#30초-설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/badge-version.svg" alt="1.0.31" /></a>
12
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
13
13
  </p>
14
14
 
15
- <p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/reference_ko-KR.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/lucide-book-open.svg" width="16" alt="" /> 상세 안내</a> &nbsp; <a href="#30초-설치">설치</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/cover-motion.webp"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/lucide-play.svg" width="16" alt="" /> 커버 모션</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/assets/readme/lucide-shield-check.svg" width="16" alt="" /> MIT</a></p>
15
+ <p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/reference_ko-KR.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/lucide-book-open.svg" width="16" alt="" /> 상세 안내</a> &nbsp; <a href="#30초-설치">설치</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/cover-motion.webp"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/lucide-play.svg" width="16" alt="" /> 커버 모션</a> &nbsp; <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/assets/readme/lucide-shield-check.svg" width="16" alt="" /> MIT</a></p>
16
16
 
17
17
  Grok Build 세션이 길어지면 대화가 압축되고, 새 세션은 앞 세션이 알던 내용을 모른 채 시작하며, "다 됐다"는 말도 모델이 그렇게 말했다는 뜻일 때가 많습니다. LitGrok은 세션마다 따라갈 순서를 줍니다. 첫 수정 전에 계획을 글로 남기고, 그 계획에 비추어 확인하고, 다음 세션이 읽을 인수인계 문서를 남깁니다. 스킬 42개, 에이전트 11개, 프로젝트 규칙 하나, 훅 등록 열두 개가 들어 있고, 세션 실행과 모델 선택은 계속 Grok Build가 맡습니다. 며칠에 걸쳐 만드는 기능이나 다른 사람이 읽을 보고서처럼, 한 번 앉은 자리에서 끝나지 않는 일에 잘 맞습니다.
18
18
 
@@ -24,7 +24,7 @@ Node.js와 Grok Build가 있으면 됩니다. 써 보고 싶은 프로젝트에
24
24
  npm exec --yes --package @litfamily/litgrok@latest -- litgrok install
25
25
  ```
26
26
 
27
- 파일은 `<project>/.grok/`에 들어갑니다. `~/.grok/`에 설치하려면 `--user`를, 이 릴리스로 고정하려면 `--package @litfamily/litgrok@1.0.30`을 쓰세요. `--dry-run`을 붙이면 파일을 쓰기 전에 들어갈 경로를 모두 보여 줍니다.
27
+ 파일은 `<project>/.grok/`에 들어갑니다. `~/.grok/`에 설치하려면 `--user`를, 이 릴리스로 고정하려면 `--package @litfamily/litgrok@1.0.31`을 쓰세요. `--dry-run`을 붙이면 파일을 쓰기 전에 들어갈 경로를 모두 보여 줍니다.
28
28
 
29
29
  로컬 패키지로 써 보려면 실제 절대 경로를 변수에 넣고 두 줄을 따로 실행하세요.
30
30
 
@@ -73,7 +73,7 @@ npm exec --yes --package "$LITGROK_PACK" -- litgrok install
73
73
 
74
74
  ## 설치하면 달라지는 것
75
75
 
76
- 설치 프로그램이 스킬, 에이전트, 프로젝트 규칙, 훅을 `.grok/`에 복사합니다. [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/hooks/hooks.json)에 있는 훅 등록 열두 개는 신뢰한 Git 루트에서만 동작하고, `.grok/litgrok/session-ledger/`에 이벤트 순서를 남깁니다. 모든 작업은 Grok 세션 안에서 돌고 백그라운드 작업은 없습니다. 세션이 끝나면 다음 세션이 이어받을 때까지 그 자리에 멈춰 있습니다.
76
+ 설치 프로그램이 스킬, 에이전트, 프로젝트 규칙, 훅을 `.grok/`에 복사합니다. [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/hooks/hooks.json)에 있는 훅 등록 열두 개는 신뢰한 Git 루트에서만 동작하고, `.grok/litgrok/session-ledger/`에 이벤트 순서를 남깁니다. 모든 작업은 Grok 세션 안에서 돌고 백그라운드 작업은 없습니다. 세션이 끝나면 다음 세션이 이어받을 때까지 그 자리에 멈춰 있습니다.
77
77
 
78
78
  지금 어떤 LitGrok 스킬이 동작 중인지, 어떤 모델을 쓰는지, 컨텍스트를 얼마나 썼는지 보고 싶으면 `install --user --status-line`으로 상태 행을 켜세요. `~/.grok/config.toml`에 `[ui.status_line]`을 넣고, 파일이 이미 있으면 먼저 백업합니다. 프롬프트가 작업 흐름 스킬을 시작하면 지금 단계, 돌고 있는 도구, 마지막 테스트 결과도 함께 보입니다. 턴이 끝날 때 Grok의 터미널 알림을 받으려면 `--notify`를 붙이세요.
79
79
 
@@ -112,6 +112,6 @@ npm exec --yes --package @litfamily/litgrok@latest -- litgrok uninstall --user
112
112
 
113
113
  LitGrok 덕분에 같은 설명을 덜 반복하게 됐다면, [GitHub 스타](https://github.com/wjgoarxiv/litgrok) 하나가 다른 Grok Build 사용자에게 이 프로젝트를 알리는 데 도움이 됩니다.
114
114
 
115
- [상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/reference_ko-KR.md) · [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/CHANGELOG.md) · [개인정보](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/docs/privacy.md) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/LICENSE) · [상표 정책](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.30/TRADEMARK.md)
115
+ [상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/reference_ko-KR.md) · [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/CHANGELOG.md) · [개인정보](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/docs/privacy.md) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/LICENSE) · [상표 정책](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.31/TRADEMARK.md)
116
116
 
117
117
  커버의 모션은 LitFamily 모션 스킬로 만든 브랜드 연출입니다. 그려서 움직인 그림이라 실제 Grok 세션을 녹화한 장면은 들어 있지 않습니다.
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="local: 1.0.30"><title>local: 1.0.30</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="290" transform="scale(.1)">local</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.30</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="local: 1.0.31"><title>local: 1.0.31</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="290" transform="scale(.1)">local</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.31</text></g></svg>
package/docs/reference.md CHANGED
@@ -80,7 +80,7 @@ npm exec --yes --package @litfamily/litgrok@latest -- litgrok install
80
80
  Pin a version when you need a reproducible install:
81
81
 
82
82
  ```bash
83
- npm exec --yes --package @litfamily/litgrok@1.0.30 -- litgrok install
83
+ npm exec --yes --package @litfamily/litgrok@1.0.31 -- litgrok install
84
84
  ```
85
85
 
86
86
  Preview without writing files:
@@ -81,7 +81,7 @@ npm exec --yes --package @litfamily/litgrok@latest -- litgrok install
81
81
  재현 가능한 설치는 버전을 고정합니다.
82
82
 
83
83
  ```bash
84
- npm exec --yes --package @litfamily/litgrok@1.0.30 -- litgrok install
84
+ npm exec --yes --package @litfamily/litgrok@1.0.31 -- litgrok install
85
85
  ```
86
86
 
87
87
  파일을 쓰지 않고 미리보기:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/litgrok",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Installer for LitGrok: skills, a project rule and hooks that help xAI Grok Build sessions plan, build, check and hand off their work.",
5
5
  "keywords": [
6
6
  "grok",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litgrok",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Grok Build skills, project rules, and bounded lifecycle hooks for evidence-first lit work.",
5
5
  "author": {
6
6
  "name": "LitGrok contributors"