A project template for data analysis in marimo notebooks, paired with the pi coding agent.
The agent does not edit notebook files. It attaches to the running marimo kernel and changes cells there. The notebook in your browser and the notebook the agent works on are the same object.
You work from two places at once — a browser tab for the notebook, a terminal for
make and the agent — and both reach the same running kernel:
┌─────────────┐
│ YOU │
└──┬───────┬──┘
┌──────────────┘ └──────────────┐
│ │
┌───────────▼──────────┐ ┌───────────▼──────────┐
│ BROWSER │ │ TERMINAL │
│ the notebook in a │ │ make + your coding │
│ browser tab │ │ agent │
│ │ │ │
│ · write cells │ │ · make nb start │
│ · read plots and │ │ · make check verify │
│ tables │ │ · "add a cell that │
│ · watch them re-run │ │ plots the fit" │
└───────────┬──────────┘ └───────────┬──────────┘
│ │
│ websocket execute-code.sh · cm
└──────────────────┬──────────────────┘
▼
╔═════════════════════════════════════════════════════════════════╗
║ ONE LIVE KERNEL — one per open notebook ║
║ ║
║ your cells run here · Python + one embedded R ║
║ you and the agent share it, and it — not the ║
║ file on disk — is the source of truth ║
╚═════════════╤═════════════════════════════════════╤═════════════╝
│ cells import │ writes
┌───────────▼──────────┐ ┌───────────▼──────────┐
│ petri · scripts/ │ │ notebooks/yours.py │
│ the API + your own │ │ autosaved — only by │
│ pure functions │ │ the browser's tab │
└──────────────────────┘ └──────────────────────┘
Two consequences worth knowing before you start. The .py file is output —
the kernel writes it, so editing it by hand while marimo runs loses the edit.
And since the agent shares your kernel, a cell it adds appears in your tab
without a reload.
- uv — Python and virtualenv
git,bash,curl,jqR,renv(optional, forrpy2)
git clone <this-template> my-project && cd my-project
make setup # install dependencies and git hooks
make init full # copy the examples in (or `make init` for just the notebooks)
make nb # start marimo on notebooks/ (port derived from this directory)Start pi in the project root in a second terminal and ask the agent to pair on the
notebook.
Multiple tabs open? The agent attaches to one kernel. If more than one tab has the same notebook open, it will ask which session (kernel) to edit. To find it, open the hamburger menu (three lines next to settings) → Pair with an agent and copy the instructions — no commands needed, just look it up.
Your folders ship empty, make init to jump-start from petri/examples/:
| Installs | |
|---|---|
make init |
blank.py, py_example.py, r_example.py — standalone, no data needed |
make init full |
the above plus full_example.py, scripts/, and the data it reads |
full_example.py runs the whole pipeline in one notebook: it reads data/external/,
calls scripts/, publishes to data/shared/, then consumes that table and writes
deliverables to data/preserved/. Nothing is overwritten on a re-run unless you
pass ARGS=--force. make help lists all targets.
- Notebooks are
.pyfiles. They diff, review, and run as scripts (e.g.make run NB=notebooks/foo.pyorPYTHONPATH=. uv run python notebooks/foo.py). - Do not edit a notebook file while its kernel runs. The kernel overwrites it.
- Secrets go in
.env. Document new keys in.env.example. - marimo's project settings are in
[tool.marimo]inpyproject.toml. marimo reads that section and never writes to it, so a key typed into the AI panel is written to your own~/.config/marimo/marimo.toml, not to a tracked file. Personal preferences — theme, font size, keymap — belong there too. - The
marimo-pairskill is a fork inpetri/skills/, owned by this repo. See petri/docs/marimo-pair-fork.md. - Agent instructions are in AGENTS.md.
The repo works with pi and with Claude
Code. The skills are in petri/skills/ with the rest of it, and each agent's
entry point is a symlink — one copy, two readers, and neither tool's directory
owns the content:
petri/skills/ the skills themselves: marimo-pair, petri-analysis, petri-init
.pi/skills -> ../petri/skills
.claude/skills -> ../petri/skills
CLAUDE.md -> AGENTS.md
.claude/settings.json permission rules: the marimo scripts and make run without a
prompt; nb-stop, run, export, check and test still ask
Only one agent attaches to the kernel: the one you are talking to. Subagents it spawns stay outside — their own process, no marimo, no cells — and hand back code and numbers as text. The main agent turns that into a cell, so their work still reaches your notebook through the one kernel you share with it, and you still watch it arrive.
Yours, empty until make init:
notebooks/ marimo notebooks
scripts/ the heavy lifting: pure functions, no I/O
data/ your data: external/ shared/ preserved/ cache/ (see below)
All three are gitignored, so what make init installs never gets committed
back as a second, drifting copy of an example. That also means your own work in
them is untracked by default. Once a notebook or a transformation is yours rather
than installed output, put it under version control deliberately — git add -f notebooks/my_analysis.py, or drop the notebooks/* and scripts/* lines from
.gitignore if the repo is now your project rather than a copy of the template.
scripts/ especially: a manifest records the SHA-256 of the module a cell
imported, so make check errors once that file changes or goes missing, and git
is the only thing that can bring it back.
The two language toolchains are supported:
manifest lockfile library
Python pyproject.toml uv.lock .venv/
R (none) renv.lock .renv/
For renv, see: petri/docs/renv.md.
Petri infrastructure (do not delete):
petri/ paths, provenance, R interop — the API your notebooks import
examples/ what `make init` copies out: notebooks/ scripts/ data/
tests/ provenance contracts and the write-path test
docs/ architecture, sessions, git-lfs, renv, rpy2, marimo-pair-fork, todo
assets/ petri-logo.png
skills/ marimo-pair, petri-analysis, petri-init — symlinked into .pi/ and .claude/
init.py the `make init` sets
server.py the per-directory marimo port, behind `make nb`
outline.py a notebook's cells without reading the file, behind `make nb-outline`
.python-version (uv)
.Rprofile (R sources it from the startup directory)
.pre-commit-config.yaml
AGENTS.md (agent instructions)
Data moves one way. A cell loads an external file, passes it through a pure
function from scripts/, and publishes the result — and the two layers petri
owns get a manifest with every write:
data/external/ ──→ scripts/ ──→ data/shared/ ──→ data/preserved/
given to you pure fns save_shared() preserve_*()
never written + manifest + manifest
▲ ▲
└────────┬────────┘
│
make check ─────────┘
re-hashes each artifact,
says what drifted
Each data layer is named for the function that writes it.
| Directory | Written by | Read by |
|---|---|---|
data/external/ |
nobody — inputs from outside | the cell that publishes |
data/shared/ |
save_shared() |
any notebook |
data/preserved/ |
preserve_figure(), preserve_table(), preserve_file() |
people |
data/shared/ is the only channel between notebooks. data/preserved/ holds
deliverables: a figure bundle is a PDF, a PNG, the plotted source data, and a
manifest. Every write records provenance, and make check verifies it.
Git ignores all of data/, manifests included. Provenance is verified where
the data is, by make check, rather than shipped through the repository — a
manifest is a generated file, and you decide what of your own work to version.
Committing one is deliberate (git add -f), worth doing when a collaborator has
to verify a table they cannot download. To push the inputs themselves rather than
sending them around, see petri/docs/git-lfs.md.
data/cache/ is beside the three but is not a layer — nothing verifies it. It is
scratch space with a stable path, ignored by git, and safe to delete. The one
thing petri writes there is data/cache/preview/, the low-resolution figure
previews an agent reads instead of a print-resolution deliverable. data/notebook_htmls/ is the same kind of directory: make export
writes rendered notebooks there, and every one of them can be rebuilt.
See petri/docs/architecture.md for the design and
petri/provenance.py for the API.
make init full # copy the examples into notebooks/, scripts/, data/
make nb # start marimo, or print the URL if it is already running here
make nb-stop # stop this project's server, leaving other projects alone
make check # verify artifacts against their manifests
make test # contracts and the write path
make lint # ruff check and formatEach project gets its own marimo port, derived from its directory, so several petri
checkouts run side by side and make nb-stop never touches another one. make nb-url prints this project's URL rather than assuming marimo's default.
Declared in pyproject.toml, locked in uv.lock. Add with uv add <pkg>.
During a pairing session let the agent use ctx.packages.add(...) so the kernel
stays in sync.
R packages use renv:
make r-restore # rebuild .renv/library from renv.lock
make r-install PKG="ggplot2 bioc::DESeq2" # install and snapshotrpy2 needs a local R installation. See petri/docs/renv.md
and petri/docs/rpy2.md.
