diff --git a/.agents/skills/marimo-pair/SKILL.md b/.agents/skills/marimo-pair/SKILL.md
new file mode 100644
index 0000000..1d6e69f
--- /dev/null
+++ b/.agents/skills/marimo-pair/SKILL.md
@@ -0,0 +1,295 @@
+---
+name: marimo-pair
+description: >-
+ Drive a live marimo notebook as a workspace: run Python in the same kernel
+ the user does, inspect live notebook state, and commit durable notebook
+ changes. Use when the user wants to start a marimo notebook or pair on an
+ active marimo session.
+allowed-tools: Bash(bash **/scripts/discover-servers.sh *), Bash(bash **/scripts/execute-code.sh *), Read
+---
+
+marimo is a reactive Python runtime for building reproducible Python programs
+(marimo notebooks). Cells are connected by the variables they define and
+reference. Running a cell re-executes dependents in dataflow order. The active
+runtime holds the kernel namespace, cell state, and dataflow graph. The
+notebook (`.py` file) is the artifact the kernel writes from that state while a
+session is running.
+
+A user interacts with the same runtime via a notebook UI with cells, outputs,
+and widgets.
+
+**WARNING. The active runtime is the source of truth.** During a session, you
+SHOULD NOT modify the associated `.py` file directly. File edits WILL NOT reach
+the active kernel or user, and the kernel may overwrite them on save. Use
+`marimo._code_mode` (`cm`) for notebook changes. Reading disk is fine, but
+prefer `ctx.cells[...].code` for current cell code.
+
+## Connect to a Notebook
+
+Use the bundled script (`bash scripts/execute-code.sh`) or MCP
+(`execute_code(...)`) to run Python in a live marimo kernel.
+
+If the user provides a notebook URL, target it directly:
+
+```bash
+bash scripts/execute-code.sh --url http://localhost:2718 -c "print('connected')"
+```
+
+Use `-c` only for short one-liners. For multiline code or code containing
+quotes, backticks, `$`, or braces, use a single-quoted heredoc:
+
+```bash
+bash scripts/execute-code.sh --url http://localhost:2718 <<'PY'
+import marimo._code_mode as cm
+
+async with cm.get_context() as ctx:
+ cid = ctx.create_cell("x = df.head()")
+ ctx.run_cell(cid)
+PY
+```
+
+When code already lives in a file, pass the file path:
+
+```bash
+bash scripts/execute-code.sh --url http://localhost:2718 /tmp/code.py
+```
+
+If no target is provided, find or start a session. First look for a running
+session with `bash scripts/discover-servers.sh`, MCP `list_sessions()`, or
+local process context. When multiple sessions are possible, target with
+`--url`, `--port`, or `--session`.
+
+If no server is running and the user wants a notebook, start marimo with
+`--no-token` (and without `--headless`) so it auto-registers for discovery. The
+notebook UI must be open before there is an active session for `execute-code`
+to target. The right way to invoke marimo depends on context (project tooling,
+global install, sandbox mode). If the notebook file contains a PEP 723 `#
+/// script` header, it MUST be opened with `--sandbox` — otherwise marimo
+ignores the inline dependencies. See
+[finding-marimo.md](reference/finding-marimo.md) for the full decision tree and
+[execution-context.md](reference/execution-context.md) for scripts, MCP, and
+shell quoting.
+
+## Scratchpad Scope
+
+`execute-code` evaluates Python in marimo's scratchpad: a temporary namespace
+with a shallow copy of the kernel globals. Notebook variables are available by
+name, but new top-level bindings and rebindings are discarded after each call.
+In-place mutations to notebook-owned objects can persist because those names
+still reference live objects.
+
+Each call reports stdout and stderr from the scratchpad, plus console output
+from notebook cells it causes to run, including reactive descendants.
+
+### Ordinary Python
+
+Use ordinary Python in the scratchpad to inspect variables, sample data, test
+transformations, probe APIs, check imports, and read widget state.
+
+```python
+print(df.head())
+
+x = 10
+print(x)
+```
+
+Here `df` comes from notebook globals, while `x` is a scratchpad-local binding.
+`x` exists for this call only and WILL NOT be added to notebook globals.
+
+### Persist with `cm`
+
+Top-level scratchpad assignments and rebindings are temporary. To persist work,
+including new variables, you MUST submit changes through `marimo._code_mode`
+(`cm`).
+
+`marimo._code_mode` is a PRIVATE, UNSTABLE agent API (note the leading
+underscore). It exists for tools like this skill to drive a live kernel from
+the scratchpad. DO NOT import it from notebook cells, library code, or
+anything a user would run — methods can change or disappear across marimo
+versions and kernels. Treat every `import marimo._code_mode as cm` as
+scratchpad-only.
+
+At session start, inspect what `cm` exposes in the active kernel:
+
+```python
+import marimo._code_mode as cm
+
+help(cm)
+```
+
+Open a code-mode context to queue notebook changes.
+
+```python
+import marimo._code_mode as cm
+
+async with cm.get_context() as ctx:
+ cid = ctx.create_cell("x = df.head()")
+ ctx.run_cell(cid)
+```
+
+The scratchpad supports top-level async code. Use `async with` directly;
+wrapping it in `asyncio.run(...)` is unnecessary and can conflict with the
+kernel's event loop.
+
+After this block exits and the new cell runs, `x` is notebook state. Later
+scratchpad calls can read `x` by name. Code later in the same scratchpad call
+should read `ctx.globals["x"]`, because the scratchpad namespace was copied
+before the cell ran.
+
+Inside the context, queued mutation methods are synchronous. Call them
+directly; do not `await` them. Each call queues an operation for marimo to
+apply when the context exits normally. If the block raises, the queue is
+discarded.
+
+On clean exit, marimo applies packages, validates and applies structural cell
+changes, runs queued cells, then may run dependents. Validation is only
+structural since queued cell runs can still error. `create_cell` and
+`edit_cell` change notebook structure only. Use `run_cell` to execute.
+
+`create_cell` currently defaults to `hide_code=True`, which collapses the code
+editor in the UI. Pass `hide_code=False` if the user wants created cells to
+be visible without manually expanding them.
+
+
+## Marimo Rules
+
+marimo imposes a small contract on notebook code so it can keep the notebook as
+a directed acyclic graph (DAG):
+
+- **No cycles** - cells cannot depend on each other in a cycle.
+- **No public redefinitions across cells** - each name has one owning cell.
+- **No wildcard imports** - `import *` prevents static analysis of definitions.
+
+These rules keep the kernel, UI, and saved artifact consistent.
+
+When `cm` submits a cell body, marimo parses its top-level definitions and
+references. A top-level name enters the graph unless it is private with a
+leading underscore.
+
+```python
+# Public definitions: values, total, i, value, mean
+values = np.array([1, 2, 3])
+total = 0
+for i, value in enumerate(values):
+ total += value
+mean = total / len(values)
+mean
+```
+
+```python
+# Public definition: mean
+_values = np.array([1, 2, 3])
+_total = 0
+for _i, _value in enumerate(_values):
+ _total += _value
+mean = _total / len(_values)
+mean
+```
+
+Use private names for intermediates that no other cell should read. Public
+names define the notebook-level dataflow. If a `cm` edit violates the contract,
+marimo rejects the structural change and returns the validation error.
+
+## The Notebook's Shape
+
+A notebook is an ordered collection of cells. `ctx.cells` is the document view
+and `ctx.graph` is the dataflow view.
+
+```python
+for cell in ctx.cells:
+ cell # .id, .code, .name, .config, .status, .errors
+
+ctx.cells["setup"] # by name
+ctx.cells[0] # by position
+list(ctx.cells.keys()) # all IDs, in notebook order
+```
+
+Cell IDs are opaque strings which can be queried from the notebook or captured
+from `cm` return values:
+
+```python
+cid = ctx.create_cell("df = pd.read_csv('data.csv')")
+print(cid) # e.g. 'Hbol'
+```
+
+Alternatively, cells can be assigned and referenced by `name`. The graph can be
+used to understand its role in the dataflow.
+
+```python
+for cid, impl in ctx.graph.cells.items():
+ impl # .defs, .refs (sets of public names)
+
+ctx.graph.descendants(cid) # cells that re-run when this one changes
+ctx.graph.ancestors(cid) # cells this one depends on
+```
+
+In marimo, deletes are *destructive* so it can be useful to query the
+descendants prior to deleting to understand it's impact.
+
+## Writing Notebook Changes
+
+The graph contract keeps marimo able to run and save the notebook. Passing
+those checks alone does not guarantee a useful artifact. Committed cells should
+still be readable, rerunnable, and editable.
+
+Make durable edits that reuse the notebook's existing names, imports,
+dependencies, and UI model. Don't be lazy. Avoid one-off workarounds that pass
+`cm` validation but leave a brittle notebook.
+
+### Cell Bodies
+
+Submit the code that belongs in the cell.
+
+- **Submit cell contents** - `create_cell` and `edit_cell` take cell contents,
+ not saved-file `@app.cell` wrappers.
+- **Read before replacing** - for now, another editor may change a cell between
+ scratchpad calls. Before `edit_cell`, read the current body from
+ `ctx.cells[...]` and submit the full replacement.
+- **Reuse notebook imports** - if `np` already exists, use it or edit the owning
+ import cell. DO NOT add `import numpy as _np` just to bypass the graph.
+- **Define public names intentionally** - use public names for values later
+ cells should reference. Use private `_name` bindings or function locals for
+ same-cell intermediates.
+- **Define each public name once** - a public name has one owning cell.
+ Reassigning it in another cell fails with `Multiply-defined names`; edit the
+ owning cell or give the result a new name. See
+ [gotchas.md](reference/gotchas.md).
+- **Run cells deliberately** - `create_cell` and `edit_cell` change structure
+ only. Queue `ctx.run_cell(...)` when the cell should execute.
+
+### Prefer `cm`-Managed Changes
+
+Use `cm` APIs when they exist. Avoid direct file edits, shell package commands,
+and scratchpad-only state for changes that should persist.
+
+- **Do not edit the `.py` artifact** - DO NOT use `Edit`, `Write`, or
+ `NotebookEdit` on the notebook file during a live session. Use
+ `ctx.edit_cell(...)` even for small changes.
+- **Manage packages through `cm`** - use `ctx.packages.add()` or
+ `ctx.packages.remove()` instead of direct `uv` or `pip`; confirm
+ non-obvious dependency changes.
+- **Avoid transient paths** - persisted cells should not depend on `/tmp/...`
+ unless the work is intentionally transient.
+- **Delete deliberately** - deleting a cell removes globals it defines. Reuse
+ empty cells when convenient and delete cells left empty after edits.
+
+### UI and Widgets
+
+Inspect the object before changing it. Different UI objects update through
+different paths.
+
+- **Set `mo.ui.*` through `cm`** - use `ctx.set_ui_value(element, value)` inside
+ `cm.get_context()`.
+- **Set anywidget traitlets directly** - synced traitlets are Python
+ attributes, for example `widget.value = 5`.
+
+For designing custom visual or interactive output, see
+[rich-representations.md](reference/rich-representations.md).
+
+## References
+
+- [execution-context.md](reference/execution-context.md) — scripts, MCP, auth, startup, and shell quoting
+- [finding-marimo.md](reference/finding-marimo.md) — choosing the right marimo invocation
+- [gotchas.md](reference/gotchas.md) — name redefinition, cached module proxies, and notebook traps
+- [rich-representations.md](reference/rich-representations.md) — custom widgets and visualizations
+- [notebook-improvements.md](reference/notebook-improvements.md) — improving existing notebooks
diff --git a/.agents/skills/marimo-pair/reference/execution-context.md b/.agents/skills/marimo-pair/reference/execution-context.md
new file mode 100644
index 0000000..818e994
--- /dev/null
+++ b/.agents/skills/marimo-pair/reference/execution-context.md
@@ -0,0 +1,62 @@
+# Connection Troubleshooting
+
+Use this reference when `execute-code.sh` or MCP cannot reach the intended
+marimo session, cannot select a session, or fails because code was passed
+incorrectly.
+
+## Targeting
+
+Use explicit targets when possible.
+
+- `--url` connects to a known marimo server or notebook URL.
+- `--port` selects a local marimo server from the registry.
+- `--session` selects one notebook session on a server.
+
+If multiple servers or sessions are available, do not guess. Ask for the URL or
+session, or inspect local context.
+
+## Auth
+
+For token-authenticated servers, prefer `MARIMO_TOKEN`.
+
+```bash
+MARIMO_TOKEN=... bash scripts/execute-code.sh --url http://localhost:2718 -c "1 + 1"
+```
+
+`--token` also works, but may expose the token in process listings. If both are
+present, `--token` overrides `MARIMO_TOKEN`. The script sends the token as
+`Authorization: Bearer ...` on session discovery and code execution requests.
+
+## Quoting
+
+Use `-c` only for short one-liners. Use a single-quoted heredoc or file input
+for multiline code or shell-sensitive characters.
+
+```bash
+bash scripts/execute-code.sh --url http://localhost:2718 <<'PY'
+print(df.head())
+PY
+```
+
+```bash
+bash scripts/execute-code.sh --url http://localhost:2718 /tmp/code.py
+```
+
+## Common Script Errors
+
+- **No running marimo instances found** - use an explicit `--url`, or start
+ marimo with the project's normal tooling.
+- **Multiple instances found** - rerun with `--port` or `--url`.
+- **No active sessions on the server** - open the notebook in the browser or
+ provide `--session`.
+- **Multiple sessions on server** - rerun with `--session`.
+- **Failed to connect** - check the URL, token, and whether the server is still
+ running.
+- **SyntaxError** - the submitted Python was malformed; use a heredoc or file.
+- **ImportError** - diagnose in the notebook kernel. Install packages through
+ `cm` when needed.
+
+## Starting marimo
+
+Discover first. If no server is running and the user wants a notebook, use
+[finding-marimo.md](finding-marimo.md).
diff --git a/.agents/skills/marimo-pair/reference/finding-marimo.md b/.agents/skills/marimo-pair/reference/finding-marimo.md
new file mode 100644
index 0000000..f972cb9
--- /dev/null
+++ b/.agents/skills/marimo-pair/reference/finding-marimo.md
@@ -0,0 +1,99 @@
+# Finding and Invoking marimo
+
+Only servers started with `--no-token` register in the local server registry
+and are auto-discoverable — starting without a token makes discovery easier.
+If a server has a token, set the `MARIMO_TOKEN` environment variable before
+calling the execute script (avoids leaking the token in process listings).
+
+```sh
+marimo edit notebook.py --no-token [--sandbox]
+```
+
+Start marimo in edit mode without `--headless` unless the user asks for a
+headless server. The notebook UI must be open in a browser before marimo has an
+active session for `execute-code` to target. If running headless, give the user
+the local URL and wait for them to open it before executing code.
+
+How you invoke `marimo` depends on context — find the right way to run it.
+
+## Notebooks with PEP 723 metadata require `--sandbox`
+
+Before picking a runner, check the notebook file for a PEP 723 header:
+
+```python
+# /// script
+# requires-python = ">=3.11"
+# dependencies = [
+# "marimo",
+# "polars",
+# ]
+# ///
+```
+
+If the block is present, the notebook was authored as a self-contained
+sandboxed script and **SHOULD be opened with `--sandbox`**. Without the flag
+marimo runs in the ambient environment and silently ignores the inline
+dependencies. Imports will fail or, worse, resolve to a different version than
+the author pinned.
+
+`--sandbox` works regardless of project context: inside a uv project, `uv run
+marimo edit notebook.py --no-token --sandbox` still creates the isolated env
+from the PEP 723 block rather than the project's `.venv`.
+
+## Inside a Python project
+
+If there's a `pyproject.toml` in cwd or a parent directory, check that marimo
+is actually in the dependencies before using the project's runner. Look for
+`marimo` in:
+
+- `[project.dependencies]`
+- `[project.optional-dependencies]` or `[dependency-groups]` (dev deps)
+- `[tool.pixi.dependencies]`
+- The project's `.venv` (`uv pip show marimo` or check `.venv/bin/marimo`)
+
+If marimo is in a named dependency group (not the default), you need to
+specify it:
+
+```sh
+# marimo is in [dependency-groups] → "notebooks" group
+uv run --group notebooks marimo edit notebook.py --no-token
+```
+
+Once you know marimo is available, use whatever CLI runner the project uses:
+
+```sh
+# uv-managed project
+uv run marimo edit notebook.py --no-token
+# pixi-managed project
+pixi run marimo edit notebook.py --no-token
+```
+
+Skip `--sandbox` here — the project already manages dependencies.
+
+If `pyproject.toml` exists but marimo is **not** in the deps, treat this as
+"outside a project" (see below).
+
+## Outside a Python project
+
+Prefer `--sandbox`. Sandbox mode creates an isolated environment for the
+notebook and writes dependencies into the script itself as inline PEP 723
+metadata — so the notebook stays self-contained and reproducible.
+
+```sh
+# With uv available (preferred)
+uvx marimo@latest edit notebook.py --no-token --sandbox
+
+# With marimo installed globally
+marimo edit notebook.py --no-token --sandbox
+```
+
+## Global marimo install
+
+If marimo is installed globally, check the version — code mode shipped in
+v0.21.1. If the installed version is older, prompt the user to upgrade before
+proceeding.
+
+## Nothing found
+
+If no project marimo, no `uv`/`uvx`, and no global `marimo` on PATH, tell the
+user to install `uv` (