Project layout¶
Every jellycell project has a jellycell.toml at its root.
Canonical directory structure¶
my-project/
├── jellycell.toml # project config (required)
├── notebooks/ # .py source notebooks
├── data/ # input data read by jc.load
├── artifacts/ # outputs written by jc.save / jc.figure / jc.table
├── site/ # rendered HTML output
├── manuscripts/ # narrative docs + tearsheets (markdown, committed)
└── .jellycell/
└── cache/ # content-addressed cache (gitignored)
All paths are configurable — see the [paths] section of jellycell.toml.
Multi-project / monorepo pattern¶
A jellycell monorepo is one Python environment hosting several
jellycell projects side by side — a marketing-analysis project next to
a churn-model project, a paper’s experiments next to each other, or a
personal site’s OSS showcases. One pyproject.toml and one .venv
at the root; one jellycell.toml per project; one AGENTS.md at the
root covering everything inside.
my-repo/
├── pyproject.toml # one uv/pip environment for the whole repo
├── uv.lock # or requirements.txt, poetry.lock, ...
├── .python-version
├── AGENTS.md # one agent guide covering every project
├── CLAUDE.md # 3-line stub → AGENTS.md
├── README.md
├── marketing-analysis/ # a jellycell project
│ ├── jellycell.toml
│ ├── notebooks/
│ ├── artifacts/
│ └── site/
└── churn-model/ # another jellycell project
├── jellycell.toml
├── notebooks/
├── artifacts/
└── site/
Run uv sync once at the root; every project shares the same
environment. Each jellycell.toml is the anchor for its own
notebooks/, artifacts/, site/, manuscripts/, and
.jellycell/cache/ — zero cross-leak. Editing one project’s notebook
doesn’t invalidate its sibling’s cache.
Running commands in a monorepo¶
Project discovery walks up from the notebook path OR from cwd. Two equivalent forms:
# A) Full path from the monorepo root. Simplest for commands that
# take a notebook argument.
uv run jellycell run marketing-analysis/notebooks/tour.py
# B) cd into the project first.
cd marketing-analysis
uv run jellycell run notebooks/tour.py
uv run jellycell render
uv run jellycell view
Commands that don’t take a notebook (render / view / lint /
export) take --project <path>:
uv run jellycell --project marketing-analysis render
uv run jellycell --project churn-model view
jellycell --project X run notebooks/tour.pydoes not rewrite the notebook path to live underX/— the path is resolved against cwd. Use the full path (form A) or acd(form B) when running a notebook.
One AGENTS.md covers every project¶
Agentic tools (Cursor, Codex, Copilot, Aider, Zed, Windsurf) compose nested AGENTS.md files — an outer file applies to every inner path unless an inner one overrides it. Jellycell’s tooling is aware of this:
jellycell init <subdir>detects an outerAGENTS.mdand prints✓ agent guide detected at ../AGENTS.md — Cursor / Codex / Copilot / Claude Code already covered.instead of the usual “tip: add one”.jellycell prompt --write <subdir>refuses to scatter a duplicate inside<subdir>when an outerAGENTS.mdis found. Pass--forceonly if you want an inner override for that subtree.
The walk stops at the repo’s .git/ directory (or $HOME, or the
filesystem root — whichever comes first), so AGENTS.md files sitting
in random ancestor directories above the repo don’t trip the check.
Polyglot monorepos¶
Nothing about jellycell’s walk-up is Python-specific. If your Python
package lives deep inside a pnpm / turbo / Node repo —
packages/python-showcase/showcase-marketing/ — jellycell works
identically, provided AGENTS.md sits somewhere at or above the
project dir and at or below the git root. You have two defensible
patterns for where AGENTS.md goes; pick based on your repo shape.
Pattern A — AGENTS.md at the Python subtree root (recommended for
polyglot). packages/python-showcase/AGENTS.md scopes jellycell’s
~12 KB §10.3 guide to the subtree where it’s relevant. An outer
repo-wide AGENTS.md can still sit at the git root covering
monorepo-structure, TypeScript conventions, etc. — agents compose
them per the AGENTS.md spec (Codex concatenates outer→inner; Cursor,
GitLab Duo, and others read both). When an outer AGENTS.md is
present, tell jellycell the nesting is intentional with --nested:
# Writes AGENTS.md + CLAUDE.md at packages/python-showcase/.
# `uv --directory` cd's into that dir before running, which is exactly
# what we want for Pattern A. `--nested` says "I know there's an outer
# AGENTS.md at the repo root, this is a deliberate inner override."
uv --directory packages/python-showcase run jellycell prompt --write --nested
--nested bypasses the outer-AGENTS-detection refuse only; it still
refuses to clobber an existing target file (use --force to refresh
an existing inner AGENTS.md).
Pattern B — AGENTS.md at the git root (good for Python-first
repos or when you want a single source of truth). uv --project
points uv at the Python package’s env without changing cwd, so
jellycell prompt --write lands AGENTS.md at the repo root:
# cwd stays at the monorepo root; AGENTS.md + CLAUDE.md land there.
# No --nested needed because there's no outer AGENTS.md to compose with.
uv --project packages/python-showcase run jellycell prompt --write
The uv --directory vs uv --project knob is what controls
cwd-dependent behavior (jellycell prompt --write without a path
argument; jellycell init <name> with a relative name). Once you’ve
chosen a pattern, day-to-day run / render / view invocations
work the same under either:
# Notebook path anchored to the Python package root — same under A or B:
uv --directory packages/python-showcase run jellycell run showcase-marketing/notebooks/tour.py
Flag summary for jellycell prompt --write:
pnpm wrapper recipes¶
Jellycell’s --project <path> is a Typer global option — it must
precede the subcommand (jellycell --project X render, not
jellycell render --project X). That’s awkward to wire through pnpm
scripts that accept a positional showcase name at the end. A tiny bash
wrapper bridges the gap.
Use uv --directory packages/python-showcase (cd’s in) in every
wrapper — not uv --project. A showcase name like
showcase-marketing only resolves correctly when cwd is the Python
package root; uv --project keeps cwd at the repo root, where
showcase-marketing would resolve to <repo-root>/showcase-marketing
and fail with No jellycell.toml found.
{
"scripts": {
"showcase:run": "uv --directory packages/python-showcase run jellycell run",
"showcase:init": "uv --directory packages/python-showcase run jellycell init",
"showcase:render": "bash -c 'uv --directory packages/python-showcase run jellycell --project \"$1\" render \"${@:2}\"' --",
"showcase:view": "bash -c 'uv --directory packages/python-showcase run jellycell --project \"$1\" view \"${@:2}\"' --",
"showcase:lint": "bash -c 'uv --directory packages/python-showcase run jellycell --project \"$1\" lint \"${@:2}\"' --"
}
}
Then from the repo root:
pnpm showcase:run showcase-marketing/notebooks/tour.py
pnpm showcase:render showcase-marketing # "$1" → --project showcase-marketing
pnpm showcase:view showcase-churn # live viewer on :5179
pnpm showcase:lint showcase-marketing --fix # extras flow through
showcase:run and showcase:init don’t need the bash-c wrapper
because their first positional argument already locates the project
(a notebook path relative to the Python package root, or a target
directory for init).
uv --project is still the right choice for the one-shot
jellycell prompt --write under Pattern B (single root AGENTS.md) —
there you want cwd pinned at the monorepo root so the file lands
there. It’s just a bad fit for day-to-day wrappers that accept
relative showcase paths.
See examples/monorepo/
for a minimal, runnable reference.
If you already have an AGENTS.md¶
jellycell prompt --write refuses to clobber an existing
AGENTS.md or CLAUDE.md at the target. --force is the only
bypass today; see the flag table above. There’s no in-file merge
primitive yet.
Polyglot, want jellycell’s guide inside the Python subtree —
--nested(no outer-detection refuse) plus--force(only if an innerAGENTS.mdalready exists). The outer rootAGENTS.mdstays untouched; composition happens at agent-read time.Single-root pattern, existing hand-written AGENTS.md — merge by hand: back up your file, run
--force, paste your repo- specific preamble back. Or keep your hand-writtenAGENTS.mdand write jellycell’s to a siblingAGENTS.jellycell.mdthat your main file references with a plain markdown link.
A future release will add jellycell prompt --append with Next.js-
style <!-- BEGIN:jellycell … --> / <!-- END:jellycell … -->
marker blocks, idempotently refreshed in place. That removes the
--force-or-clobber tradeoff for mixed AGENTS.md files. Track /
influence at github.com/random-walks/jellycell.
Full jellycell.toml reference¶
See jellycell.toml.example in the repo root for a commented reference. Sections:
[project]¶
[project]
name = "my-project" # human-readable name; shown in the catalogue
[paths]¶
All paths are relative to the project root. Nothing jellycell writes ever
escapes a declared root (write-guard in jellycell.paths.Project.resolve).
[paths]
notebooks = "notebooks" # source .py files
data = "data" # input data
artifacts = "artifacts" # output files
site = "site" # rendered HTML catalogue
manuscripts = "manuscripts" # optional prose companions
cache = ".jellycell/cache" # content-addressed cache
[run]¶
[run]
kernel = "python3" # Jupyter kernel name
subprocess = true # subprocess-only; in-process is unsupported
timeout_seconds = 600 # per-cell default; `timeout=N` tag overrides
[viewer]¶
Only consumed by jellycell view (requires the [server] extra).
[viewer]
host = "127.0.0.1"
port = 5179
watch = ["notebooks", "manuscripts", "artifacts"] # paths triggering reloads
[artifacts]¶
Controls where path-less jc.figure() / jc.table() calls save, and
when jellycell warns about outsized outputs. Explicit paths in
jc.save(x, "artifacts/foo.json") always win — the layout setting is
only consulted when jellycell picks the location.
[artifacts]
layout = "flat" # "flat" | "by_notebook" | "by_cell"
max_committed_size_mb = 50 # post-run warning threshold; 0 to disable
layout = "flat"(default) — every artifact lands underartifacts/<name>.<ext>. Non-breaking with any existing notebook.layout = "by_notebook"— path-less figures and tables land underartifacts/<notebook-stem>/<name>.<ext>. Good when one project has many notebooks producing similarly-named outputs.layout = "by_cell"—artifacts/<notebook-stem>/<cell-name>/<name>.<ext>. Every artifact’s path names its producer, which agents and human reviewers can read at a glance without opening the manifest.
The max_committed_size_mb threshold drives a post-run warning from
jellycell run when any single artifact exceeds the limit — pointing
at either .gitignore or Git LFS. See the large-data
example for the “commit the story, git-ignore the bulk” workflow.
[journal]¶
Append-only per-run log written to <manuscripts>/<path> after every
jellycell run. Captures timestamp, notebook, cell summary, new
artifacts (with their captions, when present), any large-artifact
warnings, and the optional -m "message" note. Append-only from
jellycell’s side so hand-edits survive future runs.
[journal]
enabled = true # default; set false to skip the trail
path = "journal.md" # relative to paths.manuscripts
The journal is intentionally committed for real projects — it’s the
analysis trajectory a reviewer (or you in six months) can scan to
understand “why did the numbers change between runs?” Set
enabled = false only when the project is truly transient.
[lint]¶
Rules with a policy gate. Rules without a gate (like pep723-position) always
run.
[lint]
enforce_artifact_paths = true # flag jc.save outside paths.artifacts
enforce_declared_deps = false # flag jc.step cells missing deps=
warn_on_large_cell_output = "10MB" # warn when a cell's cached output exceeds this
File-scope overrides¶
A notebook’s PEP-723 block can override any field at file scope via a
[tool.jellycell] table:
# /// script
# requires-python = ">=3.11"
# dependencies = ["pandas"]
#
# [tool.jellycell]
# timeout_seconds = 1800
# ///
The block-scoped value wins for that file. Other [tool.*] tables are
preserved unchanged.
The .jellycell/ directory¶
Auto-created on first run. Usually git-ignored (jellycell’s own .gitignore
template excludes it).
.jellycell/
└── cache/
├── blobs/ # diskcache-backed content-addressed blob store
├── manifests/ # <cache-key>.json per cell execution
├── artifacts-index/ # reverse index: artifact sha → producing cell(s)
└── state.db # SQLite catalogue (derived; rebuilt on demand)
Filesystem is the source of truth. jellycell cache rebuild-index re-scans
manifests if the SQLite index gets corrupted or out of sync.
Project discovery¶
jellycell walks up from the current directory looking for jellycell.toml.
Override with --project /path/to/root.
Tooling¶
Git: commit
notebooks/,data/(small files; use LFS or external storage for large),artifacts/if they’re outputs worth reviewing,jellycell.toml. Git-ignore.jellycell/andsite/.pre-commit:
jellycell lintfits cleanly as a pre-commit hook.CI: run
jellycell run notebooks/*.pyto refresh the cache on PR.