diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index 368de9f..ae9f2f3 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -25,7 +25,8 @@
"skills/procedural-materials-and-shaders/SKILL.md",
"skills/depsgraph-and-evaluated-data/SKILL.md",
"skills/drivers-and-app-handlers/SKILL.md",
- "skills/bl-info-migration/SKILL.md"
+ "skills/bl-info-migration/SKILL.md",
+ "skills/vse-python/SKILL.md"
],
"rules": [
"rules/prefer-data-over-ops-in-loops.mdc",
diff --git a/AGENTS.md b/AGENTS.md
index 4d98124..1ac7f8f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -20,7 +20,7 @@ a `.cursor-plugin/plugin.json` manifest so the ecosystem drift checker
classifies it as a `cursor-plugin`. This is content the AI loads when the user
asks Blender questions or works on Blender add-ons in Cursor or Claude Code.
-The content base is 12 skills, 6 rules, 2 templates, 17 snippets, and 45
+The content base is 13 skills, 6 rules, 2 templates, 17 snippets, and 45
examples (counts are CI-enforced against README.md and the manifest). The full
inventory tables and per-item purposes live in `CLAUDE.md`. Example anatomy
and authoring rules: copy `examples/bmesh-gear/`; the render look is specified
@@ -31,7 +31,7 @@ in `docs/VISUAL-STYLE.md`; the canonical run prompt is
```
Blender-Developer-Tools/
- skills//SKILL.md # 12 skill files
+ skills//SKILL.md # 13 skill files
rules/.mdc # 6 rule files
templates// # 2 starter templates
snippets/.py # 17 standalone Python snippets
diff --git a/CLAUDE.md b/CLAUDE.md
index 1291163..01463cd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,7 +17,7 @@ The **Blender Developer Tools** repository is at **v0.47.0**. It packages skills
## Repository Architecture
```
-skills//SKILL.md - AI workflow definitions, 12 total
+skills//SKILL.md - AI workflow definitions, 13 total
rules/.mdc - Anti-pattern rules, 6 total
templates// - Starter projects, 2 total
snippets/.py - Standalone code patterns, 17 total
@@ -28,7 +28,7 @@ docs/gallery/ - Committed generated gallery pages + hero render
VERSION - Source of truth for the repo version
```
-## Skills (12)
+## Skills (13)
| Skill | Purpose |
| --- | --- |
@@ -44,6 +44,7 @@ VERSION - Source of truth for the repo version
| depsgraph-and-evaluated-data | `evaluated_get` / `to_mesh` / `to_mesh_clear` lifetime contract for exporters and measurement |
| drivers-and-app-handlers | Driver expressions, `driver_namespace`, application handlers including the new 5.1 `exit_pre` |
| bl-info-migration | Three-step migration from legacy `bl_info` to Extensions Platform, dual-format pattern |
+| vse-python | VSE timeline from Python: `.strips` vs `.sequences`, `new_effect` kwargs, 5.2 COLOR `width`/`height` bake |
## Rules (6)
@@ -149,7 +150,7 @@ The AI consumes content via:
## Key Conventions
-- **Blender versions**: 5.2 LTS primary, 5.1 prior stable, 4.5 LTS fallback. Skills must show both code paths when 4.x and 5.x APIs diverge, and the 5.1-vs-5.2 NodesModifier input split.
+- **Blender versions**: 5.2 LTS primary, 5.1 prior stable, 4.5 LTS fallback. Skills must show both code paths when 4.x and 5.x APIs diverge, including the 5.1-vs-5.2 NodesModifier input split and the 5.2 COLOR strip `width`/`height` bake.
- **Properties as annotations**: `my_prop: bpy.props.FloatProperty(...)` (correct), not `my_prop = bpy.props.FloatProperty(...)` (deprecated).
- **bmesh memory**: every `bmesh.new()` must be paired with `bm.free()` in a `try`/`finally`.
- **No `bpy.ops` in tight loops**: use `bpy.data.*` and `bmesh` for bulk work.
diff --git a/README.md b/README.md
index d9c10fc..129d1f6 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
- 12 skills • 6 rules • 2 templates • 17 snippets • 45 examples
+ 13 skills • 6 rules • 2 templates • 17 snippets • 45 examples
@@ -36,13 +36,13 @@
## Overview
-This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 45 runnable examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.
+This repository ships **13 skills, 6 rules, 2 templates, 17 snippets, and 45 runnable examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.
The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly.
| Layer | Role |
| --- | --- |
-| **Skills** | Guided workflows: scaffolding, operators, panels, properties, mesh and bmesh, headless batch, slotted actions, geometry nodes, procedural materials, depsgraph queries, drivers and handlers, `bl_info` migration |
+| **Skills** | Guided workflows: scaffolding, operators, panels, properties, mesh and bmesh, headless batch, slotted actions, geometry nodes, procedural materials, depsgraph queries, drivers and handlers, `bl_info` migration, video sequencer |
| **Rules** | Guardrails for the most common AI mistakes: ops-in-loops, bmesh leaks, legacy `bl_info` only, prop assignments, deprecated context-copy override, per-element loops over bulk mesh data |
| **Templates** | A working Extensions Platform add-on starter and a headless batch script starter |
| **Snippets** | 17 small standalone Python files demonstrating canonical patterns |
@@ -887,7 +887,7 @@ Companion to [`color-attribute-wheel`](examples/color-attribute-wheel/) and
## How content is organized
```
-skills//SKILL.md - 12 skill files, YAML frontmatter, one canonical pattern each
+skills//SKILL.md - 13 skill files, YAML frontmatter, one canonical pattern each
rules/.mdc - 6 rule files, anti-pattern + correction
templates// - 2 template directories (extension-addon-template, headless-batch-script-template)
snippets/.py - 17 standalone Python snippets, 5 to 50 lines each
diff --git a/ROADMAP.md b/ROADMAP.md
index 0a9a4ce..8e02f8b 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -16,6 +16,7 @@ derives the actual version from conventional-commit types.
| Examples and demos (smoke-gated) | 12 | 6 | 2 | 17 | Shipped (v0.3.0) |
| More examples (turntable, SDF remesh) | 12 | 6 | 2 | 17 | Shipped (v0.4.0) |
| 5.2 LTS targeting, GN modifier inputs | 12 | 6 | 2 | 17 | Shipped |
+| VSE COLOR strip intrinsic size (undocumented 5.2) | 13 | 6 | 2 | 17 | Shipped |
| Modal operators, USD, mathutils | — | — | — | — | Upcoming |
| Stable | — | — | — | — | Upcoming |
@@ -100,7 +101,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
- ~~Image save-format witness~~ **SHIPPED** as `examples/png-exr-alpha/` — float→PNG is RGBA16 and false-unpremultiplies as if associated-alpha (closed-form err 0.98 at RGB 0.02 / a=1/255); OpenEXR preserves float RGBA; byte→PNG is straight RGBA8; `EXR color_mode='RGB'` drops alpha
- ~~Attribute domain witness~~ **SHIPPED** as `examples/attribute-domain-shear/` — POINT vs CORNER semantics on an 8-wedge pinwheel with one shared hub: CORNER authoring exact (hub corners disagree by face, err ≤ 1e-6), naive per-wedge POINT loop shears to the last write at hub + ring verts, measured deviation == palette closed form (0.751031); byte-identical on 4.5.11 and 5.1.2; crisp-vs-smeared dual pinwheel still — companion to `color-attribute-wheel`
- ~~Light-linking collection witness~~ **SHIPPED** as `examples/light-link-studio/` — the API is `obj.light_linking` on the light OBJECT (`ld.light_linking` is an AttributeError on both versions); linked ratio 4.0x at projected-center luminance samples, unlink raises the decoy 244–251% with 0.0% hero drift in the same two-render check; EEVEE Next honors linking too (5.5x on 4.5.11, 5.9x on 5.1.2), Cycles pinned for deterministic samples
-- ~~VSE sequences-to-strips witness~~ **SHIPPED** as `examples/vse-cut-list/` — `.sequences` removed on 5.x (4.5 bridges to `.strips`), `new_effect` end kwarg `frame_end=` (4.5) vs `length=` (5.x), `frame_final_*` deprecated in favor of `left_handle`/`right_handle`/`duration`, TRANSFORM effect type removed, GAMMA_CROSS clamps to the source overlap, effect strips consume inputs only when stacked above them, same-scene scene strips render transparent; save/reload round-trip + tiny-render pixel witness
+- ~~VSE sequences-to-strips witness~~ **SHIPPED** as `examples/vse-cut-list/` — `.sequences` removed on 5.x (4.5 bridges to `.strips`), `new_effect` end kwarg `frame_end=` (4.5) vs `length=` (5.x), `frame_final_*` deprecated in favor of `left_handle`/`right_handle`/`duration`, TRANSFORM effect type removed, GAMMA_CROSS clamps to the source overlap, effect strips consume inputs only when stacked above them, same-scene scene strips render transparent; save/reload round-trip + tiny-render pixel witness. **5.2 COLOR `width`/`height` bake** (not in the python_api notes) taught in `skills/vse-python/`; the example `--check-pixels` path is the witness (set scene resolution before `new_effect`)
- ~~glTF export round-trip witness~~ **SHIPPED** as `examples/gltf-export-roundtrip/` — `export_yup` bakes `(x,y,z)→(x,z,−y)` into vertex data with no node rotation (probed identical on 4.5.11 and 5.1.2), `export_apply` ships the evaluated mesh (one disk vertex per evaluated loop), TEXCOORD_0 is V-flipped on disk, per-triangle material bindings survive; exporter/importer RNA signatures byte-identical between 4.5.11 and 5.1.2 (guarded against future renames); `Mesh.calc_normals()` removal on 5.x surfaced during authoring
- ~~LOD decimate chain witness~~ **SHIPPED** as `examples/lod-decimate-chain/` — Decimate COLLAPSE evaluated through the depsgraph is non-destructive (obj.data keeps closed-form counts), evaluated tris hit `ratio × base` within 5% (measured 0.0–0.44%), silhouette bbox survives within 1e-3 (measured 7.7e-6); a stacked Decimate halves the effective ratio, an aggressive 0.02 ratio collapses the nose tip — both caught failure modes
- ~~Vertex weight limit witness~~ **SHIPPED** as `examples/vertex-weight-limit/` — the 4-influence engine cap enforced via the data API (`v.groups` + `VertexGroup.remove` + renormalize); unit sums (measured 3e-8), pose preserved (4.9e-3), exact LBS from the mesh's own deform layer (2.7e-7), Root mount pinned
diff --git a/docs/technical-audit.md b/docs/technical-audit.md
index 46b6489..d181e27 100644
--- a/docs/technical-audit.md
+++ b/docs/technical-audit.md
@@ -2,6 +2,47 @@
_Audit date: 2026-06-20 · Repo @ v0.5.0 · Auditor: Principal Eng review_
+## Findings the release notes did not list
+
+_Standing log. Notes-derived 5.1→5.2 (and later) deltas are incomplete by default._
+
+The Blender `python_api` release notes are a starting set, not a closed
+inventory. A sweep that only copies listed removals will miss behavior
+changes that keep the same RNA names. Record those here when CI or
+authoring finds them, even when upstream notes omit the break. Do not
+treat a clean notes-diff as proof that two series are interchangeable.
+
+### 2026-09-09 — COLOR strip intrinsic size (5.2, not in the notes)
+
+**4.5 LTS / 5.1:** COLOR strips have no intrinsic size. `transform.scale_*`
+is a fraction of the output frame. Creating at factory resolution then
+rendering a tiny buffer is fine.
+
+**5.2+:** `new_effect(..., type='COLOR')` bakes readonly `width` / `height`
+from `scene.render.resolution_*` at creation. Scale and offset are in that
+media space. Factory 1920×1080 + later 96×54 render → 0.36-scaled cells
+larger than the output; the long-runner covers the frame.
+
+A lone COLOR strip **does** honor `transform.scale_*` on 5.2. The break is
+media size vs a later output size, not "transform is ignored."
+
+Set scene resolution **before** building strips.
+
+- Taught: `skills/vse-python/SKILL.md`
+- Witness: `examples/vse-cut-list/` `--check-pixels` (on 5.2 asserts
+ `A.width, A.height == (96, 54)`). No separate example — that script
+ already fails if the bake drifts.
+
+### Contrast — NodesModifier dict assignment (5.2, was in the notes)
+
+`mod[identifier] = value` raises `TypeError` on 5.2 (`id properties not
+supported for this type`). Replacement: `getattr(mod.properties.inputs,
+ident).value`. This one **was** listed in the 5.2 python_api notes.
+Taught in `skills/geometry-nodes-python/SKILL.md`, witnessed by
+`examples/gn-modifier-inputs/`. Keep both classes of finding in this
+log: notes-listed removals that still need a taught split, and
+behavior changes the notes never mentioned.
+
## 0. Important reframing
The audit brief assumed a runnable add-on (operators, UI panels, a socket/file-watcher
diff --git a/skills/vse-python/SKILL.md b/skills/vse-python/SKILL.md
new file mode 100644
index 0000000..850a3d9
--- /dev/null
+++ b/skills/vse-python/SKILL.md
@@ -0,0 +1,157 @@
+---
+name: vse-python
+description: Build Video Sequence Editor timelines from Python. SequenceEditor.strips vs .sequences, new_effect length vs frame_end, and the 5.2 COLOR strip width/height bake from scene resolution.
+standards-version: 1.10.0
+---
+
+# Video Sequence Editor in Python
+
+## Trigger
+
+Use this skill when the user:
+
+- Builds or inspects a VSE / sequencer timeline from a script
+- Mentions `sequence_editor`, `strips`, `sequences`, `new_effect`, COLOR strips, or `StripTransform`
+- Hits `AttributeError: 'SequenceEditor' object has no attribute 'sequences'` on 5.x
+- Hits `TypeError` on `new_effect` kwargs (`frame_end` vs `length`)
+- Renders a sequencer composite whose COLOR cells ignore `transform.scale_*` after a resolution change
+
+## High-level shape
+
+```python
+import bpy
+
+scene = bpy.context.scene
+se = scene.sequence_editor or scene.sequence_editor_create()
+coll = se.strips if hasattr(se, "strips") else se.sequences
+
+if bpy.app.version >= (5, 0, 0):
+ strip = coll.new_effect(
+ name="A", type="COLOR", channel=1,
+ frame_start=1, length=10,
+ )
+else:
+ strip = coll.new_effect(
+ name="A", type="COLOR", channel=1,
+ frame_start=1, frame_end=11,
+ )
+strip.color = (0.85, 0.10, 0.22)
+```
+
+Branch on `bpy.app.version`, never on `bpy.app.version_string`. An empty `bpy_prop_collection` is falsy — `se.strips or se.sequences` silently falls through to the legacy accessor on an empty timeline. Always branch on `hasattr`.
+
+## Accessor: `.strips` vs `.sequences`
+
+| | 4.5 LTS | 5.1 / 5.2+ |
+| --- | --- | --- |
+| Canonical collection | `.strips` (`.sequences` is a bridge to the same strips) | `.strips` only |
+| `.sequences` | Present, same contents as `.strips` | `AttributeError` |
+
+```python
+def strips_coll(se):
+ return se.strips if hasattr(se, "strips") else se.sequences
+```
+
+Do not write `se.sequences` in new code. On 5.x it is gone; on 4.5 `.strips` already exists.
+
+## `new_effect` end kwarg
+
+`strips.new_effect(...)` ends a strip with **one** accepted end argument. The other raises `TypeError`.
+
+```python
+span = (1, 33) # end-exclusive [start, end)
+
+if bpy.app.version >= (5, 0, 0):
+ strip = coll.new_effect(
+ name="A", type="COLOR", channel=1,
+ frame_start=span[0], length=span[1] - span[0],
+ )
+else:
+ strip = coll.new_effect(
+ name="A", type="COLOR", channel=1,
+ frame_start=span[0], frame_end=span[1],
+ )
+```
+
+Timeline bounds: `frame_final_start` / `frame_final_end` / `frame_final_duration` on 4.5. On 5.x those names are deprecated aliases (removal announced for 6.0); canonical accessors are `left_handle` / `right_handle` / `duration`. Scene strips take four args — `new_scene(name, scene, channel, frame_start)` — with no `length` / `frame_end` kwarg on either version.
+
+The `TRANSFORM` effect type is removed on 5.x. Place strips with per-strip `strip.transform` (`StripTransform`) on both versions. Effect inputs are `input_1` / `input_2` (creation kwargs `input1` / `input2`).
+
+## COLOR strip intrinsic size (5.2)
+
+4.5 LTS and 5.1 COLOR strips have **no** intrinsic size. `transform.scale_*` is a fraction of the **output frame**. Creating at factory resolution then rendering a tiny buffer is fine.
+
+5.2 and later bake readonly `width` / `height` from **scene render resolution at `new_effect` time**. Scale and offset are then in that **media** space, not a later output size. Set scene resolution **before** building strips.
+
+This split is **not** in the 5.2 `python_api` release notes. Witnessed by `examples/vse-cut-list/` `--check-pixels`.
+
+```python
+width, height = 96, 54
+scene.render.resolution_x = width
+scene.render.resolution_y = height
+
+se = scene.sequence_editor or scene.sequence_editor_create()
+coll = se.strips if hasattr(se, "strips") else se.sequences
+
+if bpy.app.version >= (5, 0, 0):
+ strip = coll.new_effect(
+ name="A", type="COLOR", channel=1,
+ frame_start=1, length=10,
+ )
+else:
+ strip = coll.new_effect(
+ name="A", type="COLOR", channel=1,
+ frame_start=1, frame_end=11,
+ )
+
+if bpy.app.version >= (5, 2, 0):
+ # 5.2+: readonly width/height baked at new_effect from scene.render
+ assert (strip.width, strip.height) == (width, height)
+ # strip.width = 64 # TypeError — readonly
+else:
+ # 4.5 / 5.1: no intrinsic size; hasattr(strip, "width") is False
+ pass
+
+strip.transform.scale_x = 0.36
+strip.transform.scale_y = 0.36
+```
+
+Creating at factory 1920×1080 then dropping the scene to 96×54 for a check render makes a 0.36-scaled COLOR cell larger than the output. On 5.2 the long-runner covers the frame; pixel samples read the wrong strip.
+
+A lone COLOR strip **does** honor `transform.scale_*` on 5.2. The break is media size vs a later output size, not "transform is ignored."
+
+## Common AI mistakes
+
+1. **`sequence_editor.sequences` on 5.x.** `AttributeError`. Use `.strips` (with `hasattr` only as a 4.5 bridge).
+
+2. **Wrong `new_effect` end kwarg.** `frame_end=` on 5.x and `length=` on 4.5 both raise `TypeError`.
+
+3. **Creating COLOR strips, then changing render size** (the 5.2 trap):
+
+ ```python
+ a = coll.new_effect(...) # bakes 1920×1080 on 5.2
+ scene.render.resolution_x = 96 # output changed; media size did not
+ ```
+
+ Set `scene.render.resolution_*` **before** `new_effect`.
+
+4. **`se.strips or se.sequences`.** An empty collection is falsy, so this falls through to `.sequences` on a fresh editor.
+
+5. **`TRANSFORM` effect type on 5.x.** Gone. Use `strip.transform`.
+
+6. **Scene strip pointing at its own scene.** Feedback loop; renders transparent. Source a **separate** scene.
+
+7. **Effect strips consume their inputs** only when stacked on a channel **above** those inputs. Below, the inputs keep compositing independently. A `GAMMA_CROSS` asked to outlast its inputs' overlap is silently clamped to the overlap.
+
+## See also
+
+- Example `examples/vse-cut-list/` — accessor rename, `new_effect` kwargs, save/reload, and the 5.2 COLOR size bake (`--check-pixels` asserts `A.width, A.height == render size` on 5.2+).
+- Example `examples/vse-gamma-cross/` — `GAMMA_CROSS` blend curve.
+- Audit log: `docs/technical-audit.md` § Findings the release notes did not list.
+
+## References
+
+- `bpy.types.SequenceEditor`: https://docs.blender.org/api/current/bpy.types.SequenceEditor.html
+- `bpy.types.Sequence`: https://docs.blender.org/api/current/bpy.types.Sequence.html
+- 4.5 LTS `SequenceEditor`: https://docs.blender.org/api/4.5/bpy.types.SequenceEditor.html
+- 5.1 `SequenceEditor`: https://docs.blender.org/api/5.1/bpy.types.SequenceEditor.html