| name | editing |
| description | How to edit a vean timeline as an agent — drive the .mlt through the MCP/CLI domain tools (apply-op / preview-op / undo / render / still), trust the ambient vean-lsp for diagnostics (never poll diagnose in the loop), read each tool's consequences + inverse + optional alerts, and use render→still as your eyes. Use when making any change to a .mlt document through the bridge (apply an op, fix a diagnostic, tighten a cut, duck audio, add a dissolve). |
Editing a vean timeline (the agent method)
The first real vean skill, written from the Move-2 build. It encodes the method
the agent bridge was designed for: edit a timeline the way you edit code.
The language server tells you what broke — ambiently, without being asked. The
domain tools tell you what each change did and how to take it back. The
render→still loop is your eyes, because you cannot watch video.
If you take one thing from this skill: don't confuse the three surfaces. They
have three different jobs, and blurring them is the one regression the whole
design exists to prevent.
| Surface | What it is | What it's for | How you reach it |
|---|
vean-lsp | ambient feedback | seeing — pushes the FULL current diagnostic set after every document change | runs in your editor/host; you read its diagnostics, you don't call it |
vean-mcp tools | domain actions | doing — apply/preview/undo an op, resolve a value, find references, render, still | MCP tools, or the vean ... CLI |
diagnose | a debug verb | a deliberate full report (CI gate, sanity sweep) — not the per-edit loop | the diagnose tool / vean timeline diagnose |
The mental model
A .mlt is a document. vean is a language server for video over it. The loop:
apply an op ──► read the result (consequences · inverse · optional alerts)
│ │
│ ambient: vean-lsp re-publishes the full diagnostic set
│ for the changed file — you didn't ask, you just see it
▼ ▼
render + still ──► READ the PNG ──► iterate, or undo (it's free + exact)
The payoff of doing it this way: an op carries its own inverse, so exploration
is reversible; the LSP carries the full picture, so a tool reply can stay focused
on mutation-local facts instead of flooding you on every call; and a still is a real frame,
so "the diagnostics are clean" never gets mistaken for "the cut looks right."
The loop, step by step
-
Discover before guessing. If you do not already know the exact operation
and argument shape, start with structured discovery:
vean discover "crossfade clips" --kind op --json
vean timeline ops list --json
vean timeline ops describe crossfade --json
vean timeline ops examples volume --json
Aliases are ergonomic only. crossfade resolves to canonical dissolve,
volume resolves to gain, and trim-out resolves to trimOut; durable
identity in results, inverses, registry ids, MCP tools, and undo stays
canonical.
-
Set or confirm the active timeline. Prefer a project-local
timeline:main route over repeating absolute paths:
vean timeline current --json
vean timeline use timelines/main.mlt --json
Once timeline:main exists, timeline commands may omit the URI. Use
--timeline <path|file://uri|route-alias> when you need an explicit target.
-
Mutate with an op, never by hand-editing XML. The only legal mutation path
is an op through the edit algebra (apply-op tool, or vean timeline apply-op). An op
is { op, args } — e.g. { op: "trimIn", args: { uuid: "<clip-uuid>", delta: 10 } },
{ op: "gain", args: { uuid: "<clip-uuid>", db: -6 } }, { op: "dissolve", args: { leftUuid, rightUuid, frames: 20 } }. Hand-editing the .mlt desyncs
clip identity and the played-window bookkeeping the diagnostics engine reads —
the ops exist precisely so you don't reason about that by hand.
- Refer to a clip by its stable uuid, not its index. Indices are ephemeral;
a uuid is the identity that survives the edit and that the inverse names.
-
Read the ToolResult before you render. Every mutating tool returns the
mutation facts — read them in this order:
consequences — the structured "what changed": which clips trimmed/moved,
whether a ripple shifted downstream clips, the duration delta, any warnings.
This is the report before a frame renders — the whole reason the ops layer
exists. Read it to confirm the edit did what you intended (e.g. that a trim
rippled the clips you expected and nothing else).
inverse — the op invocation that undoes this exact edit. Keep it; it's
how you take the edit back (step 6).
touchedUris — which document(s) changed (so you re-read them; the LSP
re-publishes for them).
alerts — optional, present only when this mutation introduced new
blocking error diagnostics. Clean edits omit it entirely. If it is present,
fix the error or undo before rendering.
-
Let the LSP show you the rest — don't poll diagnose. After the edit
writes the file, vean-lsp re-publishes the complete current diagnostic set for
that document. New adverse effects appear in your context ambiently, the same
way a type error does after you save a .ts file. Calling diagnose after an
ordinary edit is the anti-pattern this design forbids — diagnose is for a
deliberate full report (a CI gate, a one-off audit, a non-LSP client), never the
safety step that makes an edit loop OK. Tool alerts only tell you what this
mutation newly broke; the ambient set from the LSP is the full picture.
-
Prefer preview-op when you're unsure or comparing. preview-op returns
the same consequences + inverse + optional alerts a real edit would produce,
without writing the document. Reach for it when:
- the op might fail a precondition and you want to see the typed error first;
- you're choosing between two candidate edits (preview both, compare the
consequences/alerts, then
apply-op the winner);
- the op ripples and you want to confirm the blast radius before committing.
Reach straight for apply-op when the edit is unambiguous and you'll inspect
the rendered frame next anyway — undo is exact, so a wrong apply-op is cheap
to reverse.
-
SEE the frame — non-negotiable for anything perceptual. A clean diagnostic
set is the type-checker passing; it is not proof the cut looks right.
render the document to an MP4, then still the exact 0-based frame at the
moment you changed (both return the produced file in touchedUris — read that
PNG). For a fade, dissolve, or any motion, grab stills at a few frames across
the transition so you see the arc, not a single instant. Judge feel from the
pixels, not from the absence of diagnostics.
-
Undo is free and exact. Pass a prior result's inverse to the undo tool
(or vean timeline undo); it restores the prior IR deep-equal and re-publishes the
cleared diagnostics. This is what makes exploration cheap — try the tighter cut,
render it, keep it or undo it. (One caveat: an op that mints a new uuid —
split's left half — names that in-session uuid in its inverse, so undo it
within the session, before a serialize→parse reload renames it.)
Iterating a Remotion comp overlay — LIVE, never bake
Step 7's render + still loop is for footage / op edits (melt renders those).
A Remotion comp overlay is different: it is a first-class LIVE entity, and
baking it to a .mov to preview is the anti-pattern the composition work exists
to kill. (This bit hard on 2026-07-01 — a whole bake→melt-still rebuild loop;
DESIGN-LIVE-COMP-PREVIEW.md is the contract.)
- Add a comp with
vean timeline add-composition --composition <id> (action
timeline.addComposition) — a LIVE, no-bake edit. The clip carries
composition:{id,props} and a .vean/cache/remotion/<id>.mov resource path →
isGraphicClip (viewer/src/types.ts) → the viewer renders the comp live
via @remotion/player + the dynamic @project-comp glob, with HMR. The
.mov need not exist yet: it is consumed only by melt at EXPORT — the live
preview ignores it and reads composition.id from the IR. (timeline.addGraphic
remains for plain pre-rendered alpha .mov overlays.)
- To iterate a comp: edit its
.tsx and preview in the running viewer via the
drive skill (.agents/skills/drive/SKILL.md) — headless: bun run drive up --project <project> → agent-browser … open $URL → seek the scrubber (the
slider ref; key End jumps to the last frame) → screenshot. The edit
hot-reloads live; the playhead is preserved. Do NOT bake or render + still to
SEE a comp change — that's the slow, stale-artifact loop (a full ProRes bake
per edit). There is no standalone bake verb by design.
- Bake is internal to export:
vean render video <timeline> --out <deliverable>
self-bakes every comp overlay's alpha .mov from its composition.id right
before melt (via bakeOverlaysForExport). Run it ONCE for a final deliverable,
never during authoring — you never invoke a bake by hand.
How code actions fit (auto-fixing a diagnostic)
A diagnostic that has a single deterministic repair exposes an LSP code action
— a previewable text edit your host offers as a quick-fix. When the ambient LSP
flags one of these, prefer the code action over hand-fixing: it edits the right
byte, and applying it fires the same didChange → publishDiagnostics loop, so the
flagged diagnostic clears with no extra step. The three repairs that exist today:
in-out-beyond-source (a clip's played window runs past its source length)
→ "Trim out-point to N" — clamps the clip's <entry> out-point to length-1.
transition-inverted-window (a field transition's in > out) → "Swap the
transition window".
transition-no-overlap (the transition starts past the track's content)
→ clamps it onto the last content frame.
Two judgment points the design draws sharply:
- A quick-fix is self-contained; a choice is not. Defects that need a human
decision or a structural rewrite — relinking a missing asset to a new path,
picking a ripple direction, re-timing a same-track dissolve and its nested
tractor — are not code actions. They're your job through
apply-op. Don't
wait for a quick-fix that won't come; make the call and apply the op.
- A code action only exists for a defect the LSP can see, which means the
document parsed. A defect that makes the IR itself invalid surfaces as a
parse-error, not a fixable diagnostic.
Hard rules (the ones that bite)
- Timeline window vs source window. A clip's timeline (played) window lives
in its
<entry>; its source window lives in the <producer>. A diagnostic
about a played-window problem (e.g. in-out-beyond-source) is fixed by changing
the entry — that's where the IR reads [in, out]. Rewriting the producer
alone won't clear it. (The code action knows this; if you ever hand-fix, fix the
entry.)
- Ops refuse to write an invalid state — and that's correct. An op whose
precondition fails (a trim past source, a clip uuid that doesn't exist, a
dissolve longer than its clips) returns a typed
ToolError (kind +
detail), not a thrown exception and not a silently bad edit. Read the
kind, pick a valid argument or a different op. Don't try to force it.
- Color clips are positionless. Their
in/out are 0-based by convention and
a split re-bases them to [0, playtime-1]. Don't reason about a color clip's
window as if it indexed a file — it doesn't.
- Frame-exact integers only. Never introduce a float frame or a float fps —
one float makes every downstream diagnostic subtly, permanently wrong. fps is
[num, den] (29.97 is 30000/1001).
- Never reimplement a diagnostic in the loop. If a check seems missing, it
belongs in
src/diagnostics/ (called by every surface) — never inline in a tool
call or worked around in the edit. That's a hard project boundary, not a style
note.
Reading consequences, inverse, and alerts (a worked shape)
{
"ok": true,
"consequences": { },
"inverse": { "op": "trimIn", "args": { "uuid": "clip-3", "delta": -10 } },
"touchedUris": ["file:///…/cut.mlt"]
}
No alerts field means the edit did not introduce a new blocking error. Had the
trim pushed a clip past its source, the result would include alerts: [...]; fix
that or undo before rendering. Existing warnings and standing health belong to the
ambient LSP and deliberate diagnose, not every mutation response.
CLI surface (when there's no MCP host)
Every MCP tool is projected from the same action registry as the Commander CLI.
Note:
without an LSP host you lose the ambient push — the CLI prints the consequence +
inverse, and you reach for vean timeline diagnose <file> deliberately for the full
set (this is the legitimate non-LSP use of diagnose, not per-edit polling).
| Tool | CLI |
|---|
| discover | vean discover [query] --json |
| op catalog | vean timeline ops list/describe/examples --json |
| active timeline | vean timeline use <path-or-alias> --json |
| apply-op | vean timeline apply-op <op-or-alias> --args-json '<json>' --json |
| preview-op | vean timeline preview-op <op-or-alias> --args-json '<json>' --json |
| undo | vean timeline undo --inverse-json '<json>' --json |
| render | vean render video <file> --out <path> --json |
| still | vean render still <file> <frame> --out <path> --json |
| resolve-value-at-frame | vean timeline resolve-value-at-frame <frame> --target-json '<json>' --json |
| find-references | vean timeline find-references --query-json '<json>' --json |
| diagnose (debug only) | vean timeline diagnose [file] --json |
Host setup
Do not do host setup from this skill. Use the setup skill
(.agents/skills/setup/SKILL.md) and bun run doctor for fresh clones, Claude
Code/Codex wiring, system dependency checks, and LSP/MCP startup verification.
Verifying your own work (the gate)
bun run move2:e2e runs the whole loop end-to-end (op → ambient clean → render →
still) over seeded tasks and asserts the tool-output discipline — run it after any
bridge change. The standing gates are bun run test, bun run typecheck,
bun run lint, bun run lint:xml, and bun run verify:corpus.