versioncam 0.1.1 → 0.1.3

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 (44) hide show
  1. package/CHANGELOG.md +79 -37
  2. package/README.md +382 -242
  3. package/bin/versioncam.js +1 -1
  4. package/dist/app-server.js +2 -2
  5. package/dist/app-server.js.map +1 -1
  6. package/dist/cli/commands/doctor.js +2 -2
  7. package/dist/cli/commands/doctor.js.map +1 -1
  8. package/dist/cli/commands/init.js +1 -1
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/record.js +3 -2
  11. package/dist/cli/commands/record.js.map +1 -1
  12. package/dist/cli/commands/render.js +1 -1
  13. package/dist/cli/commands/render.js.map +1 -1
  14. package/dist/cli/commands/stability.js +4 -4
  15. package/dist/cli/commands/stability.js.map +1 -1
  16. package/dist/cli/main.js +0 -0
  17. package/dist/cli/usage.d.ts +9 -5
  18. package/dist/cli/usage.js +36 -23
  19. package/dist/cli/usage.js.map +1 -1
  20. package/dist/core/timeline.d.ts +22 -0
  21. package/dist/core/timeline.js +10 -0
  22. package/dist/core/timeline.js.map +1 -1
  23. package/dist/driver/clip.d.ts +4 -1
  24. package/dist/driver/clip.js +9 -1
  25. package/dist/driver/clip.js.map +1 -1
  26. package/dist/driver/session.d.ts +13 -0
  27. package/dist/driver/session.js +57 -1
  28. package/dist/driver/session.js.map +1 -1
  29. package/dist/driver/settle.js +2 -2
  30. package/dist/driver/settle.js.map +1 -1
  31. package/dist/inspect/inspect.js +1 -1
  32. package/dist/page/index.html +1 -1
  33. package/dist/render/render.js +1 -1
  34. package/dist/render/render.js.map +1 -1
  35. package/dist/render/sampling.js +1 -1
  36. package/dist/render/sampling.js.map +1 -1
  37. package/dist/review/review.js +3 -3
  38. package/dist/review/review.js.map +1 -1
  39. package/dsl.md +230 -91
  40. package/package.json +2 -2
  41. package/plugin/README.md +53 -47
  42. package/dist/.types-render/render-page/draw.d.ts +0 -28
  43. package/dist/.types-render/render-page/main.d.ts +0 -24
  44. package/dist/.types-render/render-page/theme.d.ts +0 -37
package/dsl.md CHANGED
@@ -1,119 +1,258 @@
1
1
  # The clip DSL
2
2
 
3
- Everything a clip body can call on its session, `s`. `test/dsl.spec.ts`
4
- asserts that this list and `RecordingSession` name exactly the same members, so
5
- a method here exists and a method that exists is here. The README promised an
6
- `s.drag` for months while there was none; there is one now, because a clip that
7
- needed it had been silently doing nothing instead.
3
+ Everything a clip body can call on its session, `s`, with every option and its
4
+ default. A test holds this page and the session's code to the same list: a
5
+ method here exists, and a method that exists is here.
8
6
 
9
- Three rules the whole surface follows:
7
+ Three rules hold across the whole surface:
10
8
 
11
9
  - **Every `await` spends clip time and captures frames.** `hold`, `click`,
12
10
  `type` and the rest advance the authored clock.
13
- - **A track write spends none.** `camera.*`, `highlight` and `caption` write at
14
- the current instant and return immediately, so each one needs frames after it
15
- — a `hold` or the next action — or it is never seen.
16
- - **Target by role or test id, never by coordinates.** A clip built from
17
- locators survives a layout change; one built from points does not.
11
+ - **A track write spends none.** `camera.*`, `highlight` and `caption` write
12
+ at the current instant and return at once. Each needs frames after it, from
13
+ a `hold` or the next action, or it is never seen.
14
+ - **Target by test id or role, never by coordinates.** A clip built from
15
+ locators survives a layout change. One built from points does not.
16
+
17
+ Durations are in milliseconds, except a caption's, which are in seconds. A
18
+ point is `[x, y]` in viewport pixels, and a rect is `{ x, y, w, h }`.
18
19
 
19
20
  ## Locators
20
21
 
21
- They resolve when used, not when written, and wait up to 15 seconds for the
22
- element to be visible. A locator that matches several elements is an error
23
- naming the count — narrow it rather than reaching for the first match.
24
-
25
- - `byTestId(testId)` → `Locator` — first choice: `data-testid` is the only
26
- handle an app can promise a clip.
27
- - `byRole(role, options?)` → `Locator` — second choice: `byRole("button", { name: "Save" })`
28
- is how a person would describe it.
29
- - `byLabel(text)` → `Locator` — a form field, by its label.
30
- - `byPlaceholder(text)` → `Locator` — a field with no label but a placeholder.
31
- - `byText(text)` → `Locator` — content, not controls, and the one most likely
32
- to match twice.
33
- - `css(selector)` → `Locator` — the escape hatch; a CSS selector is a claim
34
- about markup, which is the thing most likely to change.
22
+ A locator resolves when it is used, not when it is written. It waits up to 15
23
+ seconds for its element to be visible. A locator that matches several
24
+ elements is an error that names the count: narrow it rather than reaching for
25
+ the first match.
26
+
27
+ ### `byTestId(testId)`
28
+
29
+ First choice. `data-testid` is the only handle an app can promise a clip.
30
+
31
+ ### `byRole(role, options?)`
32
+
33
+ Second choice, and how a person would describe the control:
34
+ `byRole("button", { name: "Save" })`. The options are Playwright's for
35
+ `getByRole`.
36
+
37
+ ### `byLabel(text)`
38
+
39
+ A form field, by its label or its `aria-label`.
40
+
41
+ ### `byPlaceholder(text)`
42
+
43
+ A field with no label but a placeholder.
44
+
45
+ ### `byText(text)`
46
+
47
+ Content rather than a control, by a string or a regular expression. The
48
+ locator most likely to match twice.
49
+
50
+ ### `css(selector)`
51
+
52
+ The escape hatch. A CSS selector is a claim about markup, which is the thing
53
+ most likely to change.
35
54
 
36
55
  ## Time
37
56
 
38
- - `hold(ms)` — stay still and keep capturing. The pause that makes a beat
39
- readable, and what a track write needs after it.
40
- - `settle(options?)` — wait for the page to stop working: DOM quiet, no
41
- requests in flight, fonts loaded, and whatever `settle.ready` adds. Advances
42
- the page's clock without capturing, so the wait never reaches the video.
43
- `{ label }` is what `versioncam review` checks a beat sheet against, and what
44
- `--at-marks` samples, so give every beat one. Also takes `{ quietMs,
45
- timeoutMs, yieldMs, hook }`.
46
- - `offCamera(fn)` — run raw Playwright with nothing captured and no time spent:
47
- mocking an API, seeding data, priming a conversation. `fn` is given the
48
- Playwright `Page`, which is also `s.page` if you need it directly. No time
49
- on the page's clock either: what the app needs time to finish after `fn`,
50
- a `settle` gives it.
57
+ ### `hold(ms)`
58
+
59
+ Stay still and keep capturing. It is the pause that makes a beat readable,
60
+ and what a track write needs after it.
61
+
62
+ ### `settle(options?)`
63
+
64
+ Wait for the page to stop working: the DOM quiet, no requests in flight, the
65
+ fonts loaded, and whatever `settle.ready` in the config adds. It advances the
66
+ page's clock without capturing, so the wait never reaches the video.
67
+
68
+ Give every beat a `label`. `versioncam review` checks a beat sheet against the
69
+ labels, and `sheet --at-marks` samples the frames they mark. A settle that
70
+ reaches `timeoutMs` warns and carries on, so the clip still records, and
71
+ `review` fails it.
72
+
73
+ | Option | Default | What it sets |
74
+ |---|---|---|
75
+ | `label` | none | The beat this wait marks. |
76
+ | `quietMs` | the config's, `150` | How long the page must stay quiet, on its own clock. |
77
+ | `timeoutMs` | the config's, `15000` | Real time to wait before giving up. |
78
+ | `yieldMs` | the config's, `20` | Real time yielded per tick, so the network and the renderer can work. |
79
+ | `hook` | the config's `settle.ready` | `() => Promise<boolean>`, one more condition, for this wait only. |
80
+
81
+ ### `offCamera(fn)`
82
+
83
+ Run raw Playwright with nothing captured and no time spent: mocking an API,
84
+ seeding data, priming a conversation. `fn` gets the Playwright `Page`, which
85
+ is also `s.page`, and `offCamera` returns what `fn` returns. The page's clock
86
+ does not move either. Whatever the app needs time to finish after `fn`, a
87
+ `settle` gives it.
51
88
 
52
89
  ## Navigation
53
90
 
54
- - `open(path, options?)` — go to `baseUrl + path`, sign in if the config says
55
- how, settle, and capture frame 0. Every clip starts here. `{ settleTimeoutMs }`
56
- for an app that boots slowly; the default is 90 seconds.
91
+ ### `open(path, options?)`
92
+
93
+ Go to `baseUrl` plus `path`, or to `path` itself when it is a full URL. Then
94
+ sign in, if the config's `auth` has a `login` hook, settle, and capture frame
95
+ 0. Every clip starts here.
96
+
97
+ | Option | Default | What it sets |
98
+ |---|---|---|
99
+ | `settleTimeoutMs` | `90000` | How long the first settle may take, for an app that boots slowly. |
57
100
 
58
101
  ## Pointer
59
102
 
60
- - `moveTo(target, options?)` — glide to a locator or a point without pressing.
61
- `{ duration }` overrides the Fitts-law timing.
62
- - `hover(target, options?)` — move there and stay, the way a person does before
63
- reading a tooltip. `{ dwell, duration }`.
64
- - `click(target?, options?)` — approach, dwell, press, release. Called with no
65
- target it clicks where the cursor already is. `{ dwell }`.
66
- - `drag(target, from, to, options?)` — press, draw a straight stroke, release.
67
- `from` and `to` are fractions of the target's own box, so the drag survives a
68
- layout change: `{ x: 0.18, y: 0.5 }` to `{ x: 0.36, y: 0.5 }` crosses the
69
- middle. For a control that answers a stroke and ignores a click — a brush, a
70
- slider, a range handle. Mind the edges: a chart's axis labels live inside its
71
- own box, and a drag starting at x 0.08 selects that text instead of the
72
- control. `{ duration, modifiers }`.
73
- - `lasso(target, planner, options?)` — press, follow a smoothed closed loop,
74
- release. The planner comes from `loop([[0.3, 0.3], …])`, exported beside the
75
- session, and is written in fractions of the target's box, so the stroke
76
- survives a layout change. Measure first: `versioncam measure <path> <selector>`.
77
- `{ duration, modifiers }`.
78
- A frame taken partway through the stroke looks wrong, and is not: the
79
- smoothing curves outside the points it passes through, so a half-drawn loop
80
- swings wide of the thing it is about to enclose before coming back. Judge the
81
- selection from a frame after the loop closes, not from the middle of it.
103
+ A target is a locator or a point. The cursor travels to it along a curved
104
+ path, timed by Fitts's law: longer for a far or small target.
105
+
106
+ ### `moveTo(target, options?)`
107
+
108
+ Glide to a locator or a point without pressing.
109
+
110
+ | Option | Default | What it sets |
111
+ |---|---|---|
112
+ | `duration` | from distance and size | How long the move takes. |
113
+
114
+ ### `hover(target, options?)`
115
+
116
+ Move there and stay, the way a person does before reading a tooltip.
117
+
118
+ | Option | Default | What it sets |
119
+ |---|---|---|
120
+ | `dwell` | `220` | How long to stay once there. |
121
+ | `duration` | from distance and size | How long the move takes. |
122
+
123
+ ### `click(target?, options?)`
124
+
125
+ Approach, dwell, press, release. With no target it clicks where the cursor
126
+ already is. The press lasts 70 to 100 milliseconds, seeded.
127
+
128
+ | Option | Default | What it sets |
129
+ |---|---|---|
130
+ | `dwell` | `80` to `150`, seeded | How long to rest on the target before pressing. |
131
+
132
+ ### `drag(target, from, to, options?)`
133
+
134
+ Press, draw a straight stroke, release. For a control that answers a stroke
135
+ and ignores a click: a brush, a slider, a range handle. `target` is a locator
136
+ or a rect. `from` and `to` are fractions of its box, written `{ x, y }`, so
137
+ the drag survives a layout change: `{ x: 0.18, y: 0.5 }` to
138
+ `{ x: 0.36, y: 0.5 }` crosses the middle.
139
+
140
+ Mind the edges. A chart's axis labels live inside its own box, and a drag
141
+ that starts at x 0.08 selects that text instead of the control.
142
+
143
+ | Option | Default | What it sets |
144
+ |---|---|---|
145
+ | `duration` | `1400` | How long the stroke takes, from press to release. |
146
+ | `modifiers` | none | Keys held through the stroke: `["Shift"]`, `["Alt"]`, or both. |
147
+
148
+ ### `lasso(target, planner, options?)`
149
+
150
+ Press, follow a smoothed closed loop, release. The planner comes from
151
+ `loop([[0.3, 0.3], …])`, exported beside `clip`, and its points are fractions
152
+ of the target's box, so the stroke survives a layout change. Find the
153
+ fractions with `versioncam measure <path> <selector>`.
154
+
155
+ A frame from partway through the stroke looks wrong, and is not. The
156
+ smoothing curves outside the points it passes through, so a half-drawn loop
157
+ swings wide of what it is about to enclose before it comes back. Judge the
158
+ selection from a frame after the loop closes.
159
+
160
+ | Option | Default | What it sets |
161
+ |---|---|---|
162
+ | `duration` | `1400` | How long the stroke takes, from press to release. |
163
+ | `modifiers` | none | Keys held through the stroke: `["Shift"]`, `["Alt"]`, or both. |
82
164
 
83
165
  ## Keyboard
84
166
 
85
- - `typeInto(target, text)` — click the field, then type. What you want almost
86
- always: without the click the keystrokes go wherever focus happens to be.
87
- - `type(text)` — type at a human cadence into whatever has focus.
88
- - `press(key)` — one key, immediately. `"Enter"`, `"Escape"`, `"Meta+a"`.
167
+ ### `typeInto(target, text)`
168
+
169
+ Click the field, then type. This is almost always what you want: without the
170
+ click, the keystrokes go wherever focus happens to be.
171
+
172
+ ### `type(text)`
173
+
174
+ Type into whatever has focus, at a human cadence: about 55 milliseconds a key,
175
+ varied, with a longer pause after a space or punctuation.
176
+
177
+ ### `press(key)`
178
+
179
+ One key, at once: `"Enter"`, `"Escape"`, `"Meta+a"`.
89
180
 
90
181
  ## Camera, highlight, caption
91
182
 
92
- None of these spends clip time. Each needs frames after it.
93
-
94
- - `camera.zoom(target, scale?, options?)` — frame a locator or a rect, padded,
95
- keeping the target inside the frame even at the edge of the page.
96
- `scale` is a **ceiling, not a multiplier**: it caps how far in the camera may
97
- go (the frame is never narrower than `viewport.width / scale`), and the
98
- default cap of 1.65 exists so a captured pixel is never upscaled. A small
99
- target does not fill the frame as a result — at 1440 wide, `zoom(x, 1.7)`
100
- still shows about 850 pixels of page, which for a 310-pixel card reads as
101
- barely zoomed at all. If you want the target to dominate the shot, pass a
102
- rect from `versioncam measure` to `camera.to` instead.
103
- - `camera.to(rect, options?)` — frame an exact rectangle in viewport pixels.
104
- - `camera.reset(options?)` — back to the whole viewport. Ending a clip on this
105
- and nothing else is the classic mistake: `versioncam review` warns about it.
106
- - `highlight(target, style, options?)` — `"ring"` around it or `"spotlight"` on
107
- it, drawn in post at output resolution. `{ for }` in milliseconds.
108
- - `caption(text, options?)` — a line of text from now until the next caption or
109
- the end of the clip. It starts where it is written, so a second caption
110
- mid-clip replaces the first rather than doubling over it. `{ from, to }` in
111
- seconds to place it by hand.
183
+ None of these spends clip time, so each needs frames after it.
184
+
185
+ ### `camera.zoom(target, scale?, options?)`
186
+
187
+ Frame a locator or a rect, padded by 12% of its size on each side, and keep
188
+ the frame inside the page even at its edge.
189
+
190
+ `scale` is a **ceiling, not a multiplier**. It caps how far in the camera may
191
+ go: the frame is never narrower than `viewport.width / scale`. So a small
192
+ target does not fill the frame. At 1440 wide, `zoom(x, 1.7)` still shows
193
+ about 850 pixels of page, which for a 310-pixel card reads as barely zoomed.
194
+ For a target that dominates the shot, pass a rect from `versioncam measure` to
195
+ `camera.to` instead.
196
+
197
+ The default, 1.5, is as far in as the camera goes before captured pixels are
198
+ upscaled, for a 1440-wide page captured at 2× into a 1920-wide video. Past
199
+ it, the zoom softens.
200
+
201
+ | Option | Default | What it sets |
202
+ |---|---|---|
203
+ | `scale` | `1.5` | The most the camera may magnify. |
204
+ | `duration` | `1100` | How long the camera takes to get there. |
205
+
206
+ ### `camera.to(rect, options?)`
207
+
208
+ Frame an exact rectangle, in viewport pixels.
209
+
210
+ | Option | Default | What it sets |
211
+ |---|---|---|
212
+ | `duration` | `1100` | How long the camera takes to get there. |
213
+
214
+ ### `camera.reset(options?)`
215
+
216
+ Back to the whole viewport. Ending a clip on this and nothing after it is the
217
+ classic mistake, and `versioncam review` warns about it.
218
+
219
+ | Option | Default | What it sets |
220
+ |---|---|---|
221
+ | `duration` | `1100` | How long the camera takes to get there. |
222
+
223
+ ### `highlight(target, style, options?)`
224
+
225
+ A `"ring"` around a locator or a rect, or a `"spotlight"` on it, drawn at
226
+ output resolution. A locator is measured once, when the line runs: the
227
+ highlight stays where the element was.
228
+
229
+ | Option | Default | What it sets |
230
+ |---|---|---|
231
+ | `for` | `1800` | How long the highlight stays up. |
232
+
233
+ ### `caption(text, options?)`
234
+
235
+ A line of text from now until the next caption, or the end of the clip. It
236
+ starts where it is written, so a second caption mid-clip replaces the first
237
+ rather than doubling over it.
238
+
239
+ | Option | Default | What it sets |
240
+ |---|---|---|
241
+ | `from` | now | When the caption appears, in seconds. |
242
+ | `to` | the next caption | When it goes, in seconds. |
112
243
 
113
244
  ## What the runner reads
114
245
 
115
- Not for clips; the CLI reports them.
246
+ These are for the CLI, not for clips.
247
+
248
+ ### `cost`
249
+
250
+ Where the wall-clock time of this recording went.
251
+
252
+ ### `settles`
253
+
254
+ Every settle so far, as written to `settles.json`.
255
+
256
+ ### `at`
116
257
 
117
- - `cost` — where the wall-clock time of this recording went.
118
- - `settles` — every settle so far, as written to `settles.json`.
119
- - `at` — how far the clip has got, as `{ frame, t }`.
258
+ How far the clip has got, as `{ frame, t }`.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "versioncam",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
- "description": "Record demo clips of a real web app: scripted, deterministic, regenerated on deploy.",
5
+ "description": "Record demo clips of a real web app: scripted, deterministic, re-recorded on every push.",
6
6
  "license": "FSL-1.1-ALv2",
7
7
  "author": "Peter Bulovec",
8
8
  "homepage": "https://version.cam",
package/plugin/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # The versioncam skill
2
2
 
3
- `/versioncam "<what the clip should show>"` records a clip of your web app: it
3
+ `/versioncam "<what the clip should show>"` records a clip of your web app. It
4
4
  writes the script, records it, has a reviewer look at the result, and fixes
5
- what the reviewer finds — until the reviewer passes it or the rounds run out.
6
- The first time it runs in a repository, it sets the repository up first.
5
+ what the reviewer finds. It stops when the reviewer passes the clip or the
6
+ rounds run out. The first time it runs in a repository, it sets the
7
+ repository up first.
7
8
 
8
9
  Nothing here calls an API. The model is whichever agent session runs the
9
10
  skill, and the vision step is that session's file reader opening a contact
@@ -17,11 +18,11 @@ The skill ships inside the `versioncam` npm package:
17
18
  npx versioncam init # .claude/skills/versioncam/ and .claude/agents/versioncam-reviewer.md
18
19
  ```
19
20
 
20
- `init` installs the skill and nothing else — no config: the first run writes
21
- that. It refuses to overwrite a file that is already there, and refuses the
22
- whole install if any one is; `--force` overwrites. For an agent host that reads
23
- skills from somewhere other than `.claude/`, `--into <dir>` puts the skill
24
- directory at `<dir>/versioncam/`, with the reviewer inside it.
21
+ `init` installs the skill and nothing else, not even a config: the first run
22
+ writes that. It refuses to overwrite a file that is already there, and
23
+ refuses the whole install if any one is. `--force` overwrites. For an agent
24
+ host that reads skills from somewhere other than `.claude/`, `--into <dir>`
25
+ puts the skill directory at `<dir>/versioncam/`, with the reviewer inside it.
25
26
 
26
27
  Or, without copying anything, load the plugin directory as it is:
27
28
 
@@ -38,40 +39,45 @@ claude --plugin-dir node_modules/versioncam/plugin
38
39
 
39
40
  ## What it does
40
41
 
41
- **The first run** — no `versioncam.config.ts` in the directory — sets the
42
- repository up (`skills/versioncam/onboarding.md`): it works out how the app
43
- starts and where it is served, from `package.json`, the framework's config and
44
- the README, and asks when there is more than one plausible answer; asks whether
45
- the app signs in and how you want that handled; writes the config, with a
46
- `webServer` so every command starts the app itself; runs `doctor` and
47
- `inspect` to prove the app starts and settles; and records a three-second clip
48
- twice with `stability`, to prove the app records the same way twice before a
49
- clip is written for it. Nothing is committed.
42
+ **The first run**, in a directory with no `versioncam.config.ts`, sets the
43
+ repository up (`skills/versioncam/onboarding.md`):
50
44
 
51
- **Every run** then (`skills/versioncam/SKILL.md`):
45
+ 1. It works out how the app starts and where it is served, from
46
+ `package.json`, the framework's config and the README. When there is more
47
+ than one plausible answer, it asks.
48
+ 2. It asks whether the app signs in, and how you want that handled.
49
+ 3. It writes the config, with a `webServer` so every command starts the app
50
+ itself.
51
+ 4. It runs `doctor` and `inspect` to prove the app starts and settles.
52
+ 5. It records a three-second clip twice with `stability`, to prove the app
53
+ records the same way twice before a clip is written for it.
52
54
 
53
- 1. `versioncam doctor` — which starts the app — and `inspect /`.
55
+ Nothing is committed.
56
+
57
+ **Every run** then goes like this (`skills/versioncam/SKILL.md`):
58
+
59
+ 1. `versioncam doctor`, which starts the app, and `inspect /`.
54
60
  2. The session writes the clip itself, following
55
- `skills/versioncam/authoring.md`: it reads `versioncam dsl` and the existing
56
- clips, writes a beat sheet, writes the clip, records it in draft, and runs
57
- the deterministic checks in `versioncam review`. Three recordings at most,
58
- then on. On a page too big to hold in context, it hands this to a subagent
59
- and keeps that subagent for later rounds.
60
- 3. A contact sheet sampled at the beats, and a **fresh reviewer** on it — one
61
- picture, a five-item checklist, a JSON verdict. Fresh every round,
62
- deliberately: it sees the clip, never the argument for it.
63
- 4. On a pass, a full-quality record and render. Otherwise the fixes the verdict
64
- names, and another round.
65
-
66
- The reviewer is `agents/versioncam-reviewer.md`, one file used three ways:
67
- registered as an agent, which is what Claude Code does with it; as the prompt
68
- of a plain subagent, on a host that cannot register agents; or as a checklist
69
- the session applies to its own sheet, on a host with no subagents at all. The
70
- verdict records which, as `"reviewer": "restricted"`, `"unrestricted"` or
71
- `"self"` — a clip passed by its own author is a weaker claim, and says so.
72
-
73
- The skill never runs `git`. The clip it writes lands in your clips directory as
74
- an ordinary file, for you to read and commit yourself.
61
+ `skills/versioncam/authoring.md`. It reads `versioncam dsl` and the
62
+ existing clips, writes a beat sheet, writes the clip, records it in draft,
63
+ and runs the checks in `versioncam review`. Three recordings at most, then
64
+ it moves on. On a page too big to hold in context, it hands this step to a
65
+ subagent and keeps that subagent for later rounds.
66
+ 3. A contact sheet sampled at the beats, and a **fresh reviewer** on it: one
67
+ picture, a five-item checklist, a JSON verdict. The reviewer is new every
68
+ round, on purpose. It sees the clip, never the argument for it.
69
+ 4. On a pass, a full-quality record and render. Otherwise the fixes the
70
+ verdict names, and another round.
71
+
72
+ The reviewer is `agents/versioncam-reviewer.md`, one file used three ways.
73
+ Claude Code registers it as an agent. A host that cannot register agents uses
74
+ it as a plain subagent's prompt. A host with no subagents at all applies it as
75
+ a checklist to the session's own sheet. The verdict records which, as
76
+ `"reviewer": "restricted"`, `"unrestricted"` or `"self"`. A clip passed by its
77
+ own author is a weaker claim, and says so.
78
+
79
+ The skill never runs `git`. The clip it writes lands in your clips directory
80
+ as an ordinary file, for you to read and commit yourself.
75
81
 
76
82
  ## What it leaves behind
77
83
 
@@ -87,19 +93,19 @@ an ordinary file, for you to read and commit yourself.
87
93
  round-2/ …
88
94
  ```
89
95
 
90
- `.versioncam/` is a build product and is gitignored. Keep the clip; the
96
+ `.versioncam/` is a build product and is gitignored. Keep the clip. The
91
97
  transcripts are for reading when something goes wrong, and for finding the
92
- next thing to fix in the recorder — the paragraph on awkward tooling is there
98
+ next thing to fix in the recorder. The paragraph on awkward tooling is there
93
99
  because the first agent to write one produced a list that became a work
94
100
  package.
95
101
 
96
102
  ## What it costs
97
103
 
98
- On the example app, headless, with an earlier two-agent version of this loop:
99
- one clip took between six and twelve minutes, in one or two rounds, and all six
104
+ On the example app, headless, with an earlier two-agent version of this loop,
105
+ one clip took between six and twelve minutes, in one or two rounds. All six
100
106
  intents across two apps passed. Most of the time is model latency rather than
101
- the recorder: a round is a handful of recordings of a few seconds each, and a
107
+ the recorder. A round is a handful of recordings of a few seconds each, and a
102
108
  reviewer's single look at a single picture still takes a minute or two end to
103
- end. It runs on a subscription rather than a metered key, so the setup cost is
104
- close to zero; for anything higher-volume the prompts in these files are what
105
- would move to a direct SDK loop, unchanged.
109
+ end. It runs on a subscription rather than a metered key, so the setup cost
110
+ is close to zero. For anything higher-volume, the prompts in these files are
111
+ what would move to a direct SDK loop, unchanged.
@@ -1,28 +0,0 @@
1
- import type { Timeline } from "../src/core/timeline";
2
- import type { RenderConfig } from "../src/config";
3
- export type Canvas2D = CanvasRenderingContext2D;
4
- export type Frames = {
5
- /** Decoded state images, indexed the same way `timeline.states` is. */
6
- images: (ImageBitmap | HTMLImageElement)[];
7
- };
8
- /**
9
- * Which state to show at `frame`, and what to crossfade it with.
10
- *
11
- * Only a settle produces a crossfade. Everywhere else a change between
12
- * consecutive frames is a real one-frame change and must stay crisp —
13
- * blurring those would smear the cursor's own motion.
14
- */
15
- export declare function stateForFrame(timeline: Timeline, frame: number, crossfadeFrames: number): {
16
- current: number;
17
- previous: number | null;
18
- alpha: number;
19
- };
20
- /**
21
- * Draw one output frame. A pure function of `frame` — nothing here reads the
22
- * wall clock, which is what lets the renderer be scrubbed, re-run and
23
- * byte-compared.
24
- */
25
- export declare function drawRecordingFrame(ctx: Canvas2D, timeline: Timeline, frames: Frames, frame: number, canvas: {
26
- width: number;
27
- height: number;
28
- }, config: RenderConfig): void;
@@ -1,24 +0,0 @@
1
- /**
2
- * The render page.
3
- *
4
- * Two jobs, one code path: a scrubber for tuning motion by eye, and a
5
- * `window.__render.setFrame(n)` the encoder drives headlessly. They must be
6
- * the same path — a preview that renders differently from the export is worse
7
- * than no preview.
8
- *
9
- * Everything it needs arrives over routes the renderer installs, not from a
10
- * web server: the recording, the config, and the app's scene bundle if it has
11
- * one. Rendering therefore needs nothing running except this page.
12
- */
13
- type RenderApi = {
14
- ready: Promise<void>;
15
- setFrame: (frame: number) => Promise<void>;
16
- durationFrames: number;
17
- fps: number;
18
- };
19
- declare global {
20
- interface Window {
21
- __render?: RenderApi;
22
- }
23
- }
24
- export {};
@@ -1,37 +0,0 @@
1
- /**
2
- * The fixed half of the clip's look.
3
- *
4
- * What an app chooses — its accent, whether there is a backdrop and a device
5
- * frame at all — comes from the config. What is here is the craft that should
6
- * not vary between apps: corner radii, shadow weights, the cursor's geometry,
7
- * the caption's type. An app that wants these different is asking for a
8
- * different renderer, not a different config.
9
- */
10
- export declare const CHROME: {
11
- /** Device frame, used only when presentation asks for one. */
12
- readonly radius: 16;
13
- readonly hairline: "rgba(255, 255, 255, 0.16)";
14
- readonly shadow: "rgba(0, 0, 0, 0.45)";
15
- readonly shadowBlur: 60;
16
- readonly shadowOffsetY: 18;
17
- readonly captionFont: "600 30px \"Helvetica Neue\", Helvetica, Arial, \"Liberation Sans\", sans-serif";
18
- readonly captionColor: "rgba(255, 255, 255, 0.94)";
19
- readonly captionShadow: "rgba(0, 0, 0, 0.5)";
20
- /** The band a caption sits on when it overlays the app rather than the padding. */
21
- readonly captionScrim: "rgba(0, 0, 0, 0.45)";
22
- readonly captionScrimHeight: 86;
23
- readonly cursorFill: "#ffffff";
24
- readonly cursorStroke: "rgba(0, 0, 0, 0.55)";
25
- readonly cursorShadow: "rgba(0, 0, 0, 0.35)";
26
- readonly ringWidth: 3;
27
- readonly ringRadius: 10;
28
- readonly spotlightDim: 0.55;
29
- readonly spotlightTint: "6, 2, 16";
30
- };
31
- /**
32
- * `#rrggbb` plus an alpha, for the ring's glow.
33
- *
34
- * Falls back to the colour untouched if it is not a hex triple, so a config
35
- * can use `rgb()` or a named colour and only lose the glow.
36
- */
37
- export declare function withAlpha(color: string, alpha: number): string;