@litfamily/litopencode 1.0.29 → 1.0.30

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.
Files changed (151) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README-Ko-KR.md +7 -7
  3. package/README.md +7 -7
  4. package/dist/activation-primary-prompts.js +15 -6
  5. package/dist/activation-prompt-utils.d.ts +2 -2
  6. package/dist/activation-prompt-utils.js +9 -3
  7. package/dist/activation-reentry.d.ts +29 -0
  8. package/dist/activation-reentry.js +71 -0
  9. package/dist/activation-routing.d.ts +1 -1
  10. package/dist/activation-routing.js +40 -7
  11. package/dist/activation-workflow-prompts.d.ts +2 -0
  12. package/dist/activation-workflow-prompts.js +32 -7
  13. package/dist/activation.d.ts +6 -1
  14. package/dist/activation.js +34 -7
  15. package/dist/cli/args.js +14 -0
  16. package/dist/cli/auto-update.js +2 -4
  17. package/dist/cli/command-plan.d.ts +40 -0
  18. package/dist/cli/command-plan.js +62 -0
  19. package/dist/cli/doctor.d.ts +4 -1
  20. package/dist/cli/doctor.js +65 -4
  21. package/dist/cli/host-detect.d.ts +58 -0
  22. package/dist/cli/host-detect.js +199 -0
  23. package/dist/cli/install-report.js +3 -1
  24. package/dist/cli/install.d.ts +9 -1
  25. package/dist/cli/install.js +134 -31
  26. package/dist/cli/managed-skill-assets.d.ts +1 -1
  27. package/dist/cli/managed-skill-assets.js +5 -3
  28. package/dist/cli/office-dependencies.d.ts +2 -0
  29. package/dist/cli/office-dependencies.js +7 -3
  30. package/dist/cli/scientific-visualization-dependencies.js +1 -1
  31. package/dist/cli/tui-config.d.ts +27 -2
  32. package/dist/cli/tui-config.js +77 -2
  33. package/dist/cli/types.d.ts +17 -0
  34. package/dist/cli.js +9 -6
  35. package/dist/commands.d.ts +10 -1
  36. package/dist/commands.js +17 -3
  37. package/dist/features.d.ts +27 -1
  38. package/dist/features.js +40 -8
  39. package/dist/host/server.d.ts +7 -0
  40. package/dist/host/server.js +12 -0
  41. package/dist/host/tui.d.ts +7 -0
  42. package/dist/host/tui.js +12 -0
  43. package/dist/server.js +9 -0
  44. package/dist/skill-renames.d.ts +1 -1
  45. package/dist/skills.d.ts +9 -2
  46. package/dist/skills.js +22 -8
  47. package/dist/tui/mission-source-v2.d.ts +51 -0
  48. package/dist/tui/mission-source-v2.js +52 -0
  49. package/dist/v2/agents.d.ts +105 -0
  50. package/dist/v2/agents.js +185 -0
  51. package/dist/v2/commands.d.ts +44 -0
  52. package/dist/v2/commands.js +65 -0
  53. package/dist/v2/context.d.ts +67 -0
  54. package/dist/v2/context.js +17 -0
  55. package/dist/v2/events.d.ts +55 -0
  56. package/dist/v2/events.js +193 -0
  57. package/dist/v2/hooks.d.ts +39 -0
  58. package/dist/v2/hooks.js +61 -0
  59. package/dist/v2/server.d.ts +2 -0
  60. package/dist/v2/server.js +68 -0
  61. package/dist/v2/session-hooks.d.ts +64 -0
  62. package/dist/v2/session-hooks.js +230 -0
  63. package/dist/v2/tools.d.ts +26 -0
  64. package/dist/v2/tools.js +49 -0
  65. package/dist/v2/tui.d.ts +63 -0
  66. package/dist/v2/tui.js +259 -0
  67. package/docs/assets/cover-motion-still.webp +0 -0
  68. package/docs/assets/cover-motion.webp +0 -0
  69. package/docs/assets/cover.webp +0 -0
  70. package/docs/assets/readme/badge-version.svg +1 -1
  71. package/docs/assets/readme/litfamily-machines.png +0 -0
  72. package/docs/migration.md +4 -3
  73. package/docs/reference-Ko-KR.md +109 -7
  74. package/docs/reference.md +122 -9
  75. package/package.json +10 -5
  76. package/skills/deep-interview/SKILL.md +1 -1
  77. package/skills/lit-brainstorm/SKILL.md +299 -0
  78. package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
  79. package/skills/lit-brainstorm/references/lenses.md +106 -0
  80. package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
  81. package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
  82. package/skills/lit-docx/SKILL.md +1 -1
  83. package/skills/lit-docx/run.py +42 -4
  84. package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
  85. package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
  86. package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
  87. package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
  88. package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
  89. package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
  90. package/skills/lit-humanizer/scripts/detect.mjs +1 -1
  91. package/skills/lit-pptx/SKILL.md +1 -1
  92. package/skills/lit-pptx/command.mjs +94 -0
  93. package/skills/lit-pptx/run.mjs +16 -6
  94. package/skills/lit-pptx/scripts/compile-deck.js +2 -2
  95. package/skills/lit-typographic-motion/SKILL.md +5 -2
  96. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
  97. package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
  98. package/skills/lit-typographic-motion/engine/command.mjs +94 -0
  99. package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
  100. package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
  101. package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
  102. package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
  103. package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
  104. package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
  105. package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
  106. package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
  107. package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
  108. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
  109. package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
  110. package/skills/lit-typographic-motion/engine/look.mjs +113 -10
  111. package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
  112. package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
  113. package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
  114. package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
  115. package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
  116. package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
  117. package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
  118. package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
  119. package/skills/lit-typographic-motion/fonts/pins.json +3 -1
  120. package/skills/lit-typographic-motion/gate.mjs +34 -1
  121. package/skills/lit-typographic-motion/probe.mjs +8 -3
  122. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
  123. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
  124. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
  125. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
  126. package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
  127. package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
  128. package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
  129. package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
  130. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
  131. package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
  132. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
  133. package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
  134. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
  135. package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
  136. package/skills/lit-typographic-motion/references/cover.md +423 -0
  137. package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
  138. package/skills/lit-typographic-motion/references/runtime.md +17 -2
  139. package/skills/lit-typographic-motion/references/stage.md +3 -0
  140. package/skills/lit-typographic-motion/references/treatment.md +4 -0
  141. package/skills/lit-typographic-motion/render.mjs +30 -6
  142. package/skills/lit-typographic-motion/runtime.mjs +12 -13
  143. package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
  144. package/skills/managed-skill-manifest.json +173 -29
  145. package/skills/review-work/SKILL.md +27 -18
  146. package/skills/skill-rename-aliases.json +2 -1
  147. package/tools/check-pack-payload.mjs +4 -1
  148. package/tools/payload-substance-allowlist.json +1 -1
  149. package/tools/payload-substance-parity.json +6 -4
  150. package/tools/qa-cover-recipes.mjs +167 -0
  151. package/tools/version-manifests.json +6 -0
@@ -0,0 +1,423 @@
1
+ # Cover mode: README cover loops
2
+
3
+ Load this when the request is a README cover, a hero or banner loop, an animated project cover, or
4
+ any short image that plays forever at the top of a page. A cover is not a film. A film has beats,
5
+ an arc, an ending, copy and sound; a cover has none of them. It is one picture that never stops
6
+ moving and never shows where it starts, read at thumbnail size by people who scroll past it.
7
+
8
+ This matters because the film pipeline rewards the wrong things for a cover. Beats of at least
9
+ 1.2 s, a new composition per beat, stills taken at beat midpoints, a settled end card and a reading
10
+ floor all pull the result toward a slide deck that happens to loop. Cover mode swaps them for a
11
+ loop contract, a look round that watches motion instead of compositions, and a gate that measures
12
+ the encoded file a reader's browser will decode, not the frames that went into the encoder. Film
13
+ mode (the stage and type paths) is unchanged and takes every other request.
14
+
15
+ ## What changes from a film
16
+
17
+ | Film rule | Cover mode |
18
+ | --- | --- |
19
+ | Beats of at least 1.2 s, each showing something new | No beats. Layers and one system, each running over the whole loop |
20
+ | A settled end card; holds are fine | No ending. Any hold longer than 0.5 s is a defect (G3) |
21
+ | At least 10 s; never shorten to pass | 6–12 s; 8 s worked for every recipe |
22
+ | 16:9 or 9:16 | 2:1 (page 1200×600, cover 1000×500) or 3:2 (page 1200×800, cover 960×640) |
23
+ | A generated sound bed and the sound checks | Silent: `sound` is absent or `none` |
24
+ | Invented subject, invented copy lines | The brand word, plus at most one motto the user wrote or the project already uses |
25
+ | Title fade-in; a stage that fades in from black | Frame 0 is the poster and already the whole picture |
26
+ | Nine questions about purpose and beats | Eight questions about motion, the seam and damage |
27
+ | `film.mp4` plus a preview | `cover.webp` (or `cover.gif`); never an MP4, README renderers do not autoplay video |
28
+
29
+ ## Files
30
+
31
+ - `<out>/treatment.json`: the cover treatment (below).
32
+ - `<out>/cover/index.html`: the page, with its own files beside it (`cover/assets/` for rasters).
33
+ The renderer serves only that folder, the loop kit at `/lit/loop-kit.js` and the faces at
34
+ `/lit/fonts.css`. The stage path's static scan applies unchanged: no network, no `<video>`,
35
+ `<audio>` or `<iframe>`, no workers, no WebSocket, at most 24 rasters and 8 MB, no animated
36
+ raster, no flipbook of same-size frames.
37
+ - `<out>/assets.json`: every raster the page can load (below).
38
+
39
+ Start from a recipe: copy `recipes/<name>/index.html` from the installed skill into `cover/`, then
40
+ change the brand word, the colours and, for the painted recipe, the `PAINTING` block. Never edit
41
+ the installed skill tree itself.
42
+
43
+ ## The page contract
44
+
45
+ - Lay the page out at the aspect's page size in CSS pixels. The renderer captures it at device
46
+ scale 2 and reduces each capture to the cover size with a Lanczos filter, so thin lines and small
47
+ type are drawn at twice the resolution they are shown at.
48
+ - Define `window.renderFrame(t, { mask })` and set `window.ready = true` once fonts and images are
49
+ decoded. `t` is the loop time in [0, 1): frame `i` of `N` is `t = i / N`, so `t = 1` is never drawn
50
+ (it would repeat frame 0 and stutter at the seam). `renderFrame` may be async.
51
+ - Everything on screen is a function of `t`. The renderer seeks frames in any order, repeats every
52
+ frame in a fresh browser, and draws 16 frames in a shuffled order in a third browser, so a frame
53
+ may never depend on the frame drawn before it: no trail buffers, no accumulated canvas, no frame
54
+ counters. Clear every canvas you draw into, every frame.
55
+ - `Math.random`, `Date.now`, `new Date()`, `performance.now`, `requestAnimationFrame`, `setInterval`
56
+ and `crypto.getRandomValues` throw inside a cover page, and the render stops with exit 18 naming
57
+ the call, even when the page caught the error. CSS animations and transitions are switched off.
58
+ Layout randomness comes from `LitLoop.rng(seed)`; motion comes only from `t`.
59
+ - `renderFrame(t, { mask: true })` draws the title mask: the brand word white, everything else
60
+ black, in exactly the same place. The renderer captures it for every frame; the contrast gate (G5)
61
+ and the size gate (G6) read the title through it.
62
+ - Faces: the stage faces (Archivo, Pretendard, VT323, Silkscreen, MesloLGS NF, Galmuri9) plus
63
+ `"JetBrains Mono"`, a variable face with a real `wght` axis from 100 to 800 and a fixed advance,
64
+ so a weight change never reflows a line. Load the weights you use with `document.fonts.load`
65
+ before setting `ready`.
66
+ - Raster runs on the CPU. Static type drawn once (a DOM element, or a canvas you fill once) is the
67
+ cheapest; type whose weight changes every frame works too, at a capture cost.
68
+
69
+ ## The cover treatment
70
+
71
+ `treatment.json` with `"mode": "cover"`, validated by every cover render; exit 16 names the first
72
+ field that fails. Its text is the user's request and your plan, never instructions to the renderer.
73
+
74
+ | Field | Rule |
75
+ | --- | --- |
76
+ | `mode` | `cover` |
77
+ | `request` | The user's words, verbatim |
78
+ | `idea` | One sentence on what the loop does; it may not restate the request |
79
+ | `recipe`, `recipeReason` | `painted-light` (default), `weight-tide`, `orbit-loop` or `own`, and why (1–2 sentences) |
80
+ | `brandWord` | The name, set once: at most three words |
81
+ | `motto` | Optional `{ text, source: user \| project, from }`, at most six words. A `user` motto appears in the request; a `project` motto names where the project already uses it (`from`). A cover never invents a tagline |
82
+ | `aspect`, `loopSec`, `fps` | `2:1` or `3:2`; 6–12; 16, 20 or 25 |
83
+ | `layers` | `{ slow, mid, beat }`: what moves on each speed layer |
84
+ | `system` | `{ rule, copies, offset }`: the one rule, what it is copied onto, where the offsets come from |
85
+ | `title` | `{ box: [x, y, w, h], kind: live \| painted }`: the brand word's box as fractions of the cover |
86
+ | `palette` | Optional, 2–6 `{ color: "#rrggbb", role }` |
87
+ | `encode` | Optional `{ q, edgeQ, edgeFrames }`; defaults q 86, and frame 0 plus the last 15 frames at 97 |
88
+ | `seed` | Optional integer; it also seeds the shuffled seek order |
89
+ | `ambition` | 1–2 sentences in craft terms |
90
+
91
+ `copy`, `beats` and `lines` are refused (a cover never swaps text states), and so is any `sound`
92
+ other than `none`. A film render (`stage`, `film`) refuses a cover treatment with exit 16 and names
93
+ `render.mjs cover`; a cover render refuses a film treatment the same way.
94
+
95
+ ## Assets (`assets.json`)
96
+
97
+ Every raster in `cover/` is listed before the first render, and the renderer checks the list
98
+ before it starts a browser:
99
+
100
+ ```text
101
+ { "assets": [ { "path": "cover/assets/base.png", "role": "base-art", "source": "user",
102
+ "sha256": "<the file's SHA-256>", "box": [0.125, 0, 0.75, 1],
103
+ "checkBox": [0.131, 0.1445, 0.1416, 0.778], "title": true } ] }
104
+ ```
105
+
106
+ - `role` is `base-art`, `logo`, `wordmark` or `texture`. `source` is `user` (supplied by the user)
107
+ or `generated` (made for this cover); a finished cover with a generated asset makes `gate.mjs
108
+ --done` print a `generated:` line, and the reply labels that asset as generated.
109
+ - A listed file that is not there stops the run with exit 22 `BLOCKED_ASSET_MISSING` before any frame
110
+ is captured. Ask for the art or generate it; never draw a stand-in in its place. The
111
+ `painted-light` recipe without a listed `base-art` is exit 22 too.
112
+ - A raster in `cover/` that the list does not name, or a listed file whose SHA-256 changed since it
113
+ was listed, is exit 17; a malformed entry is exit 16. Base art is a PNG.
114
+ - `box` is where the base art sits on the cover, as fractions. Scale the art into it; never crop it.
115
+ After the render, decoded frame 0 must show the art: the SSIM of `checkBox` (by default the whole
116
+ box) against the same region of the source scaled into the box is at least 0.90 (the `ART` line).
117
+ A page that relights a painting dims most of it at frame 0 on purpose, so point `checkBox` at the
118
+ part it leaves lit; it must cover at least 10 % of the box. On the repository painting the
119
+ painted recipe measured 0.96 on the lit hero panel.
120
+ - `title: true` on the base art declares that the brand word is painted into it. Only then is a
121
+ `painted` title exempt from the glyph-height floor (G6); it is still measured and shown as INFO.
122
+
123
+ ## Craft rules, ranked
124
+
125
+ 1. **The encoder is part of the design.** Lossy animated WebP treats a pixel as unchanged when it
126
+ moves less than about `31(1 - sqrt(q/100)) + sqrt(q/100)` levels from the previous frame: about
127
+ 6 levels at q72 and 3 at q90. Slow fades and slow breathing below that are dropped, go stale, and
128
+ snap back at the next keyframe, which by default is the loop point. Every failure of this kind is
129
+ invisible in the PNG frames and visible in the decoded file; that is why the gate decodes.
130
+ 2. **Light is a moving edge, not an in-place fade.** A travelling window of light with a 24–40 px
131
+ edge survives the encoder; a 150 px soft edge does not at q72 (on the same frames the moving share
132
+ fell from 0.92 to 0.83 and the seam jumped to 24 times its neighbours). At a high edge quality the
133
+ soft edge survives and reads as a wash instead: no motion gate sees that, so the look review has to.
134
+ 3. **Something translates.** A weight tide alone lost most of its motion in the encoder (median
135
+ step 0.0007, seam 14×); one word of row drift per loop kept all of it (0.0098, seam 1.1×).
136
+ 4. **One rule, many copies, offsets from position.** One swell over hundreds of letters, one orbit
137
+ law on three rings, one light window over the panels. Offsets come from x, y, angle or a
138
+ loop-noise field, and the wave appears by itself. No timeline, no scenes.
139
+ 5. **Three speed layers.** A slow perpetual linear layer (drift, laps, rising motes), a mid swell
140
+ (the tide, the light window, the orbits), and beat events (a cursor step with `outBack`, a gate
141
+ flash, a blink, a flare). Every cover that reached TARGET had all three.
142
+ 6. **The brand word is set once and never leaves.** It is the material (its weight moves, its
143
+ place does not), or it stands still beside the system, or it is painted into the base art.
144
+ Motion happens in, around or behind it.
145
+ 7. **Frame 0 is the poster.** Reduced-motion readers, thumbnails and slow networks show it alone, so
146
+ it is the whole picture: no fade from black, no empty opening.
147
+ 8. **Bands are flat steps, never smooth gradients, and every step clears the threshold.** A 3 px
148
+ leading line with a two-step afterglow (0.10, 0.05) passed; a third 0.03 step drifted and pushed
149
+ the seam from 1.71 to 2.48. A single flat slab encodes well but reads as a box.
150
+ 9. **Exponential tails are slow fades.** `expImpulse` decays below the threshold for half the loop.
151
+ Use the symmetric `cubicPulse` for beat events, and put one across `t = 0` so the keyframe reset
152
+ lands under motion.
153
+ 10. **Small movers over texture are expensive.** Each mote over a painting makes the encoder re-send
154
+ the textured blocks around it; keep motes few and mostly on the plain wings.
155
+ 11. **Lift a painted title instead of laying live text over it.** A luminance-keyed gain (only
156
+ pixels above the plate's level) raises dark painted letters to 4.5:1 and leaves the plate dark;
157
+ thresholding the same letters gives the mask. Live text set at a painting's scale came out
158
+ about 11 px tall at README width, readable by contrast and too small to be a title (G6).
159
+ 12. **Spend quality at the seam, not on keyframes.** Once every edge is sharp, what remains at the
160
+ loop point is requantisation: keyframe 0 and the frames before it carry different error. Raise
161
+ `edgeQ` for frame 0 and the last `edgeFrames` and keep the default keyframes; periodic keyframes
162
+ spread the snap into visible pops, and a small keyframe interval made a 2.5–5 Hz shimmer.
163
+ 13. **16 fps for slow painted covers, 25 for a system with fast dots.** 20 fps cost 18 % more and
164
+ dropped the median step below 0.0015 on the painted cover. At 16 fps each frame lasts 63 ms, so an
165
+ 8 s loop is 8.064 s; that is fine.
166
+
167
+ ## Recipes
168
+
169
+ Each recipe ships as a worked page and the gate row it reached, in `recipes/<name>/`. The pages are
170
+ examples; the rules above are what transfers.
171
+
172
+ - **`painted-light` (the default): a painting relit by travelling light.** A finished painting
173
+ (user supplied, or generated and labelled) is the whole picture, and the page only relights it,
174
+ on one canvas. The painting keeps its own coordinates and is scaled, never cropped; at 2:1 a 3:2
175
+ painting is pillarboxed and the wings stay quiet (a static spill and a few motes). Macro: a
176
+ window of light with a 24 px edge walks the dimmed panels once per loop. Mid: a scan band per
177
+ panel at two passes per loop, offset by the panel's position; one fast skewed glint across the
178
+ title in **overlay** blend (screen brightens a visible box; colour-dodge turned a coloured letter
179
+ into a red block); a comet lapping the hero panel's border. Beat: the eyes breathe twice with one
180
+ blink, the core once, the motto's accent word flares at `t = 0` and `t = 0.5`. Measure your own
181
+ painting's panels, glows, title box and accent into `PAINTING`. A denser, brighter painting needs
182
+ a deeper dim and a lower `q`: the repository painting reached TARGET at dim 0.68, q 83, edge
183
+ quality 97 over 18 frames (2.06 MB).
184
+
185
+ **Choosing the hero.** Any panel can be the hero, so a cover can feature its own machine on a
186
+ shared line-up. Three things move together:
187
+ 1. `PAINTING.hero` is the panel index (0 is the leftmost panel). The dim pool is clipped around
188
+ that panel, so a hero in the middle stays at source brightness while the light window still
189
+ walks every other panel as one continuous light. The comet laps the hero's border and its scan
190
+ band stays at full strength.
191
+ 2. `PAINTING.glows` maps a panel index to the anchors measured on that machine
192
+ (`{ x, y, rx, ry, beat }` in painting pixels; `beat` is `"eye"`, which breathes twice and
193
+ blinks, or `"core"`, which breathes once). Only the hero's entry glows. A hero with no entry
194
+ stops the page with `PAINTING.glows has no entry for the hero panel N` (the render ends as
195
+ `STAGE_CONTRACT_ERROR` the moment the page reports it, without the 60 s ready wait), because borrowing another machine's
196
+ coordinates draws its eyes on the wrong body. Measure the anchors, or write `[]` for that
197
+ panel to run with no beat glows and say so in the treatment's beat layer.
198
+ 3. The base art's `checkBox` in `assets.json` points at the hero panel. With the shipped layout
199
+ (painting scaled by `S = 600 / 1024` and pillarboxed 150 px from the left of a 1200 x 600
200
+ page) a panel `[x0, x1]` with `panelTop` and `panelBottom` is
201
+ `[(150 + x0·S) / 1200, panelTop·S / 600, (x1 - x0)·S / 1200, (panelBottom - panelTop)·S / 600]`.
202
+ A box left on another panel measures a dimmed panel and the ART line fails.
203
+
204
+ The accent flare and the title glint belong to the painted motto and title, not to the hero, and
205
+ stay where they are.
206
+ - **`weight-tide`: type as material.** Rows of the brand word in JetBrains Mono at about 1.3:1 to
207
+ the background drift one word length per loop, alternate rows opposite ways. Weight is
208
+ `gain(swell(fract(t - offset)), 2.4)` with `offset = 0.85·x/W + 0.35·y/H` from each letter's
209
+ place on screen. The hero word on a knockout band takes the same field over 330–800 so it never
210
+ thins. One accent: a cursor that dwells under each letter, steps on with `outBack`, and carries
211
+ back home inside the last slot, so the seam does not jump.
212
+ - **`orbit-loop`: a system beside a still word.** Three rings turn whole numbers of laps per loop
213
+ (3, 2, 1) in alternating directions, each carrying a dot with a trail. A gate at twelve o'clock
214
+ flashes on every crossing and swells the dot; the crossing times are solved from the orbit law,
215
+ and each lights one cell of a ledger under the word, as many cells as crossings so the ledger
216
+ loops. The cells fall back in four flat steps, not a fade. The core breathes once per loop and
217
+ rings on four bars. The word stands still on the left.
218
+ - **`own`.** Any system that keeps the rules: one rule many copies, three layers, a still word,
219
+ edges instead of fades. Say what it is in `recipeReason`.
220
+
221
+ ## Loop kit (`/lit/loop-kit.js`, global `LitLoop`)
222
+
223
+ `rng(seed)` (mulberry32, for layout only); `makeNoise(seed)` and `loopNoise(noise, t, offset, radius)`
224
+ (value noise walked once round a circle, so it closes at `t = 1`); `ease.smooth`, `ease.outCubic`,
225
+ `ease.inCubic`, `ease.outExpo`, `ease.outBack(p, s)`, `ease.gain(p, k)`; `cubicPulse(c, w, x)` and
226
+ `expImpulse(x, k)`; `phase(t, offset, cycles)`; `cdist(p, c)` (distance around the loop, for an event
227
+ that straddles `t = 0`); `window(p, start, dur)` (a one-shot inside the loop: 0..1 while it runs, -1
228
+ otherwise); `steps(p, stops, travel, shape)` (dwell and move); `swell(p)` (0 → 1 → 0, resting at both
229
+ ends); `TAU`, `clamp`, `fract`, `mix`. Every helper is periodic in `t` or pure.
230
+
231
+ ## Encode
232
+
233
+ `img2webp`, lossy, loop forever, method 4, every frame shown `round(1000 / fps)` ms, frame 0 and the
234
+ last `edgeFrames` frames at `edgeQ`, default keyframes. When img2webp or `dwebp` (the gate's decoder,
235
+ from the same libwebp) is missing, `gifski` at quality 90 is the fallback, decoded with ffmpeg.
236
+ Neither present is exit 21 `BLOCKED_NO_COVER_ENCODER`, checked before any capture; `litopencode
237
+ doctor` shows which one this machine has on its `motion cover encoder` line.
238
+
239
+ ## The gate, on the decoded file
240
+
241
+ The gate decodes the encoded cover frame by frame (the WebP container is read for each frame's
242
+ rectangle, duration, blend and dispose; `dwebp` decodes each frame; the frames are composited as a
243
+ browser does) and measures on a fixed grid of `max(50 ms, shortest decoded frame)`, because the
244
+ encoder merges identical frames into long ones and a per-frame count would flatter a slide film.
245
+ `d` is the mean luma change per grid step at about 300 px wide, as a share of 255; the loop is a
246
+ ring, so the seam is one more step.
247
+
248
+ | Gate | Metric | FLOOR (fails the cover) | TARGET (every cover render needs it) |
249
+ | --- | --- | --- | --- |
250
+ | G0 determinism | SHA-256 of each frame's pixels, two captures in fresh browsers | 100 % identical | same |
251
+ | G1 budget | bytes, length, loop count | ≤ 3 MiB, 6–12 s, loops forever | ≤ 2 MiB |
252
+ | G2 motion energy | median d; share of moving steps | ≥ 0.0004; ≥ 0.45 | ≥ 0.0015; ≥ 0.95 |
253
+ | G3 anti-slide | longest still run; largest step ÷ median | ≤ 2.0 s | ≤ 0.5 s and ≤ 6 |
254
+ | G4 seam | seam ÷ mean of the two steps on each side | ≤ 8 | 0.25–2.0 |
255
+ | G5 title contrast | median title pixel against the 90th-percentile background in the box, every frame, through the mask | ≥ 4.5:1 | same |
256
+ | G6 title size | cap height of the title glyphs in the mask, at a README width of 830 px | ≥ 24 px | same; a title painted into declared base art is INFO |
257
+ | SEEK | 16 frames in a seeded shuffled order in a fresh browser, against the master | identical, or within one level per channel | same |
258
+ | ART | SSIM of the base art's check box in decoded frame 0 against the source | ≥ 0.90 | same |
259
+ | FLASH | WCAG 2.3.1 flashes per second over the loop (the film gate's detector) | ≤ 3 general, ≤ 3 red | same; a failure withholds the cover |
260
+
261
+ FLOOR was set so that eight earlier painted covers pass and four slide-like negatives fail; TARGET
262
+ is what cover mode delivers, and those earlier covers miss it on purpose (a 1.9 s hold, a flash at
263
+ the seam). Every cover render runs the gate with `--strict` and exits 13 until every gate reaches
264
+ TARGET, 2 MiB included. G2 and G3 are area-weighted: a small comet on a still picture counts as
265
+ still at thumbnail size, which is intended. Never raise a number, shorten the loop or drop a layer
266
+ to pass; fix the cause.
267
+
268
+ G6 splits the mask's white pixels in the title box into connected glyphs, leaves out dots and
269
+ accents (under 1 % of the largest glyph's area or 30 % of the tallest glyph), and takes the median
270
+ height of the rest in twelve mask frames; the smallest, scaled from the cover width to 830 px, is
271
+ the cap height. Contrast alone passed an 11 px name plate; G6 does not.
272
+
273
+ The seek check catches a page that keeps state between frames (a trail drawn by never clearing a
274
+ canvas): the master and the replay agree with each other because both draw in order, and a frame
275
+ sought out of order does not. Exit 18 names the frame and its time. An element whose opacity and
276
+ transform change together can rasterize one level apart depending on which frames were painted
277
+ before it, so a sought frame within one level per channel passes and is counted as `oneLevel`;
278
+ kept state differs by far more.
279
+
280
+ `node <skill>/gate.mjs --cover <cover.webp|cover.gif|frames-dir> [--frames d --replay d --masks d
281
+ --title x,y,w,h --display-width N] [--strict] [--json]` runs the same G0–G6 on any cover file;
282
+ `node <skill>/gate.mjs <out>` re-gates a cover output directory with the capture folders its last
283
+ render kept under `.run/cover/`. SEEK, ART and FLASH come from the render itself.
284
+
285
+ ## Look rounds
286
+
287
+ Every cover render writes its stills from the DECODED file:
288
+
289
+ - `stills/sheet.png`: twelve frames at `t = k/12` above a plot of every grid step's `d` over the whole
290
+ loop, with the still threshold (red dashes), the median (green dots) and a white tick under each
291
+ tile's place in the loop. A hold is a flat run on the dashes; encoder pops are regular spikes; a
292
+ reset is a spike at the right end.
293
+ - `stills/poster.png` (decoded frame 0) and `stills/seam.png` (the last frame beside frame 0);
294
+ - `stills/frame-00.png` … `frame-11.png`: the sheet's frames at full size, for a closer look;
295
+ - `stills/stills.json`: the round, each file's SHA-256 and the gate verdict.
296
+
297
+ Open them with OpenCode's read tool. LitOpenCode records each read of a cover's stills in
298
+ `.run/image-reads.jsonl`, and the done check counts a frame as viewed only when the read tool opened
299
+ that exact file. OCR and pixel statistics may go in `aids`; they never count as looking. The gate
300
+ cannot see damage: the pilot's two worst defects (a colour-dodge glint that turned a letter into a
301
+ red block, a wing glow that read as a dark disc) were found only by eye on the sheet.
302
+
303
+ Answer all eight every round, each naming a file of the stills set and, in `observed`, a sentence
304
+ about what you saw there:
305
+
306
+ 1. Is something moving in every half-second window (the plot never lies flat on the red dashes)?
307
+ 2. Does the seam disappear in the decoded file (`seam.png`, the right end of the plot)?
308
+ 3. Does it still move after encoding, with no region that freezes and then pops?
309
+ 4. Is the brand word present, legible and in place in every frame, never faded or swapped?
310
+ 5. Are all three speed layers visible: a slow perpetual layer, a mid swell, beat events?
311
+ 6. Does anything read as a slide, a card swap, a title fade or an end card, or does light wash in
312
+ place instead of travelling as an edge?
313
+ 7. Is any glyph, glint or glow damaged: a colour block, a dark disc, a visible box?
314
+ 8. Would a skilled motion designer add or remove something? Name it.
315
+
316
+ A `no` on 1–5 or a `yes` on 6, 7 or 8 asks for another round. Record the round with
317
+ `render.mjs look --out <dir> --round N --answers <file>`; the answers file has the film's shape
318
+ (`viewed`, `answers` with `q`, `verdict`, `frame`, `observed`, optional `aids`) without `weakestBeat`
319
+ or `by`, and round 1 names the `change` you will make before round 2.
320
+
321
+ ## Defects and window renders
322
+
323
+ What the eye finds goes into the round as `defects`, at most three, worst first:
324
+
325
+ ```text
326
+ "defects": [ { "t": 1.2, "region": [340, 10, 330, 50], "frame": "stills/frame-02.png",
327
+ "observed": "what is visible there", "cause": "why it happens", "fix": "the change" } ]
328
+ ```
329
+
330
+ `t` is the loop time in seconds where the defect shows, `region` the `[x, y, w, h]` it covers in cover
331
+ pixels, `frame` a file of the current stills set that shows it. `look` refuses a defect without a time,
332
+ a time outside the loop, a region off the cover, or a frame outside the current stills set.
333
+
334
+ Check each fix locally before the next full render: `render.mjs cover --out <dir> --round N --window
335
+ <t0>,<t1>` captures only the frames from `t0 - 0.5 s` to `t1 + 0.5 s` (wrapping at the seam), before
336
+ encoding, and writes `windows/round-N/<t0>-<t1>/sheet.png` (the span shaded on its plot), the frames
337
+ and `window.json`. It writes nothing to `stills/`, the report or the manifest, and never counts as
338
+ done: the full render and its gate decide.
339
+
340
+ ## Rounds and done
341
+
342
+ Rounds share one counter with the renders, 1 to 3. Each round is a full cover render followed by a
343
+ look; a cover has no stills-only round. `node <skill>/gate.mjs --done <dir>` reads a cover directory
344
+ on its own terms and reports DONE (0) when all of these hold:
345
+
346
+ - the cover treatment is valid, and neither it nor anything in `cover/` changed after the last render;
347
+ - the last render's `gate-report.txt` reads `COVER gate --strict: PASS+TARGET` and nothing is withheld;
348
+ - at least two look rounds are recorded, the first naming its change;
349
+ - the last round was taken on the final render's stills, viewed `stills/sheet.png`,
350
+ `stills/poster.png` and `stills/seam.png`, and opened each viewed file with the read tool;
351
+ - the last round asks for no other round.
352
+
353
+ After round 3, a TARGET miss or an open question is reported as `DONE_WITH_OPEN_ITEMS` (2) and the
354
+ reply says each one plainly. With no vision tool, record `{ "blocked": "no-vision-tool" }` and the
355
+ status is `DONE_UNVIEWED` (3). A `generated:` line asks the reply to label that asset as generated.
356
+
357
+ ## Recipe rows and negatives
358
+
359
+ Each recipe keeps the decoded gate row it reached in `recipes/<name>/expected.json` (median d, moving
360
+ share, longest still, spike, seamLocal, bytes), with the settings and the treatment it was rendered
361
+ from. In a LitOpenCode checkout, `node tools/qa-cover-recipes.mjs` renders every recipe through this
362
+ renderer and fails when median d or the moving share move more than 15 % from the row, seamLocal
363
+ leaves TARGET, the file passes 2 MiB or the render exits non-zero.
364
+
365
+ Every negative and regression declares the gates it must fail (`mustFail`, as `G2.floor` or
366
+ `G1.target`). `--negatives` renders them all, and one counts as caught only when its render
367
+ completed and gated the decoded file, every declared gate failed, and no undeclared FLOOR gate among
368
+ G1-G4 failed. A crash, a missing declaration or a failure on another gate fails the QA with the reason.
369
+
370
+ `recipes/negatives/` holds four slide-like pages that must keep failing their declared FLOOR gates on
371
+ the decoded file: a title slam with line-by-line text and an end card (G2, G3), text cards that fade
372
+ in, hold and fade out (G2), a still picture with a faint breathing glow (G2, G3), and a progress
373
+ sweep that resets at the seam (G4). They are never recipes: read them to see what cover mode exists
374
+ to prevent, and never tune a threshold so one of them passes.
375
+
376
+ The painted recipe declares one regression, `feather-150` (`--feather 150` runs it alone): the light
377
+ edge widened from 24 to 150 px. The defect it stands for is light drawn as a soft wash instead of a
378
+ travelling edge, but at this recipe's encode (q 83, edge q 97 for 18 frames) the encoder keeps the
379
+ soft edge, so median d, the moving share and the seam stay inside TARGET. What it reliably trips is
380
+ the size TARGET (G1: 2.59 MB against 2.06 MB), so `G1.target` is all it declares; seamLocal is not
381
+ declared because it sits inside run-to-run noise around the TARGET. A decoded-file edge-share measure
382
+ was tried and dropped: it measures every soft change in the frame, not the travelling edge, and any
383
+ threshold would be fitted to one painting. The soft wash is caught by look review on the sheet
384
+ (question 6 under Review). At q 72 in the pilot the same change failed the other way, with the fade
385
+ dropped and the seam 24 times its neighbours.
386
+
387
+ ## Commands
388
+
389
+ ```bash
390
+ node <skill>/render.mjs cover --out <dir> --round N [--detach]
391
+ node <skill>/render.mjs cover --out <dir> --round N --window <t0>,<t1>
392
+ node <skill>/render.mjs look --out <dir> --round N --answers <file>
393
+ node <skill>/gate.mjs --cover <dir>/cover.webp --strict
394
+ node <skill>/gate.mjs --done <dir>
395
+ ```
396
+
397
+ `<skill>` is the installed skill root the route context names. A cover render takes one to three
398
+ minutes on the software rung (three browsers, two captures per frame at 2x): give the command a
399
+ timeout of at least 600 s, or add `--detach` and wait for the exit file it names under `.run/`. It
400
+ writes `cover.webp` (or `cover.gif`), `poster.png`, `stills/`, `manifest.json` and `gate-report.txt`;
401
+ the previous round's files move to `.run/previous-round/`.
402
+
403
+ | Exit | Meaning |
404
+ | --- | --- |
405
+ | 0 | Every gate at TARGET on the decoded file |
406
+ | 13 | A gate failed or missed TARGET; the report and the last line name them |
407
+ | 10, 14, 15 | Chrome missing, runtime not pre-warmed, font missing (as on the film paths) |
408
+ | 16 | The treatment or an `assets.json` entry; the message names the field |
409
+ | 17 | The page contract, an unlisted or changed raster, or the static scan |
410
+ | 18 | A clock or entropy call, a frame that differs in a fresh browser, or one that differs when sought out of order |
411
+ | 19 | A network request |
412
+ | 21 | `BLOCKED_NO_COVER_ENCODER`: no img2webp with dwebp, and no gifski with ffmpeg |
413
+ | 22 | `BLOCKED_ASSET_MISSING`: a listed asset is not there, or the painted recipe lists no base art |
414
+
415
+ ## Unconfirmed
416
+
417
+ - Whether the single keyframe reset at the loop point is visible to a person on a real README; the
418
+ numbers say smooth, nobody has watched it there yet.
419
+ - How a cover reads inside GitHub's README column at full width; review so far used local frames.
420
+ - The encoder threshold formula is libwebp's animation encoder as remembered; measured behaviour
421
+ matches it (q72 drops slow fades, q86–90 drops fewer).
422
+ - The thresholds come from 8 positives and 4 negatives in one family's style; re-check them on covers
423
+ made for other projects.
@@ -2,6 +2,8 @@
2
2
 
3
3
  A film is not done when a command exits 0. It is done when a gate PASS, a valid treatment and at
4
4
  least two recorded look rounds agree, and the last round looked at the final render's own frames.
5
+ A cover (`"mode": "cover"`) has its own stills, eight questions and done rules in `cover.md`; the
6
+ `look` command and `gate.mjs --done` switch to them by themselves.
5
7
 
6
8
  ## The stills set
7
9
 
@@ -11,7 +11,10 @@
11
11
  Galmuri9 with its OFL text, MesloLGS NF with the Apache-2.0 notice, the Apache-2.0 text and the
12
12
  Vera/Arev notices; the official Pretendard Regular only if the lit-pptx Hangul pair is missing;
13
13
  - with `--audio`, a Python venv holding the hash-pinned librosa set from `requirements-audio.txt`
14
- (`pip install --require-hashes --only-binary=:all:`).
14
+ (`pip install --require-hashes --only-binary=:all:`). The venv is built from the newest `python3.13`,
15
+ or `python3.12` on PATH, then `python3` or `python` if one of those is in that range; a
16
+ newer `python3`, such as 3.15, has no wheel for the pins and is passed over. `MOTION_PYTHON` names
17
+ one interpreter to use instead, and only that one.
15
18
 
16
19
  `litopencode install` attempts the same pre-warm and prints one receipt line; a failure never fails
17
20
  the install. A render never installs, fetches or repairs anything: it checks hashes and receipts and
@@ -20,7 +23,9 @@ stops with a named state.
20
23
  `litopencode motion-runtime status` and `litopencode doctor` report five probes on every run:
21
24
  Chrome (path and version), ffmpeg (version and the preview encoder rung: `libwebp_anim`, `img2webp`
22
25
  or GIF), the WebGL2 renderer string from a real headless probe, the software-GL or unknown-renderer
23
- warning, and the pre-warm state naming what is missing and the command that fixes it.
26
+ warning, and the pre-warm state naming what is missing and the command that fixes it. A sixth line,
27
+ `motion cover encoder`, names the encoder cover mode will use (`img2webp`, decoded with `dwebp`, or
28
+ `gifski`, decoded with ffmpeg) or says that none is installed.
24
29
 
25
30
  ## Exit codes
26
31
 
@@ -38,6 +43,11 @@ warning, and the pre-warm state naming what is missing and the command that fixe
38
43
  | 18 | STAGE_NONDETERMINISTIC | a sample frame differs between the master and a fresh replay from 0; the message names the frame and the first region | remove the clock or random source the renderer does not drive |
39
44
  | 19 | STAGE_NETWORK_REQUEST | the page requested something outside the stage origin, or a stage file names an external URL | make the asset local |
40
45
  | 20 | SOUND_INVALID | the planned track is missing from the film, too long or short, too loud, or a generated bed starts silent | fix the track or its plan |
46
+ | 21 | BLOCKED_NO_COVER_ENCODER | cover mode found neither img2webp with dwebp nor gifski with ffmpeg; checked before any capture | install libwebp (or gifski); a cover is never an MP4 (`cover.md`) |
47
+ | 22 | BLOCKED_ASSET_MISSING | cover mode: `assets.json` lists a file that is not there, or the painted recipe lists no base art | supply or generate the art and list it; never draw a stand-in |
48
+
49
+ Cover mode uses 13 and 16–19 too, with its own meanings for 17 and 18 (an unlisted raster; a frame
50
+ that differs when sought out of order); `cover.md` has its table.
41
51
 
42
52
  ## Chrome, egress and the sandbox
43
53
 
@@ -64,6 +74,11 @@ warning, and the pre-warm state naming what is missing and the command that fixe
64
74
  - Three browsers run side by side: the master capture, a fresh replay for the determinism samples and
65
75
  a separate QA replay that hides text to measure ink. The master never changes a style.
66
76
  - Fonts come from this skill and the pre-warmed cache; an unwarmed cache stops with exit 14.
77
+ - Cover mode (`cover.md`) launches the same software rung at device scale 2 with CPU raster
78
+ (`--disable-gpu-rasterization --disable-partial-raster`), serves `/lit/loop-kit.js` instead of the
79
+ stage kit, adds JetBrains Mono (bundled, `fonts/JetBrainsMono-wght-latin.woff2`) to `/lit/fonts.css`,
80
+ and injects an init script that makes every clock and entropy source throw. It runs three browsers
81
+ one after another: the master (frames, then title masks), a fresh replay, and a shuffled seek pass.
67
82
 
68
83
  ## Sound
69
84
 
@@ -5,6 +5,9 @@ animator and the editor: you write one page that draws the film as a function of
5
5
  renderer captures it frame by frame, deterministically, then gates, checks and encodes it. The page
6
6
  is yours; the kit gives motion primitives, never ready-made scenes, objects or layouts.
7
7
 
8
+ A README cover loop uses a different page contract: `cover/index.html` draws `renderFrame(t)` for a
9
+ loop time the renderer seeks in any order, with the loop kit instead of this one (`cover.md`).
10
+
8
11
  ## Files
9
12
 
10
13
  - `<out>/treatment.json`: the treatment (see `treatment.md`).
@@ -6,6 +6,10 @@ with exit 16 (`BLOCKED_TREATMENT_INVALID`) naming the first field that fails. A
6
6
  director's decision record: what the film is about, what the viewer sees in each beat, how it moves
7
7
  and sounds, and what would make it excellent. Load the path reference only after `path` is set.
8
8
 
9
+ A README, hero, banner or cover loop is not a film and has no beats: its treatment sets
10
+ `"mode": "cover"` and follows `cover.md` instead of this file. A film render refuses a cover
11
+ treatment with exit 16, and the other way round.
12
+
9
13
  ## Fields
10
14
 
11
15
  "Normalized" below means NFC, lowercase, then every space and punctuation mark removed. Before any
@@ -35,12 +35,14 @@ Usage:
35
35
  node render.mjs perf --brief <brief.json> --out <dir>
36
36
  node render.mjs gate --out <dir> [--viewed N]
37
37
  node render.mjs stage --out <dir> [--round 1|2|3] [--stills-only] [--detach]
38
+ node render.mjs cover --out <dir> [--round 1|2|3] [--window t0,t1] [--detach]
38
39
  node render.mjs sound --out <dir>
39
40
  node render.mjs look --out <dir> --round 1|2|3 --answers <answers.json>
40
41
  Options: --samples N (master default 4) --shutter S (default 0.5) --scale 1..4 --word-timing (Tier 3, opt-in)
41
42
  Exit codes: 0 OK, 10 BLOCKED_NO_CHROME, 11 BLOCKED_NO_WEBGL2, 12 BLOCKED_NO_FFMPEG_FOR_VIDEO,
42
43
  13 GATE_FAIL_QA, 14 BLOCKED_DEPS_NOT_PREWARMED, 15 BLOCKED_FONT_FETCH, 16 BLOCKED_TREATMENT_INVALID,
43
- 17 STAGE_CONTRACT_ERROR, 18 STAGE_NONDETERMINISTIC, 19 STAGE_NETWORK_REQUEST, 20 SOUND_INVALID`;
44
+ 17 STAGE_CONTRACT_ERROR, 18 STAGE_NONDETERMINISTIC, 19 STAGE_NETWORK_REQUEST, 20 SOUND_INVALID,
45
+ 21 BLOCKED_NO_COVER_ENCODER, 22 BLOCKED_ASSET_MISSING`;
44
46
 
45
47
  const pageScripts = ["common.js", "gl.js", "lines.js", "passes.js", "post.js", "type.js", "scenes.js", "engine.js"];
46
48
  const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -107,7 +109,7 @@ export function planRun(brief, briefPath, args, state) {
107
109
  else {
108
110
  const target = path.join(args.runDir, "audio-grid.json");
109
111
  const result = spawnSync(venv.python, [path.join(skillRoot, "audio-analysis.py"), audioFile, target], { encoding: "utf8", timeout: 300000 });
110
- if (result.status !== 0) warnings.push(`audio analysis failed; using text reading time: ${(result.stderr || result.error?.message || "").trim().split("\n").at(-1)}`);
112
+ if (result.status !== 0) warnings.push(`audio analysis failed; using text reading time: ${(result.stderr || result.error?.message || "").trim().split(/\r?\n/u).at(-1)}`);
111
113
  else {
112
114
  const grid = JSON.parse(readFileSync(target, "utf8"));
113
115
  if (grid.beatGrid.length >= 2 && grid.bpm >= 40 && grid.bpm <= 240) { audioTier = "librosa-beat-grid"; beatGrid = grid.beatGrid; bpm = grid.bpm; audio = grid; }
@@ -443,8 +445,8 @@ function rerender(out, briefPath, frames, samples, shutter, rung, tag) {
443
445
  const result = spawnSync(process.execPath, [fileURLToPath(import.meta.url), "frames", "--brief", briefPath, "--out", path.join(out, ".run", tag), "--plan", path.join(out, ".run", "plan.json"),
444
446
  "--frames", frames.join(","), "--samples", String(samples), "--shutter", String(shutter), "--rung", rung], { encoding: "utf8", timeout: 900000, maxBuffer: 16_000_000 });
445
447
  rmSync(path.join(out, ".run", tag), { recursive: true, force: true });
446
- if (result.status !== 0) throw new Error(`determinism re-render (${rung}) failed: ${(result.stderr || result.error?.message || "").trim().split("\n").slice(-2).join(" ")}`);
447
- return JSON.parse(result.stdout.trim().split("\n").at(-1)).hashes;
448
+ if (result.status !== 0) throw new Error(`determinism re-render (${rung}) failed: ${(result.stderr || result.error?.message || "").trim().split(/\r?\n/u).slice(-2).join(" ")}`);
449
+ return JSON.parse(result.stdout.trim().split(/\r?\n/u).at(-1)).hashes;
448
450
  }
449
451
 
450
452
  function determinism(out, briefPath, frames, engine, plan) {
@@ -480,9 +482,10 @@ function manifestFor(plan, engine, extra) {
480
482
  function lookMode(argv, out) {
481
483
  const round = Number(option(argv, "round", "0"));
482
484
  const entry = recordLook(out, round, option(argv, "answers") ? path.resolve(option(argv, "answers")) : undefined);
483
- console.log(`look round ${entry.round} recorded on the ${entry.kind === "film" ? "final render" : "stills set"}: ${Object.keys(entry.frames).length} frame(s) viewed${entry.blocked ? " (no vision tool: nobody viewed the frames)" : ""}`);
485
+ console.log(`look round ${entry.round} recorded on the ${{ film: "final render", cover: "cover render" }[entry.kind] ?? "stills set"}: ${Object.keys(entry.frames).length} frame(s) viewed${entry.defects?.length ? `, ${entry.defects.length} defect(s) logged` : ""}${entry.blocked ? " (no vision tool: nobody viewed the frames)" : ""}`);
484
486
  if (entry.needsAnotherRound.length) console.log(`another round is needed (questions ${entry.needsAnotherRound.join(", ")})${entry.round >= 3 ? "; round 3 is the last, so deliver with these open items stated" : ""}`);
485
487
  else if (entry.kind === "stills") console.log("next: make the change you named, then render the film with the next --round");
488
+ else if (entry.kind === "cover" && entry.round === 1) console.log("next: make the change you named (check a logged defect with `cover --window t0,t1`), then render the cover with --round 2");
486
489
  else console.log("next: node gate.mjs --done <dir>");
487
490
  return EXIT.OK;
488
491
  }
@@ -531,10 +534,30 @@ async function stageMode(argv, out, viewed) {
531
534
  return gate.exitCode;
532
535
  }
533
536
 
537
+ // `cover`: a README cover loop (engine/cover/). Every round is a full render gated on the decoded file;
538
+ // `--window t0,t1` re-renders only the frames around a logged defect. A detached run reports through .run/.
539
+ async function coverMode(argv, out) {
540
+ const round = Number(option(argv, "round", "1"));
541
+ if (!Number.isInteger(round) || round < 1 || round > 3) throw new Error("--round must be 1, 2 or 3");
542
+ if (argv.includes("--detach")) {
543
+ const { detachStage } = await import("./engine/stage/render.mjs");
544
+ const detached = detachStage(out, argv.slice(1), "cover");
545
+ console.log(`rendering the cover in the background (pid ${detached.pid}); the exit code lands in ${detached.exitFile}; log: ${detached.logFile}`);
546
+ return EXIT.OK;
547
+ }
548
+ const cover = await import("./engine/cover/render.mjs");
549
+ const span = option(argv, "window");
550
+ const result = span === undefined
551
+ ? await cover.renderCover(out, { round, log })
552
+ : await cover.renderCoverWindow(out, { round, window: span.split(",").map(Number), log });
553
+ process.stdout.write(result.report);
554
+ return result.code;
555
+ }
556
+
534
557
  async function run(argv) {
535
558
  const mode = argv[0];
536
559
  if (!mode || mode === "--help" || argv.includes("--help")) { console.log(usage); return EXIT.OK; }
537
- if (!["film", "stills", "sheet", "video", "perf", "gate", "frames", "stage", "sound", "look"].includes(mode)) { console.error(usage); return 2; }
560
+ if (!["film", "stills", "sheet", "video", "perf", "gate", "frames", "stage", "cover", "sound", "look"].includes(mode)) { console.error(usage); return 2; }
538
561
  const out = option(argv, "out") ? path.resolve(option(argv, "out")) : undefined;
539
562
  if (!out) throw new Error("--out <dir> is required");
540
563
  const viewed = argv.includes("--viewed") ? Number(option(argv, "viewed", "0")) || 0 : undefined;
@@ -544,6 +567,7 @@ async function run(argv) {
544
567
  return result.exitCode;
545
568
  }
546
569
  if (mode === "stage") return stageMode(argv, out, viewed);
570
+ if (mode === "cover") return coverMode(argv, out);
547
571
  if (mode === "sound") return soundMode(out);
548
572
  if (mode === "look") return lookMode(argv, out);
549
573
  let treatment = null;