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.py does not rewrite the notebook path to live under X/ — the path is resolved against cwd. Use the full path (form A) or a cd (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 outer AGENTS.md and 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 outer AGENTS.md is found. Pass --force only 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:

Target state

Flag needed

No outer, no existing target file

(none)

Outer AGENTS.md exists, no existing inner file

--nested

Existing target file (any scope)

--force

Outer AGENTS.md exists + existing inner file

--nested --force

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 inner AGENTS.md already exists). The outer root AGENTS.md stays 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-written AGENTS.md and write jellycell’s to a sibling AGENTS.jellycell.md that 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 under artifacts/<name>.<ext>. Non-breaking with any existing notebook.

  • layout = "by_notebook" — path-less figures and tables land under artifacts/<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.

manuscripts/ — hand-authored writeups + tearsheets

The manuscripts/ directory holds markdown files that live alongside notebooks. By convention it has a clean two-way split:

manuscripts/
├── README.md              # explains the layout (optional but helpful)
├── paper.md               # hand-authored; you own this
├── reviewer-memo.md       # hand-authored; you own this
└── tearsheets/            # auto-generated; regenerate overwrites
    ├── analysis.md        # = notebooks/analysis.py
    └── exploration.md     # = notebooks/exploration.py
  • Root manuscripts/*.md — hand-authored writeups: paper drafts, thesis chapters, decision memos, reviewer notes. Stable across any tearsheet regeneration. You edit freely; nothing overwrites your work.

  • manuscripts/tearsheets/*.md — produced by jellycell export tearsheet <nb>, which writes manuscripts/tearsheets/<stem>.md by default. Markdown narration + inlined figures (via ../../artifacts/foo.png relative paths) + JSON summaries as two-column tables. Header links back to the source notebook and the rendered HTML report when it exists. Regenerating overwrites the file, so never hand-edit; use the -o PATH override to target somewhere else if you need custom layouts.

Both reference the same artifacts/ tree, so figures in the hand-authored paper and the tearsheet dashboard are always byte-identical to the latest run. Commit manuscripts/ so reviewers and agents see the latest tearsheets + writeups without re-running anything.

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/ and site/.

  • pre-commit: jellycell lint fits cleanly as a pre-commit hook.

  • CI: run jellycell run notebooks/*.py to refresh the cache on PR.