This commit is contained in:
wassname
2026-07-13 05:38:29 +08:00
parent 22613ce40d
commit 89fc7b1068
16 changed files with 1479 additions and 0 deletions
+295
View File
@@ -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
@@ -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).
@@ -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` (<https://docs.astral.sh/uv/getting-started/installation/>).
@@ -0,0 +1,88 @@
# Gotchas
## Private variables are cell-scoped
Variables with a `_` prefix are **private to the cell that defines them** in
marimo. They cannot be referenced from other cells — you'll get a `NameError`.
This matters when building notebooks programmatically. A common mistake:
```python
# Cell A
_df = pd.DataFrame(results) # _df is private to this cell
# Cell B — FAILS
mo.ui.table(_df) # NameError: name '_df' is not defined
```
**Fix:** Either merge both into one cell, or use a non-private name (`df`).
## Redefining a public name across cells
Each public name has one owning cell. Defining it again in another cell fails
with `Multiply-defined names`. This is easy to hit when building a notebook
incrementally — a second cell reassigns `df`, `results`, `data`, etc.
```python
# Cell A
df = pd.read_csv("data.csv")
# Cell B — FAILS: df already defined in Cell A
df = df.dropna() # Multiply-defined names: df
```
**Fix — pick one:**
- **Edit the owning cell** if the step belongs there (`ctx.edit_cell`).
- **Use a new name** when later cells need the result (`clean = df.dropna()`).
- **Use a private `_` name** for a throwaway intermediate (`_clean = df.dropna()`).
`ctx.graph.cells[cid].defs` shows what a cell already owns.
## Duplicate public imports across cells
The same single-definition rule applies to imports: a public name (like `pd`)
can only be defined in one cell. If two cells both `import pandas as pd`, you
get a `Multiply-defined names` error at validation.
**Fix:** Use a `_` prefix on the second import (`import pandas as _pd`) or
consolidate imports into a shared cell.
## `inspect.getsource()` on methods is indented
`inspect.getsource()` on a class method preserves the original indentation.
Passing this to `ast.parse()` fails with `IndentationError`.
```python
# FAILS
src = inspect.getsource(SomeClass.some_method)
tree = ast.parse(src) # IndentationError: unexpected indent
# FIX
import textwrap
src = textwrap.dedent(inspect.getsource(SomeClass.some_method))
tree = ast.parse(src)
```
## Cached module availability
Some libraries cache optional-dependency availability at import time. Installing
a package mid-session via `ctx.packages.add()` won't update those caches.
The user may need to restart the kernel — but try known workarounds first.
### Polars + pyarrow
`df.to_pandas()` fails with `ModuleNotFoundError: pa.Table requires 'pyarrow'`.
**Workaround** — if this error occurs after installing pyarrow mid-session,
run the following via `execute-code` (scratchpad), NOT in a cell. The patch
mutates the cached module object in the running kernel, so it doesn't need to
persist in the notebook.
```python
import pyarrow as _pa
import polars.dataframe.frame as _frame_mod
_frame_mod.pa = _pa
```
Then re-run the failing cell.
@@ -0,0 +1,97 @@
# Notebook Improvements
When the user asks to improve, optimize, or clean up their notebook, scan the
current cells for these opportunities. Use your judgment — don't over-apply,
and if you're unsure whether a change is worthwhile, ask the user.
## Cell names
Low priority unless the user asks. `setup` and cells defining
functions/classes are auto-named by marimo. Beyond that, naming is optional.
Note that naming markdown cells clutters the UI by showing the cell header
that's normally hidden.
## Setup cell
A setup cell is named `"setup"` and is guaranteed to run before all other
cells. It's the place for module imports. Consolidating imports here keeps
the notebook clean and ensures every cell can rely on those modules being
available.
**The setup cell cannot reference other cells' variables.** It runs first, so
it must be self-contained: imports, constants, and definitions that depend only
on each other. Reading a name defined elsewhere (e.g. `df`, a UI element) fails
with `The setup cell cannot have references`.
First check if the notebook already has a cell named `"setup"`. If not, create
one and hoist scattered imports into it. `name="setup"` auto-positions the cell
first — no `before`/`after` needed:
```python
cid = ctx.create_cell('''import polars as pl
import marimo as mo
import anywidget
import traitlets''', name="setup")
ctx.run_cell(cid)
```
If a setup cell already exists, `create_cell(name="setup")` raises `ValueError`;
use `ctx.edit_cell("setup", code=...)` and `ctx.run_cell("setup")` instead.
## Lift reusable functions into their own cells
When a cell contains a single function or class that doesn't reference
variables from other cells, marimo treats it specially — it can be written as
a standalone definition and reused outside the notebook. These functions can
use modules from the setup cell.
Look for functions that **could belong in a library**: data loading, transforms,
parsers, domain logic, custom widgets. If someone might reasonably `import` it
from another module, it's a good candidate to lift into its own cell.
Don't lift everything — notebook-specific wiring (UI layout, display logic,
cell-level orchestration) should stay where it is. Use `_prefix` for
cell-internal helpers that aren't meant to be reused.
```python
# before: useful logic buried in a larger cell
objects = pl.read_csv("https://example.com/objects.csv")
artists = pl.read_csv("https://example.com/artists.csv")
def top_counts(df, col, n=5):
return df.group_by(col).len().sort("len", descending=True).head(n)
result = top_counts(objects.join(artists, on="id"), "category")
```
```python
# after: top_counts is general-purpose — give it its own cell
# cell 1
def top_counts(df, col, n=5):
return df.group_by(col).len().sort("len", descending=True).head(n)
```
```python
# cell 2
result = top_counts(df, "category")
```
## `mo.persistent_cache`
`@mo.persistent_cache` caches a function's result to disk so it isn't
recomputed on subsequent runs. The cache persists across kernel restarts.
```python
@mo.persistent_cache
def load_data():
objects = pl.read_csv("https://example.com/objects.csv")
artists = pl.read_csv("https://example.com/artists.csv")
return objects.join(artists, on="id", how="left")
df = load_data()
```
Good candidates: data loading, ETL, expensive computation that rarely changes.
Don't over-optimize — if you're unsure, suggest it to the user rather than
applying it.
@@ -0,0 +1,303 @@
# Rich Representations
Custom visual encodings for data that go beyond standard charts and tables.
## Guiding principles
**Visualization matters.** Helping users build custom visual representations
is one of the highest-impact things the agent can do. A bespoke encoding
tailored to the task — labeling, batch review, comparing variants — lets
users *see* their data in ways that tables and numbers never will. marimo
is an environment where users create their own views, not just consume
library charts. Help them imagine what's possible, then build it.
**Use modern web APIs.** Models may default to older browser patterns; prefer
modern HTML, CSS, and JavaScript that are supported in current browsers. Avoid
build steps unless the task clearly needs them.
**Prefer compact output.** marimo clips cell output at ~610px and scrolls.
Avoid hitting that limit; if you need more space, manage your own scrolling
inside a fixed-height container.
**Keep it thin, make it compose.** A widget is a thin layer over data, not
an application. One clear purpose, few traitlets, small `_esm`. Build small
pieces that compose in the notebook — combine with other cells, UI elements,
and views. Don't over-engineer.
## Decision tree
| Need | Approach |
|------|----------|
| Custom output or interaction | **anywidget** — flexible enough to grow from display-only to interactive |
| Tiny static HTML representation | `_display_()` or `mo.Html` |
| Built-in control used as-is (slider, dropdown) | `mo.ui.*` |
For custom representations, prefer anywidget unless the output is clearly a
small static one-off.
## anywidget
[anywidget](https://anywidget.dev) bridges Python and JavaScript via
traitlets. `.tag(sync=True)` makes a traitlet bidirectional — Python sets a
value → JS sees it; JS calls `model.set()` + `model.save_changes()`
Python sees it. `_css` is optional global CSS.
**marimo does not render traditional Jupyter widgets.** Libraries like jscatter,
ipyvolume, etc. often have a top-level object whose default representation is a
Jupyter widget (`application/vnd.jupyter.widget-view+json`). marimo cannot
display these — you need to find the underlying **anywidget** instance, which
marimo *does* support.
Common pattern: look for a `.widget` attribute on the library object:
```python
# jscatter example — Scatter is not renderable, but .widget is an anywidget
scatter = jscatter.Scatter(data=df, x="x", y="y")
scatter.widget # <-- use this in the cell output
```
When unsure, check in the scratchpad:
```python
import anywidget
obj = scatter.widget # or whatever accessor the library provides
print(isinstance(obj, anywidget.AnyWidget)) # True = marimo can render it
```
### `_esm` lifecycle
**Render only** (most widgets):
```js
function render({ model, el }) { /* ... */ }
export default { render };
```
**Initialize + render** (shared state across views, one-time setup):
```js
export default () => {
return {
initialize({ model }) {
// Once per widget instance — timers, connections, shared handlers
return () => { /* cleanup */ };
},
render({ model, el }) {
// Once per view — display in 3 cells = 3 renders
return () => { /* cleanup DOM listeners */ };
}
};
};
```
- `model.on()` is auto-cleaned when a view is removed
- DOM `addEventListener` is **not** — clean up with `AbortController`
### Timer example (initialize + render)
`initialize` owns one interval; each `render` view displays it.
```python
import anywidget
import traitlets
_TIMER_ESM = """
export default () => {
return {
initialize({ model }) {
const id = setInterval(() => {
if (model.get("running")) {
model.set("seconds", model.get("seconds") + 1);
model.save_changes();
}
}, 1000);
return () => clearInterval(id);
},
render({ model, el }) {
const controller = new AbortController();
const { signal } = controller;
const span = document.createElement("span");
span.style.cssText = "font: 24px monospace;";
const btn = document.createElement("button");
btn.style.cssText = "margin-left: 8px; cursor: pointer;";
function update() {
const s = model.get("seconds");
const mm = String(Math.floor(s / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
span.textContent = `${mm}:${ss}`;
btn.textContent = model.get("running") ? "" : "";
}
model.on("change:seconds", update);
model.on("change:running", update);
btn.addEventListener("click", () => {
model.set("running", !model.get("running"));
model.save_changes();
}, { signal });
update();
el.append(span, btn);
return () => controller.abort();
}
};
};
"""
class Timer(anywidget.AnyWidget):
seconds = traitlets.Int(0).tag(sync=True)
running = traitlets.Bool(True).tag(sync=True)
_esm = _TIMER_ESM
```
### Composing with the notebook
Widgets become reactive notebook citizens when you bridge a traitlet to
`mo.state`. This is a two-cell pattern — create the widget and wire up the
observer in one cell, read the value in another:
```python
# Cell 1 — widget + observer
timer = Timer()
get_seconds, set_seconds = mo.state(timer.seconds)
timer.observe(lambda _: set_seconds(timer.seconds), names=["seconds"])
timer # display the widget
```
```python
# Cell 2 — reacts to changes
seconds = get_seconds()
mo.md(f"Timer is at **{seconds}s** — {'running' if seconds > 0 else 'stopped'}")
```
The common pattern is `mo.state(widget.trait)` for the initial value,
`.observe()` on the specific trait name, and reading with the getter in a
downstream cell. See [Reactive anywidgets](#reactive-anywidgets-in-marimo)
for the details.
### CDN dependencies
Import JS libraries from [esm.sh](https://esm.sh) — no build step:
```js
import * as d3 from "https://esm.sh/d3@7";
import { tableFromIPC } from "https://esm.sh/@uwdata/flechette@2";
```
### DataFrames and binary data
**Prefer reducing data on the Python side.** Aggregate, filter, sample —
send the widget only what it needs. Most widgets should receive a small,
pre-processed payload via simple traitlets (lists, dicts). This keeps the
widget simple and avoids extra dependencies.
**For large tabular data (>2k rows)** where the widget genuinely needs
row-level access, send Arrow IPC bytes instead of JSON. This adds
complexity and dependencies, so only reach for it when the data volume
justifies it.
**Python — serialize:**
```python
# Polars (native, no pyarrow needed)
_ipc=df.write_ipc(None).getvalue()
# Any __arrow_c_stream__ source (pandas, narwhals, pyarrow, etc.)
import io, pyarrow as pa, pyarrow.feather as feather
def to_arrow_ipc(data) -> bytes:
table = pa.RecordBatchReader.from_stream(data).read_all()
sink = io.BytesIO()
feather.write_feather(table, sink, compression="uncompressed")
return sink.getvalue()
```
**JS — deserialize with `@uwdata/flechette`:**
```js
import { tableFromIPC } from "https://esm.sh/@uwdata/flechette@2";
const table = tableFromIPC(new Uint8Array(model.get("_ipc").buffer));
// table.numRows, table.numCols, table.get(i), table.getChild("col_name")
```
Use `traitlets.Any().tag(sync=True)` for the IPC bytes traitlet.
## Reactive anywidgets in marimo
When an anywidget trait (selection, value, zoom, etc.) should drive a
downstream marimo cell, use `mo.state()` + `.observe()` on the **specific
trait**. This is the preferred pattern:
```python
# In the cell that creates the widget:
get_selection, set_selection = mo.state(widget.selection)
widget.observe(
lambda _: set_selection(widget.selection),
names=["selection"],
)
# In a downstream cell — re-executes when selection changes:
selection = get_selection()
```
Initialize `mo.state()` with the widget's current trait value — not a
hardcoded default. Read the trait directly off the widget in the lambda.
Do **not** use `change["new"]` or `allow_self_loops=True`.
### `mo.state` + `.observe()` vs `mo.ui.anywidget()`
Two strategies for reactive anywidgets. Choose one per widget — don't mix them.
| Strategy | Reactivity | Best for |
|----------|-----------|----------|
| `mo.state` + `.observe()` | Specific traits you pick | Precision — only named traits trigger downstream cells |
| `mo.ui.anywidget(widget)` | All synced traits as one `.value` dict | Convenience — observe everything at once |
### Programmatic widget control (scratchpad)
Read widget state or set UI controls from the scratchpad — no clicking:
```python
print(timer.seconds) # read
timer.seconds = 0 # set — frontend updates automatically
```
`mo.ui.*` elements need `ctx.set_ui_value(...)` from code mode; anywidgets use
direct assignment.
## `_display_()` protocol
Any object with a `_display_()` method renders richly in marimo. Return
anything marimo can render — `mo.Html`, `mo.md()`, a chart, a string.
Precedence: `_display_()` > built-in formatters > `_mime_()` > IPython
`_repr_*_()` methods.
```python
from dataclasses import dataclass
import marimo as mo
@dataclass
class ColorSwatch:
colors: list[str]
def _display_(self):
divs = "".join(
f'<div style="width:40px;height:40px;background:{c};border-radius:4px;"></div>'
for c in self.colors
)
return mo.Html(f'<div style="display:flex;gap:8px;">{divs}</div>')
```
For inline `<script>` tags, use `document.currentScript.previousElementSibling`
to scope to the element — never hardcode IDs (breaks with multiple instances).
## Minimize CLS (Cumulative Layout Shift)
Use `min-height` or `aspect-ratio` on the outer container so the widget
reserves space before content loads or when toggling between states.
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# List running marimo instances from the server registry.
# Cleans up stale entries (dead PIDs) and outputs live servers as JSON.
# No marimo installation required.
set -euo pipefail
# Locate the servers directory
is_windows=false
if [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]]; then
is_windows=true
servers_dir="$HOME/.marimo/servers"
else
servers_dir="${XDG_STATE_HOME:-$HOME/.local/state}/marimo/servers"
fi
if [[ ! -d "$servers_dir" ]]; then
echo "[]"
exit 0
fi
# Liveness check. On POSIX, `kill -0 $pid` is cheap and reliable. On Windows
# (Git Bash/MSYS2) `kill` operates on Cygwin PIDs, not the native Windows PIDs
# marimo writes, so fall back to an HTTP probe against marimo's /health.
check_live() {
local f="$1"
if [[ "$is_windows" == false ]]; then
local pid
pid=$(jq -r '.pid' "$f" 2>/dev/null) || return 1
kill -0 "$pid" 2>/dev/null
else
local host port base_url
host=$(jq -r '.host' "$f" 2>/dev/null) || return 1
port=$(jq -r '.port' "$f" 2>/dev/null) || return 1
base_url=$(jq -r '.base_url' "$f" 2>/dev/null) || return 1
curl -sf --max-time 1 "http://${host}:${port}${base_url}/health" >/dev/null 2>&1
fi
}
results="[]"
for f in "$servers_dir"/*.json; do
[[ -e "$f" ]] || continue
if ! check_live "$f"; then
# On Windows the HTTP probe can fail transiently (slow start, busy server),
# so keep the entry; only POSIX `kill -0` is reliable enough to delete on.
[[ "$is_windows" == false ]] && rm -f "$f"
continue
fi
entry=$(jq '.' "$f" 2>/dev/null) || continue
results=$(echo "$results" | jq --argjson e "$entry" '. + [$e]')
done
echo "$results" | jq .
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# Execute code in a running marimo session's scratchpad.
# No marimo installation required — talks directly to the HTTP API.
# Usage:
# execute-code.sh [--port PORT] [--session ID] -c "code" # inline code
# execute-code.sh [--port PORT] [--session ID] script.py # code from file
# execute-code.sh [--port PORT] [--session ID] <<< "code" # stdin (here-string)
# execute-code.sh [--port PORT] [--session ID] <<'EOF' # stdin (heredoc)
# code
# EOF
# execute-code.sh --url URL [--session ID] -c "code" # skip discovery, hit URL directly
#
# Auth: set MARIMO_TOKEN env var (preferred) or pass --token TOKEN (visible in ps).
set -euo pipefail
# Optional eval logging: set EXECUTE_CODE_LOG to a file path to record each call
if [[ -n "${EXECUTE_CODE_LOG:-}" ]]; then
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$EXECUTE_CODE_LOG"
fi
port=""
code=""
url=""
token="${MARIMO_TOKEN:-}"
session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--port) port="$2"; shift 2 ;;
--url) url="$2"; shift 2 ;;
--token) token="$2"; shift 2 ;;
--session) session="$2"; shift 2 ;;
-c) code="$2"; shift 2 ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) break ;;
esac
done
if [[ -n "$code" ]]; then
: # set via -c
elif [[ $# -gt 0 ]]; then
code=$(cat "$1")
elif [[ ! -t 0 ]]; then
code=$(cat)
else
echo "Usage: execute-code.sh [--port PORT | --url URL] -c 'code'" >&2
echo " execute-code.sh [--port PORT | --url URL] script.py" >&2
echo " echo 'code' | execute-code.sh [--port PORT | --url URL]" >&2
echo "Auth: set MARIMO_TOKEN env var (preferred) or pass --token TOKEN" >&2
exit 1
fi
if [[ -n "$url" ]]; then
base="${url%/}"
# Warn when connecting to a non-local server (data exfiltration risk)
url_host="${url#*://}"
url_host="${url_host%%[:/]*}"
case "$url_host" in
localhost|127.0.0.1|::1|0.0.0.0) ;;
*) echo "Warning: connecting to non-local server '${url_host}'. Ensure this is trusted." >&2 ;;
esac
else
# Locate the servers directory
is_windows=false
if [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]]; then
is_windows=true
servers_dir="$HOME/.marimo/servers"
else
servers_dir="${XDG_STATE_HOME:-$HOME/.local/state}/marimo/servers"
fi
# Liveness check. On POSIX, `kill -0 $pid` is cheap and reliable. On Windows
# (Git Bash/MSYS2) `kill` operates on Cygwin PIDs, not the native Windows PIDs
# marimo writes, so fall back to an HTTP probe against marimo's /health.
check_live() {
local f="$1"
if [[ "$is_windows" == false ]]; then
local pid
pid=$(jq -r '.pid' "$f" 2>/dev/null) || return 1
kill -0 "$pid" 2>/dev/null
else
local host port base_url
host=$(jq -r '.host' "$f" 2>/dev/null) || return 1
port=$(jq -r '.port' "$f" 2>/dev/null) || return 1
base_url=$(jq -r '.base_url' "$f" 2>/dev/null) || return 1
curl -sf --max-time 1 "http://${host}:${port}${base_url}/health" >/dev/null 2>&1
fi
}
# Find a live registry entry
entry=""
count=0
for f in "$servers_dir"/*.json; do
[[ -e "$f" ]] || continue
if ! check_live "$f"; then
# On Windows the HTTP probe can fail transiently (slow start, busy server),
# so keep the entry; only POSIX `kill -0` is reliable enough to delete on.
[[ "$is_windows" == false ]] && rm -f "$f"
continue
fi
e=$(cat "$f")
if [[ -n "$port" ]]; then
e_port=$(echo "$e" | jq -r '.port')
if [[ "$e_port" == "$port" ]]; then
entry="$e"
count=1
break
fi
continue
fi
entry="$e"
count=$((count + 1))
done
if [[ $count -eq 0 ]]; then
echo "No running marimo instances found." >&2
exit 1
fi
if [[ $count -gt 1 ]]; then
echo "Multiple instances found. Use --port to specify:" >&2
for f in "$servers_dir"/*.json; do
[[ -e "$f" ]] || continue
check_live "$f" || continue
jq -r '.server_id' "$f" >&2
done
exit 1
fi
host=$(echo "$entry" | jq -r '.host')
e_port=$(echo "$entry" | jq -r '.port')
base_url=$(echo "$entry" | jq -r '.base_url')
base="http://${host}:${e_port}${base_url}"
fi
# Build optional auth header
auth_args=()
if [[ -n "$token" ]]; then
auth_args+=(-H "Authorization: Bearer ${token}")
fi
# Discover session ID
if [[ -n "$session" ]]; then
session_id="$session"
else
sessions_resp=$(curl -sf "${auth_args[@]+"${auth_args[@]}"}" "${base}/api/sessions") || {
echo "Failed to connect to marimo server at ${base}" >&2
exit 1
}
session_ids=$(echo "$sessions_resp" | jq -r 'keys[]')
if [[ -z "$session_ids" ]]; then
echo "No active sessions on the server. Make sure a notebook is open in the browser." >&2
exit 1
fi
session_count=$(echo "$session_ids" | wc -l | tr -d ' ')
if [[ $session_count -gt 1 ]]; then
echo "Multiple sessions on server. Cannot auto-select:" >&2
echo "$sessions_resp" | jq -r 'to_entries[] | "\(.key) \(.value.filename // "")"' >&2
exit 1
fi
session_id=$(echo "$session_ids" | head -1)
fi
# Execute code via SSE stream
# Events: stdout/stderr stream as JSON {"data":"..."}, done is final result.
exit_code=0
current_event=""
done_received=false
while IFS= read -r line && [[ "$done_received" == false ]]; do
case "$line" in
event:*)
current_event="${line#event: }"
;;
data:*)
payload="${line#data: }"
case "$current_event" in
stdout)
echo "$payload" | jq -jr '.data'
;;
stderr)
echo "$payload" | jq -jr '.data' >&2
;;
done)
if echo "$payload" | jq -e '.success == false' >/dev/null 2>&1; then
echo "$payload" | jq -r '.error.msg' >&2
exit_code=1
else
echo "$payload" | jq -r '.output.data // empty'
fi
done_received=true
;;
esac
;;
esac
done < <(curl -sN -X POST "${base}/api/kernel/execute" \
-H "Content-Type: application/json" \
-H "Marimo-Session-Id: ${session_id}" \
${auth_args[@]+"${auth_args[@]}"} \
-d "$(jq -n --arg c "$code" '{code: $c}')" \
)
exit "$exit_code"
+141
View File
@@ -0,0 +1,141 @@
---
name: retro-marimo-pair
description: >-
Session retrospective for improving marimo-pair and marimo._code_mode.
Use when the user wants to analyze friction from a pairing session, identify
what went wrong, and brainstorm improvements to the skill docs or the
underlying API. Trigger on: "retro", "what went wrong", "improve the skill",
"session review", "friction", or /retro-marimo-pair.
---
# Session Retrospective
You are helping a **marimo team member** review a pairing session to find
friction and turn it into improvements. The target is always one or both of:
1. **The marimo-pair skill** (`github://marimo-team/marimo-pair`)
2. **`marimo._code_mode`** — the underlying notebook metaprogramming API
This is a **conversation**, not an automated report. You surface findings,
the user steers which ones matter, and together you decide what to do about
them.
## Guard Rails
- **NEVER** edit files in `github://marimo-team/marimo-pair` without explicit
user approval.
- **ALWAYS** start with session analysis (Step 1) — do not jump to solutions.
- **Present friction points before root causes** — let the user choose which
ones to dig into.
- If the user invoked with a specific complaint, focus your analysis there but
still scan for other friction in the background.
## Step 1: Session Analysis
Review the current conversation and identify friction. Look for:
| Signal | What to look for |
|--------|-----------------|
| **User frustration** | Corrections ("no not that"), repeated attempts, backtracking, confusion, tone shifts |
| **Inefficiency** | Multiple rounds for a one-step task, over-engineering, wrong API usage |
| **Errors** | Compile-check failures, runtime errors, silent failures, wrong output |
| **Workarounds** | User or Claude working around a limitation instead of doing it directly |
| **Context loss** | Claude forgetting instructions from earlier, re-asking things the skill covers |
Present a numbered summary of friction points found. For each, note:
- What happened (brief)
- Where in the conversation it occurred (quote or paraphrase)
- Initial category guess (skill structure / skill gap / API issue / etc.)
Then ask: **"Which of these should we dig into? Or is there something I missed?"**
## Step 2: Root Cause Discussion
For each friction point the user selects, work through these lenses:
| Lens | Question | Example improvement |
|------|----------|-------------------|
| **Skill structure** | Was the right info in the skill but hard to find? Buried in reference/ when it should be in SKILL.md? | Promote to guard rail, restructure progressive disclosure |
| **Skill gap** | Was information missing entirely from the skill? | Add new section, example, or anti-pattern |
| **Misleading docs** | Did the skill say something that led Claude astray? | Correct the docs, add clarifying examples |
| **API ergonomics** | Was `_code_mode` clunky or unintuitive for this task? | Propose API improvement (better defaults, clearer errors) |
| **Missing API** | Is there something `_code_mode` simply can't do that it should? | Design a new API surface |
| **API bug** | Did `_code_mode` behave incorrectly? | Characterize the bug, propose fix or workaround |
| **Context window** | Did Claude forget instructions due to long context? | Shorter, more prominent guard rails |
Discuss each lens briefly, then converge on the most likely root cause with the
user. It's okay to have multiple contributing causes.
## Step 3: Diagnose & Capture
The goal of a retro is **diagnosis**, not a contribution. Based on the root
cause, write up a clear diagnosis the team can act on — don't jump to proposing
or authoring a fix.
For each friction point, produce:
- **Diagnosis** — What went wrong and why it was frustrating, in plain terms
- **Contributing factors** — Skill structure, gap, misleading docs, API
ergonomics, missing API, API bug, context window (from Step 2)
- **Considerations** — Trade-offs, open questions, or things that would need to
be true for a fix to make sense. Note possible directions here, but frame
them as considerations rather than committed solutions.
The default next step is to **capture the diagnosis as an issue or discussion**
so the team can weigh it — not to immediately make a contribution. Concrete
code changes (skill edits, API designs) come *after* an issue/discussion
exists and the user explicitly chooses to go further.
Present the diagnosis and ask: **"Want me to draft this as an issue or
discussion?"**
## Step 4: File or Follow Up
### Default: file an issue / discussion
Write it up clearly for the marimo team to triage:
- **Problem:** What happened and why it's painful
- **Current behavior:** What the skill or `_code_mode` does today
- **Considerations:** Trade-offs and open questions (not a committed solution)
- **Example:** A minimal snippet or quote from the session, if helpful
Leave the actual filing to the user — do not auto-file. This is the preferred
outcome: surface friction for the team rather than ship a fix from the retro.
### Only if the user explicitly wants to go further
A skill edit or API change should follow an issue/discussion, not replace it.
If — and only if — the user explicitly asks to draft a change now:
1. Read the target file in `github://marimo-team/marimo-pair`
2. Show the proposed diff to the user
3. Only apply after explicit sign-off
4. After applying, verify SKILL.md stays under 500 lines (reference/ files
have no limit)
### Wrapping up
After completing the cycle for the selected friction points, ask if the user
wants to revisit any remaining items from Step 1, or if the retro is done.
## Key Files Reference
| File | Purpose |
|------|---------|
| `github://marimo-team/marimo-pair/SKILL.md` | Main skill instructions |
| `github://marimo-team/marimo-pair/reference/execute-code.md` | Scratchpad & cell operation recipes |
| `github://marimo-team/marimo-pair/reference/rich-representations.md` | Widget & display patterns |
| `github://marimo-team/marimo-pair/scripts/` | Bundled discovery & execution scripts |
To inspect the live `_code_mode` API surface during a retro, the user can
run in their notebook scratchpad:
```python
import marimo._code_mode as cm
async with cm.get_context() as ctx:
# List all public methods/attributes
print([x for x in dir(ctx) if not x.startswith('_')])
help(ctx)
```
@@ -0,0 +1,62 @@
# `marimo._code_mode` API Surface
> This is a **point-in-time snapshot** for retro discussions. The live API may
> differ — always verify with `dir(ctx)` and `help()` in the running session.
## Entry Point
```python
import marimo._code_mode as cm
async with cm.get_context() as ctx:
... # all operations go here
```
The `async with` is mandatory — without it, operations silently do nothing.
The context manager auto-compile-checks on exit: syntax errors, multiply-defined
names, and cycles are caught before any graph mutation occurs.
## Context Object (`ctx`)
### Reading State
| Attribute / Method | Returns | Notes |
|-------------------|---------|-------|
| `ctx.cells` | List of cell objects | Each has `.cell_id`, `.code`, `.name` |
| `ctx.graph` | Kernel graph | Has refs/defs info (cells themselves lack this) |
| `dir(ctx)` | All attributes | Always check this first — API evolves |
### Cell Operations (Mutating)
| Operation | Method | Notes |
|-----------|--------|-------|
| Create cell | `ctx.create_cell(code, ...)` | Adds to graph, auto-compile-checks |
| Edit cell | `ctx.edit_cell(cell_id, code, ...)` | Edits existing cell in-place |
| Delete cell | `ctx.delete_cell(cell_id)` | Confirm with user first |
| Move cell | `ctx.move_cell(cell_id, ...)` | Reorder in notebook |
### Execution
| Operation | Method | Notes |
|-----------|--------|-------|
| Execute code (scratchpad) | Via `execute-code.sh` or MCP | Results return to Claude, not user |
| Execute cell | `ctx.run_cell(cell_id)` | Explicitly queue execution; `create_cell` / `edit_cell` are structural only and do not auto-execute |
### Package Management
| Operation | Method | Notes |
|-----------|--------|-------|
| Install package | Explore via `dir(ctx)` | Prefer API over `uv add` |
## Known Friction Points
Track recurring issues here as they surface in retros:
- **Compile-check false positives on delete+create:** When deleting a cell and
creating a replacement that defines the same variables, the compile check can
see the old definitions still present and reject the new cell. Workaround:
use `check=False` or `edit_cell` instead of delete+create.
- **`ctx.cells` lacks refs/defs:** Cell objects don't expose which variables
they reference or define. Must use `ctx.graph` directly for variable flow
analysis.
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/marimo-pair
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/retro-marimo-pair
+1
View File
@@ -7,3 +7,4 @@ artifacts/
# marimo session cache
nbs/__marimo__/
docs/
+3
View File
@@ -0,0 +1,3 @@
[submodule "docs/vendor/AntiPaSTO_concepts"]
path = docs/vendor/AntiPaSTO_concepts
url = https://github.com/wassname/AntiPaSTO_concepts.git
+45
View File
@@ -0,0 +1,45 @@
# Claude: small-multiples dose-response for the 7-method demo, styled like
# word_steering's plot_sweep but read straight from the edge-find detail in
# steering_demo_results.json. y = P(YES), point colour = ans_mass (readout
# validity: bright = answer alive, dark = answer dying). red edge = readout
# invalid (ans_mass < 0.9*base). Single-seed edge-find, so no error bars.
import json
from pathlib import Path
import matplotlib.pyplot as plt
ROOT = Path(__file__).resolve().parents[2]
d = json.load(open(ROOT / "artifacts" / "steering_demo_results.json"))
detail, summary = d["detail"], {r["method"]: r for r in d["summary"]}
BASE_PYES = 0.107 # P(YES)@C=0, shared across methods (oracle brief)
methods = list(detail)
fig, axes = plt.subplots(2, 4, figsize=(15, 7), sharey=True, layout="constrained")
axes = axes.ravel()
for ax, m in zip(axes, methods):
pts = sorted(detail[m], key=lambda p: p["C"])
Cs = [p["C"] for p in pts]
pyes = [p["ans"] for p in pts]
am = [p["ans_mass"] for p in pts]
edges = ["red" if not p["readout_valid"] else "0.2" for p in pts]
ax.plot(Cs, pyes, "-", color="0.85", lw=1, zorder=1)
ax.axvline(0, color="0.85", lw=0.8, zorder=0)
ax.axhline(BASE_PYES, color="0.85", lw=0.8, ls="--", zorder=0)
sc = ax.scatter(Cs, pyes, c=am, cmap="viridis", vmin=0.0, vmax=1.0,
edgecolor=edges, linewidth=1.4, s=70, zorder=2)
s = summary[m]
ax.set_title(f"{m}\nscore={s['score']:+.3f} ok={s['readout_ok']}", fontsize=9)
ax.set_ylim(-0.03, 1.03)
ax.set_xlabel("steering coefficient C", fontsize=8)
axes[0].set_ylabel("P(YES = lie)")
axes[4].set_ylabel("P(YES = lie)")
for ax in axes[len(methods):]:
ax.set_visible(False)
cbar = fig.colorbar(sc, ax=axes.tolist(), label="ans_mass (readout validity)",
fraction=0.025, pad=0.01)
cbar.ax.axhline(0.90 * 0.56, color="red", lw=1) # ~0.9*base_am floor (base_am~0.56)
fig.suptitle("Dose-response per method: P(YES) vs C, coloured by answer-mass "
"(dark = answer dying). Dashed = baseline P(YES)=0.107. Single seed.",
fontsize=11)
out = ROOT / "artifacts" / "steering_demo_sweep.png"
fig.savefig(out, dpi=110)
print("wrote", out)
+17
View File
@@ -0,0 +1,17 @@
{
"version": 1,
"skills": {
"marimo-pair": {
"source": "marimo-team/marimo-pair",
"sourceType": "github",
"skillPath": "skills/marimo-pair/SKILL.md",
"computedHash": "85a494e8076cbea237bfbd80592a542df869b693f0b61aa90c0a48d7f4afb5a7"
},
"retro-marimo-pair": {
"source": "marimo-team/marimo-pair",
"sourceType": "github",
"skillPath": "skills/retro-marimo-pair/SKILL.md",
"computedHash": "1f89f0999518297d5a0928528c87fc082da9645cac554b0101049042ec661d7b"
}
}
}