CLAUDE.md · diff

git:20260926.26f5f4b to git:20260928.fccf2ce

20 added, 7 removed. Audit A to A.

<!-- standards-version: 1.10.0 -->
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
**Division of labor:** this file carries repo-specific operational facts an agent needs at runtime — content inventory, Blender runtime discovery, git staging hazards, and the example-shipping quality gates. `AGENTS.md` carries fleet-standard governance and workflow rules — branching, commits, merge and CI-evidence policy, release automation, authoring standards. Read both; neither repeats the other.
## Project Overview
- The **Blender Developer Tools** repository is at **v0.85.24**. It packages skills, rules, snippets, starter templates, and runnable smoke-gated examples for Blender Python development with Cursor and Claude Code. Coverage targets **Blender 5.2 LTS** (current stable) with **Blender 4.5 LTS** fallback. **Blender 5.1** is prior stable. There is no MCP server; content is consumed directly by the AI when working in Blender add-on or scripting projects.
+ The **Blender Developer Tools** repository is at **v0.106.0**. It packages skills, rules, snippets, starter templates, and runnable smoke-gated examples for Blender Python development with Cursor and Claude Code. Coverage targets **Blender 5.2 LTS** (current stable) with **Blender 4.5 LTS** fallback. **Blender 5.1** is prior stable. There is no MCP server; content is consumed directly by the AI when working in Blender add-on or scripting projects.
- **Version:** 0.85.24
+ **Version:** 0.106.0
**License:** CC-BY-NC-ND-4.0
**Author:** TM Hospitality Strategies
## Repository Architecture
```
skills/<skill-name>/SKILL.md - AI workflow definitions, 16 total
rules/<rule-name>.mdc - Anti-pattern rules, 9 total
templates/<template-name>/ - Starter projects, 3 total
snippets/<snippet-name>.py - Standalone code patterns, 27 total
- examples/<name>/ - Runnable smoke-gated examples, 59 total (+ gallery.json)
- showcase/<name>/ - Budget-conformance props, 34 pieces (sibling of examples/; see showcase/README.md)
+ examples/<name>/ - Runnable smoke-gated examples, 60 total (+ gallery.json)
+ showcase/<name>/ - Budget-conformance props, 50 pieces, each with a gallery `category` (sibling of examples/; see showcase/README.md § Categories)
scripts/build_gallery.py - Regenerates docs/gallery/ from examples/gallery.json + showcase/gallery.json
- scripts/site/ - Vendored landing-page build (Jinja2)
+ scripts/site/ - Vendored landing-page build (Jinja2); tokens.css is the shared palette
+ tests/check_site_links.py - Internal link/anchor/alt gate over the built site (docs/)
docs/gallery/ - Committed generated gallery pages + hero renders
VERSION - Source of truth for the repo version
```
## Skills (16)
| Skill | Purpose |
| --- | --- |
| addon-scaffolding | Extensions Platform manifest, file layout, register/unregister symmetry |
| ai-mesh-cleanup | Ordered cleanup for imported generated meshes: units, transform apply, origin, normals, budget, collider |
| bake-high-to-low | Cycles cage-bake of high-poly detail onto a low-poly target as a tangent-space normal map |
| engine-export-presets | Unity Y-up, Godot Z-up, and Unreal centimeter glTF/FBX presets; glTF uses export_yup, FBX uses axis_forward/axis_up |
| operators | `bpy.types.Operator` lifecycle, `bl_idname`, redo, defensive context handling |
| ui-panels | `bpy.types.Panel` declarative `draw()`, layout primitives, conditional UI |
| custom-properties | `bpy.props` annotations, PropertyGroup, PointerProperty, storage tradeoffs |
| mesh-editing-and-bmesh | When to use bpy.data vs bpy.ops vs bmesh, foreach_set, depsgraph eval |
| headless-batch-scripting | `blender --background --python`, temp_override, argparse after `--` |
| slotted-actions-animation | Blender 5.x Slotted Actions, channelbag, 4.5 LTS fallback bridge |
| geometry-nodes-python | Programmatic GN tree construction, interface sockets, NODES modifier |
| procedural-materials-and-shaders | Node tree construction for Principled BSDF, emissive, node groups, EEVEE Next vs Cycles |
| 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 (9)
Every rule sets `alwaysApply: false`, so the Scope column below is the rule's
actual load condition, not a description: a rule enters context only when a file
matching its `globs` does. Changing a glob changes when the rule fires.
| Rule | Scope | What it flags |
| --- | --- | --- |
| prefer-data-over-ops-in-loops | `*.py` | `bpy.ops.*` calls inside iteration over many objects |
| always-free-bmesh | `*.py` | `bmesh.new()` without paired `bm.free()` in a `try`/`finally` block |
| target-extensions-platform-format | Add-on roots | Legacy `bl_info` only add-ons missing `blender_manifest.toml` |
| type-annotate-props-and-defend-context | `*.py` | `bpy.props` defined as assignments, unguarded `context.active_object` |
| prefer-temp-override-over-context-copy | `*.py` | `bpy.context.copy()` passed to operators (deprecated 4.x, removed 5.x) |
| use-foreach-set-for-bulk-data | `*.py` | Python loops over `mesh.vertices` setting bulk attributes one at a time |
| validate-imported-mesh-scale | `*.py` | glTF/FBX import then mesh work with no `transform_apply` and no unit-scale check |
| no-unapplied-modifiers-on-export | `*.py` | Export with live modifiers when the export does not request evaluated geometry |
| use-correct-axis-rna-per-exporter | `*.py` | `export_scene.gltf` with FBX `axis_forward`/`axis_up`, or `export_scene.fbx` with glTF `export_yup` |
## Templates (3)
`templates/extension-addon-template/` is a copy-paste-ready Blender extension demonstrating:
- `blender_manifest.toml` with `blender_version_min = "4.5.0"`
- `register_classes_factory` registration pattern
- One Operator, one Panel, one PropertyGroup
- A `PointerProperty` bound to `bpy.types.Scene`
- Symmetric `register()` / `unregister()` with property cleanup before class unregister
`templates/headless-batch-script-template/` is a working starter for unattended Blender batch jobs:
- `argparse` parsing of args after the `--` separator
- Iteration over every mesh object via `bpy.data.objects`
- Modifier application via `bpy.context.temp_override` (not the deprecated context-dict form)
- glTF export via `bpy.ops.export_scene.gltf`
- Explicit exit codes for CI integration
`templates/ai-asset-pipeline-template/` is a working starter for a headless GLB-in / engine-ready-out job:
- `argparse` parsing of args after the `--` separator
- Import, unit-scale check, transform apply, origin, normals, evaluated tris
- LOD chain and optional convex/box collider
- Unity / Godot / Unreal glTF export via the engine-export-presets contract
- Explicit exit codes matching `headless-batch-script-template` (0, then 2+)
## Snippets (27)
Small standalone `.py` files at `snippets/<name>.py`, each 5 to 50 lines.
v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh load-edit-free, temp_override context, foreach_set vertex bulk write, register_classes_factory, PointerProperty binding, cross-version property delete, and the `action_ensure_channelbag_for_slot` slotted-actions bridge.
v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namespace`, application handler registration, shader node group with cross-version `interface` API, `foreach_get` bulk vertex read, version-branch skeleton, and USD export with `evaluation_mode='RENDER'`.
AI asset pipeline track: `decimate_to_budget.py`, `convex_hull_collider.py`, `lod_chain.py` (helper duplicated, not imported), `gltf_draco_export.py`, `export_preset_unity.py`, `export_preset_godot.py`, `export_preset_unreal.py`, `setup_bake_target_image.py`, `bake_normal_high_to_low.py`, `save_baked_image.py`.
- ## Examples (59)
+ ## Examples (60)
Runnable scripts at `examples/<name>/`, each asserting a real API contract with
deterministic checks (exit non-zero on failure) and optionally rendering a still via
`--output`. All of them run headless on Blender 5.2 LTS and 4.5 LTS in `blender-smoke.yml` (5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch);
- **51 of the 59 ship a render in the site gallery** at `docs/gallery/`. The other
+ **52 of the 60 ship a render in the site gallery** at `docs/gallery/`. The other
eight are **check-only**: they carry no `--output` path, no gallery entry, and no
hero asset. The criterion is whether the contract is expressible in pixels. An
example is check-only when its witness is a data or state fact that no scene
redesign can make visible — a datablock name, an RNA attribute's presence, a
post-exit sidecar, a topology count, export metadata — so a render would look
identical whether the API held or broke. That is a category, not a shortcut: a
subject whose render *could* be made legible must be rendered, per
`docs/new-example-prompt.md`. Smoke coverage is unaffected; every check-only
example still carries a `tests/smoke/catalog.json` row and a falsifier.
`examples/gallery.json` is the
gallery's source of truth. When authoring a new gallery example, copy the anatomy of
`examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and
wire all of: gallery.json entry (with an `alt` sentence describing the still; the build fails without one), `.cursor-plugin/plugin.json` examples array (CI-gated),
a `tests/smoke/catalog.json` row, a README gallery row, hero webp (1280×720) in
`docs/gallery/assets/` + preview webp (1200×675), then run `python scripts/build_gallery.py`.
A check-only example wires only the `.cursor-plugin/plugin.json` entry, the
`tests/smoke/catalog.json` row, and its README — it is deliberately absent from
`examples/gallery.json` and `docs/gallery/`.
Renders must conform to the gallery look spec at `docs/VISUAL-STYLE.md`.
Render paths gate framing through the shared helper `examples/gallery_framing.py` —
imported via a `__file__`-relative `sys.path` shim, the repo's only
cross-example import mechanism (see its docstring).
## Blender Runtime Discovery
- Local Blender binaries: check `.scratch/` at the repo root **first** — some machines have no system Blender install, and a prior agent run downloads official releases there (e.g. `.scratch/5.2/blender-5.2.x-.../blender[.exe]`). Then check system installs. Do not probe blindly; locate the binary, run it, and state the **exact binary path and the version the binary itself reports** in every report.
- **5.2 LTS is the local check version. 4.5 LTS is exercised by CI when unavailable locally.** 4.4 is not a substitute for 4.5 and must never be reported as 4.5.
- **CI floats within each series:** `blender-smoke.yml` resolves the highest published point release at run time (`sort -V | tail -1` on the download listing), so a local 4.5.x may lag CI — its 4.5 job ran 4.5.12 LTS on PR #107 while local `.scratch/` held 4.5.11. Either way, state the exact version the binary reports.
- If `.scratch/` lacks a needed version, download an official release from download.blender.org into it. `.scratch` is gitignored.
- In scripts, version-branch on the `bpy.app.version` tuple, never on `bpy.app.version_string` — it reads e.g. `"4.5.11 LTS"`, not bare semver.
## Live MCP vs Headless Harness
Two execution paths exist for Blender work. The headless harness (`.scratch/` or system binaries, check-only scripts, CI smoke) is the **only source of truth for correctness** — every witness, check, and CI gate runs headless. The official Blender Lab MCP server ([lab/blender_mcp](https://projects.blender.org/lab/blender_mcp)), when registered on the local machine, provides a live interactive session for exploration.
- **Use the live MCP for:** interactively inspecting a scene while designing a render, querying data relations, diagnosing why staged geometry or lighting looks wrong, and prototyping API calls before committing them to a script.
- **Never use the live MCP for:** anything a check depends on, anything reported as verification evidence, or anything CI must reproduce. A behavior observed in the live session must be re-proven in the headless script before it counts. Examples must never import from, depend on, or assume the MCP server.
- **Safety:** the server executes generated code in the running Blender instance without guards. Only attach it to scratch scenes, never to files you cannot afford to mutate.
- **Availability is machine-local and optional:** if the server is not registered, proceed headless without comment.
## Git Staging
Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor agent sessions inject a local guidance block into the working-copy `CLAUDE.md`; a bulk add sweeps it into the commit. It manifests as a `CLAUDE.md` hunk in `git diff` that you did not author (the file can be dirty before you touch anything). Leave it unstaged, and never stage `CLAUDE.md` unless you deliberately edited it.
## Quality Gates for Example Runs
- `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects.
- **Framing gate:** Layer 1 framing is measured, not eyeballed — the example's `--output` render path calls the shared helper `examples/gallery_framing.py` (`check_framing`, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. The helper measures; call sites enforce. Bleed compositions call `measure_framing_deviation` and assert their own cap at the call site — `check_framing` has no `deviation=` flag.
- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. Tooling: `python scripts/render_hero.py --blender PATH --only NAME` renders the README `--output` command and writes the hero and preview webps (the one committed PNG-to-webp path); `python scripts/contact_sheet.py NAME` builds the composite in the committed format and prints each tile's luma, stage, wedge warmth and saturation; `python scripts/measure_hero_drift.py` checks committed heroes against what the code renders now.
- **Asset-sheet gate (asset-type examples — game props/kits):** composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently `collision-hull-proxy`, `socket-attach-points`, `vertex-color-ao`, `wheelbarrow`, `apothecary-shelf` — rendered the same way; commit under `docs/gallery/asset-sheets/`, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. **This list is the canonical home of the reference set** — update it here when a new asset outclasses a member. A reference may be an example or a showcase piece. Tooling: `python scripts/asset_sheet.py --blender PATH NAME` renders the candidate and this set (read from this line) through `scripts/asset_sheet_panel.py` and writes the composite. The measurable floors behind the gate (naming, material variation, edge treatment) live in `examples/gallery_asset_quality.py` — render path only, same call pattern as `gallery_framing`, exit 11 on violation — with the calibration table and dropped-floor evidence in `docs/VISUAL-STYLE.md` § Asset quality.
- **Falsification:** every check must be proven to fail once — break the contract, observe the non-zero exit, restore — with the probe and the measured error reported in the PR body. An assertion that cannot fail witnesses nothing.
- **After gallery regeneration** (`python scripts/build_gallery.py`), read the **generated HTML** character by character — the `<img alt>` text and witnesses callouts in `docs/gallery/index.html` and `docs/gallery/<name>/index.html` — not just `examples/gallery.json`. Precedent: the `teaches.split(".")[0]` bug truncated 14/21 card alts at dotted API paths like `bmesh.ops` while the source JSON looked fine (fixed in PR #68).
- **Playwright gallery captures:** gallery `<img>` tags lazy-load, so force them first (`document.querySelectorAll('img').forEach(i => i.loading = 'eager')`, then wait). **Scroll the target card into view and take a viewport capture** — `scrollIntoView({block:'center', behavior:'instant'})`, short wait, `browser_take_screenshot` with `fullPage` omitted. A `fullPage` capture is NOT a workaround: on a tall gallery page it renders every card image blank even when the images are verified loaded (`complete === true`, `naturalWidth === 1280`, `opacity === 1`) — measured on the 45-card grid at 1425x4516. Verify load state via `browser_evaluate` rather than trusting the pixels.
## Smoke skip and post-exit
The host runner is `tests/smoke/run_example.py`. Shipped examples are listed in
`tests/smoke/catalog.json` (not a new YAML step per example).
- **SKIP:** print `SMOKE_SKIP: <reason>` and `sys.exit(77)`. Legal only when the
catalog/runner `--min-version` is **above** this Blender. Exit 0 with that
marker is FAIL (vacuous). Skip on a version that should run is FAIL.
- **Post-exit sidecar:** opt-in `--expect-sidecar PATH`. The example writes
`$BDT_SMOKE_SIDECAR` (set by the runner). The harness asserts after Blender
exits. Not a gallery still.
- **Summary:** `tests/smoke/summarize.py` prints passed/skipped/failed. Zero
PASSes makes the job red even if every example skipped cleanly.
## Example-Run Process
- The canonical example-creation prompt lives at `docs/new-example-prompt.md`; keep it in agreement with this file and `AGENTS.md`.
- The `ROADMAP.md` "Candidate pool" section is the subject source for example runs: remove a subject when it ships, and restock with subjects identified but not built.
- PR bodies label what was proven by live run versus established by inspection only (see `AGENTS.md` § Merge policy and CI evidence for the merge and post-merge verification rules).
## Development Workflow
This is a content repository, no build step for skills/rules/snippets/templates — edit
`SKILL.md`, `.mdc`, `.py`, and `.toml` files directly. The website is generated:
`scripts/build_gallery.py` (stdlib) regenerates `docs/gallery/` and must be re-run after
touching `examples/`; the landing page builds from `scripts/site/` at deploy time.
+
+ - **One palette:** `scripts/site/tokens.css` is the single `:root` for every published
+ page — the landing and 404 templates `{% include %}` it and `build_gallery.py` inlines
+ it. Change colors or type there, never in a hand-copied `:root`; a token edit changes
+ all 89 gallery pages, so regenerate and commit `docs/gallery/`.
+ - **Link gate:** after building both (`python scripts/site/build_site.py --repo-root . --out docs`
+ then `python scripts/build_gallery.py`), run `python tests/check_site_links.py`. It resolves
+ every internal `href`/`src`/`#anchor` across all pages and requires `alt` on every `<img>`;
+ the `validate-site` job and the deploy both run it. Gallery hash fragments with `=`
+ (`#tag=a,b`, `#k=showcase`, `#s=az`) are filter state, not anchors, and are skipped.
+ - **"What's new" needs history:** the landing page dates recent additions from `git log`;
+ a shallow clone hides the strip, which is why `pages.yml` checks out with `fetch-depth: 0`.
The AI consumes content via:
- **Cursor**: rules under `rules/` apply when scope globs match. Skills are referenced by name in chat.
- **Claude Code**: copy `skills/` and `rules/` into the project workspace, or use this repo as a checkout that Claude Code references directly.
## 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, 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.
- **Extensions over `bl_info`**: new add-ons ship as Extensions with `blender_manifest.toml`. `bl_info` may appear alongside as a fallback only.
- **Defensive context**: any code touching `context.active_object` must guard with `if obj is None: return`.
## Blender Documentation Quick Reference
| Area | URL |
| --- | --- |
| Python API (5.2 LTS) | https://docs.blender.org/api/current/ |
| Python API (5.1) | https://docs.blender.org/api/5.1/ |
| Python API (4.5 LTS) | https://docs.blender.org/api/4.5/ |
| Extensions Platform | https://docs.blender.org/manual/en/latest/advanced/extensions/index.html |
| Release notes | https://developer.blender.org/ |
## Release Hygiene
The release pipeline is automated via `release.yml` on push to `main` for content-changing paths. The `release-doc-sync@v1` step rewrites CHANGELOG.md, this CLAUDE.md `**Version:**` line, and ROADMAP.md `**Current:**` line on each release. Never hand-edit those lines, the action owns them.
When adding content to a future version:
1. Add files under `skills/`, `rules/`, `snippets/`, `templates/`, or `examples/`.
2. Update README.md aggregate counts (the `validate-counts` job enforces correctness).
3. Update ROADMAP.md candidate pool entries.
4. Use `feat:` for new content, `fix:` for corrections.
5. Push. The release pipeline handles VERSION, tags, CHANGELOG, the `**Version:**` line here, and the `**Current:**` line in ROADMAP.md.
# context-mode — MANDATORY routing rules
You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session.
## BLOCKED commands — do NOT attempt these
### curl / wget — BLOCKED
Any Bash command containing `curl` or `wget` is intercepted and replaced with an error message. Do NOT retry.
Instead use:
- `ctx_fetch_and_index(url, source)` to fetch and index web pages
- `ctx_execute(language: "javascript", code: "const r = await fetch(...)")` to run HTTP calls in sandbox
### Inline HTTP — BLOCKED
Any Bash command containing `fetch('http`, `requests.get(`, `requests.post(`, `http.get(`, or `http.request(` is intercepted and replaced with an error message. Do NOT retry with Bash.
Instead use:
- `ctx_execute(language, code)` to run HTTP calls in sandbox — only stdout enters context
### WebFetch — BLOCKED
WebFetch calls are denied entirely. The URL is extracted and you are told to use `ctx_fetch_and_index` instead.
Instead use:
- `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` to query the indexed content
## REDIRECTED tools — use sandbox equivalents
### Bash (>20 lines output)
Bash is ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`, and other short-output commands.
For everything else, use:
- `ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
- `ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context
### Read (for analysis)
If you are reading a file to **Edit** it → Read is correct (Edit needs content in context).
If you are reading to **analyze, explore, or summarize** → use `ctx_execute_file(path, language, code)` instead. Only your printed summary enters context. The raw file content stays in the sandbox.
### Grep (large results)
Grep results can flood context. Use `ctx_execute(language: "shell", code: "grep ...")` to run searches in sandbox. Only your printed summary enters context.
## Tool selection hierarchy
1. **GATHER**: `ctx_batch_execute(commands, queries)` — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls.
2. **FOLLOW-UP**: `ctx_search(queries: ["q1", "q2", ...])` — Query indexed content. Pass ALL questions as array in ONE call.
3. **PROCESSING**: `ctx_execute(language, code)` | `ctx_execute_file(path, language, code)` — Sandbox execution. Only stdout enters context.
4. **WEB**: `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` — Fetch, chunk, index, query. Raw HTML never enters context.
5. **INDEX**: `ctx_index(content, source)` — Store content in FTS5 knowledge base for later search.
## Subagent routing
When spawning subagents (Agent/Task tool), the routing block is automatically injected into their prompt. Bash-type subagents are upgraded to general-purpose so they have access to MCP tools. You do NOT need to manually instruct subagents about context-mode.
## Output constraints
- Keep responses under 500 words.
- Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description.
- When indexing content, use descriptive source labels so others can `ctx_search(source: "label")` later.
## ctx commands
| Command | Action |
|---------|--------|
| `ctx stats` | Call the `ctx_stats` MCP tool and display the full output verbatim |
| `ctx doctor` | Call the `ctx_doctor` MCP tool, run the returned shell command, display as checklist |
| `ctx upgrade` | Call the `ctx_upgrade` MCP tool, run the returned shell command, display as checklist |