{
  "id": "jupyter-notebooks",
  "type": "loadout",
  "title": "Jupyter notebooks agents can edit",
  "summary": "Notebook rules for agents: edit Jupytext-paired .py files instead of JSON, run top to bottom in a fresh kernel, keep outputs out of git with nbstripout, check data first.",
  "description": "Agents struggle with notebooks for a simple reason: an .ipynb is JSON full of outputs and execution counts, so edits are fragile and diffs unreadable, and a notebook that ran once out of order passes for working. This loadout uses the tools data teams already rely on: Jupytext pairs each notebook with a percent-format .py file that the agent edits, nbstripout keeps outputs out of git, and done means executed from a fresh kernel. It adds reproducibility and data-care rules, and the verifier converts and runs the example notebook.\n",
  "highlights": [
    "Edit the paired .py (Jupytext percent format), never the .ipynb JSON",
    "Done means executed top to bottom in a fresh kernel",
    "nbstripout git filter keeps outputs and counts out of commits",
    "Parameters cell, seeded randomness, relative paths",
    "Data-check cell with asserts before any result"
  ],
  "version": "1.0.0",
  "license": "MIT",
  "author": {
    "name": "Agent Loadout",
    "url": "https://loadout.jnw-it.com"
  },
  "credits": [
    {
      "title": "Jupytext documentation: paired notebooks",
      "url": "https://jupytext.readthedocs.io/en/latest/paired-notebooks.html"
    },
    {
      "title": "kynan/nbstripout",
      "url": "https://github.com/kynan/nbstripout"
    },
    {
      "title": "Jupytext documentation: percent format",
      "url": "https://jupytext.readthedocs.io/en/latest/formats-scripts.html"
    }
  ],
  "categories": [
    "data"
  ],
  "stacks": [
    "python",
    "jupyter",
    "uv"
  ],
  "files": [
    {
      "path": "AGENTS.md",
      "role": "instructions",
      "description": "Notebook editing, execution, reproducibility and data-care rules.",
      "skill": null,
      "agent": null,
      "install_to": {
        "claude-code": "AGENTS.md",
        "codex": "AGENTS.md",
        "cursor": "AGENTS.md",
        "gemini-cli": "AGENTS.md",
        "antigravity-cli": "AGENTS.md",
        "copilot": "AGENTS.md"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/AGENTS.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/latest/AGENTS.md",
      "bytes": 2143,
      "lang": "markdown",
      "content": "# AGENTS.md: Jupyter notebooks\n\nData and ML work in Jupyter notebooks, managed with uv. Notebooks are paired with plain Python files by Jupytext and\ncommitted without outputs.\n\n## Editing notebooks\n- Edit the paired `.py` file (Jupytext percent format, cells start with `# %%`), not the `.ipynb` JSON. Then sync:\n  `uv run jupytext --sync notebooks/<name>.py`. Never hand-edit cell JSON, outputs or execution counts.\n- New notebook: write `notebooks/<name>.py` in percent format and pair it:\n  `uv run jupytext --set-formats ipynb,py:percent notebooks/<name>.py`.\n- Keep outputs out of git: `nbstripout` is installed as a git filter (`uv run nbstripout --install`). Do not remove it.\n\n## Done means it runs top to bottom\n- Execute from a fresh kernel before reporting: `uv run jupytext --to ipynb --execute notebooks/<name>.py`\n  (or `uv run jupyter nbconvert --to notebook --execute --inplace notebooks/<name>.ipynb`).\n- A notebook that only works after running cells out of order is broken. Fix the order; no hidden state.\n- Report the key numbers or the figure you produced, not just \"it ran\".\n\n## Reproducibility\n- Dependencies in `pyproject.toml` via `uv add`; no `!pip install` cells. Dev tools: `uv add --dev jupytext nbconvert\n  ipykernel nbstripout` (executing needs nbconvert and ipykernel).\n- One parameters cell near the top (paths, dates, sample sizes, seeds). No magic numbers further down.\n- Seed every source of randomness (`numpy.random.default_rng(seed)`, framework seeds) and say so in the notebook.\n- Paths relative to the project root; raw data in `data/raw/` is read-only and gitignored. Write outputs to\n  `data/processed/` or `reports/`.\n- Long training or heavy queries: move the code into a module in `src/` and import it; keep the notebook for narrative,\n  checks and plots.\n\n## Data care\n- Look at the data before modelling: shape, dtypes, missing values, duplicates, a few rows. Use the\n  `notebook-change` skill.\n- Never print or commit personal data, credentials or full raw tables. Use `.head()` on synthetic or anonymised data.\n- Split train and test before any fitting or scaling. Never tune on the test set.\n"
    },
    {
      "path": "CLAUDE.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Claude Code.",
      "skill": null,
      "agent": "claude-code",
      "install_to": {
        "claude-code": "CLAUDE.md",
        "codex": null,
        "cursor": null,
        "gemini-cli": null,
        "antigravity-cli": null,
        "copilot": null
      },
      "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/CLAUDE.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/latest/CLAUDE.md",
      "bytes": 386,
      "lang": "markdown",
      "content": "# CLAUDE.md\n\n@AGENTS.md\n\n## Claude Code notes\n- Skills from this loadout live in `.claude/skills/`: `notebook-change`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From the Agent Loadout \"Jupyter notebooks agents can edit\" v1.0.0 — https://loadout.jnw-it.com/loadouts/jupyter-notebooks/ -->\n"
    },
    {
      "path": "GEMINI.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Gemini CLI.",
      "skill": null,
      "agent": "gemini-cli",
      "install_to": {
        "claude-code": null,
        "codex": null,
        "cursor": null,
        "gemini-cli": "GEMINI.md",
        "antigravity-cli": null,
        "copilot": null
      },
      "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/GEMINI.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/latest/GEMINI.md",
      "bytes": 387,
      "lang": "markdown",
      "content": "# GEMINI.md\n\n@./AGENTS.md\n\n## Gemini CLI notes\n- Skills from this loadout live in `.agents/skills/`: `notebook-change`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From the Agent Loadout \"Jupyter notebooks agents can edit\" v1.0.0 — https://loadout.jnw-it.com/loadouts/jupyter-notebooks/ -->\n"
    },
    {
      "path": "skills/notebook-change/SKILL.md",
      "role": "skill",
      "description": "Edit via the paired .py, run fresh, strip outputs.",
      "skill": "notebook-change",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/notebook-change/SKILL.md",
        "codex": ".agents/skills/notebook-change/SKILL.md",
        "cursor": ".agents/skills/notebook-change/SKILL.md",
        "gemini-cli": ".agents/skills/notebook-change/SKILL.md",
        "antigravity-cli": ".agents/skills/notebook-change/SKILL.md",
        "copilot": ".agents/skills/notebook-change/SKILL.md"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/SKILL.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/latest/skills/notebook-change/SKILL.md",
      "bytes": 1306,
      "lang": "markdown",
      "content": "---\nname: notebook-change\ndescription: Change or add a Jupyter notebook through its Jupytext-paired .py file, check the data first, run it top to bottom in a fresh kernel and keep outputs out of git. Use for any notebook work.\n---\n\n# Notebook change\n\n1. Find the paired `.py` file for the notebook (`jupytext --paired-paths notebooks/<name>.ipynb` if unsure). If the\n   notebook is not paired yet, pair it first and commit that as its own change.\n2. Read the parameters cell and the cells you will change. Edit the `.py` file only, keeping `# %%` cell markers and\n   `# %% [markdown]` cells for narrative.\n3. When data is involved, add or keep a check cell like the one in `references/analysis.py`: shape, dtypes, missing\n   values and duplicates, with `assert` lines for what the rest of the notebook relies on.\n4. Sync and execute from a fresh kernel: `uv run jupytext --sync notebooks/<name>.py` then\n   `uv run jupytext --to ipynb --execute notebooks/<name>.py`. Fix any error and run again from the top.\n5. Check git sees no outputs: `git diff --stat` shows the `.py` and a small `.ipynb` change; if outputs appear, run\n   `uv run nbstripout notebooks/<name>.ipynb` and check the filter is installed.\n6. Report: what changed, the key result (numbers or a figure described in words), and the run time.\n"
    },
    {
      "path": "skills/notebook-change/references/analysis.py",
      "role": "reference",
      "description": "Percent-format example with parameters and a data check.",
      "skill": "notebook-change",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/notebook-change/references/analysis.py",
        "codex": ".agents/skills/notebook-change/references/analysis.py",
        "cursor": ".agents/skills/notebook-change/references/analysis.py",
        "gemini-cli": ".agents/skills/notebook-change/references/analysis.py",
        "antigravity-cli": ".agents/skills/notebook-change/references/analysis.py",
        "copilot": ".agents/skills/notebook-change/references/analysis.py"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/references/analysis.py",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/latest/skills/notebook-change/references/analysis.py",
      "bytes": 1237,
      "lang": "markdown",
      "content": "# ---\n# jupyter:\n#   jupytext:\n#     formats: ipynb,py:percent\n#   kernelspec:\n#     display_name: Python 3\n#     language: python\n#     name: python3\n# ---\n\n# %% [markdown]\n# # Weekly orders: example analysis\n# Parameters first, a data check, then the result. Runs top to bottom from a fresh kernel.\n\n# %% tags=[\"parameters\"]\nSEED = 42\nN_ORDERS = 500\nWEEKS = 8\n\n# %%\nimport random\nimport statistics\n\nrng = random.Random(SEED)\norders = [{\"week\": rng.randrange(WEEKS), \"pence\": rng.randint(200, 5000)} for _ in range(N_ORDERS)]\n\n# %% [markdown]\n# ## Data check\n# Shape, missing values and ranges that the rest of the notebook relies on.\n\n# %%\nassert len(orders) == N_ORDERS, \"unexpected row count\"\nassert all(o[\"pence\"] is not None for o in orders), \"missing values\"\nassert all(0 <= o[\"week\"] < WEEKS for o in orders), \"week out of range\"\nprint(f\"rows: {len(orders)}, weeks: {len({o['week'] for o in orders})}\")\n\n# %%\nby_week = {w: [o[\"pence\"] for o in orders if o[\"week\"] == w] for w in range(WEEKS)}\nsummary = {w: (len(v), statistics.median(v)) for w, v in by_week.items()}\nbusiest = max(summary, key=lambda w: summary[w][0])\nprint(f\"busiest week: {busiest} with {summary[busiest][0]} orders, median £{summary[busiest][1] / 100:.2f}\")\n"
    }
  ],
  "requires": {
    "tools": [
      "uv"
    ],
    "env": [],
    "mcp": []
  },
  "tested": {
    "status": "untested",
    "last_run": null,
    "fixture": null,
    "per_agent": {
      "claude-code": "untested",
      "codex": "untested",
      "cursor": "untested",
      "gemini-cli": "untested",
      "antigravity-cli": "untested",
      "copilot": "untested"
    }
  },
  "security": {
    "scanned": true,
    "scanner": "intake-precheck/1",
    "verdict": "clean",
    "network": true,
    "scripts": [],
    "notes": "uv downloads Jupytext, nbconvert, ipykernel and nbstripout from PyPI. The example notebook makes no network calls."
  },
  "featured": false,
  "deprecated": false,
  "last_checked": "2026-10-17",
  "published": "2026-10-17",
  "updated": "2026-10-17",
  "compatibility": {
    "claude-code": "native",
    "codex": "native",
    "cursor": "native",
    "gemini-cli": "native",
    "antigravity-cli": "native",
    "copilot": "native"
  },
  "counts": {
    "rules": 1,
    "skills": 1,
    "mcp": 0
  },
  "url": "https://loadout.jnw-it.com/loadouts/jupyter-notebooks/",
  "api_url": "https://loadout.jnw-it.com/api/loadouts/jupyter-notebooks.json",
  "verification": {
    "level": "verified-install",
    "status": "verified",
    "label": "Verified install",
    "verified": true,
    "checked_at": "2026-10-17T08:09:30Z",
    "commit": "03c1b9839c40eb1e44c0a2877baaa3978a96895c",
    "content_current": true,
    "agent_tests": "not-run",
    "counts": {
      "pass": 9,
      "fail": 0,
      "skip": 0,
      "not_applicable": 1
    },
    "meaning": "Level 1: automated checks on the files (parse, install paths, MCP config, safety scan, references, size, licence, starter build). No AI agent was run.",
    "checks": [
      {
        "id": "listing",
        "name": "Listing",
        "status": "pass",
        "summary": "Schema valid; 5 files declared, none missing or extra",
        "details": [
          "tested.status: untested (agent tests are separate from Level 1)"
        ]
      },
      {
        "id": "parse",
        "name": "Files parse",
        "status": "pass",
        "summary": "All 5 files parse; front matter valid",
        "details": []
      },
      {
        "id": "adapters",
        "name": "Adapters",
        "status": "pass",
        "summary": "CLAUDE.md and GEMINI.md import AGENTS.md and are up to date",
        "details": [
          "CLAUDE.md: matches the generator",
          "GEMINI.md: matches the generator"
        ]
      },
      {
        "id": "install-paths",
        "name": "Install paths",
        "status": "pass",
        "summary": "Correct paths for all 6 agents; layouts re-discovered",
        "details": [
          "Claude Code: 4 files, 1 skill in .claude/skills/",
          "Codex CLI: 3 files, 1 skill in .agents/skills/",
          "Cursor: 3 files, 1 skill in .agents/skills/",
          "Gemini CLI: 4 files, 1 skill in .agents/skills/",
          "Antigravity CLI: 3 files, 1 skill in .agents/skills/",
          "GitHub Copilot: 3 files, 1 skill in .agents/skills/"
        ]
      },
      {
        "id": "mcp",
        "name": "MCP config",
        "status": "n/a",
        "summary": "No MCP servers in this loadout",
        "details": []
      },
      {
        "id": "safety",
        "name": "Safety scan",
        "status": "pass",
        "summary": "Clean: no credentials, risky shell or hidden text",
        "details": [
          "Scanner intake-precheck/1: verdict clean; network yes (listing says yes)"
        ]
      },
      {
        "id": "references",
        "name": "References",
        "status": "pass",
        "summary": "1 reference between files resolves for every agent",
        "details": []
      },
      {
        "id": "size",
        "name": "Size limits",
        "status": "pass",
        "summary": "5 files, 5.3 KB total; largest AGENTS.md 2.1 KB",
        "details": [
          "Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule"
        ]
      },
      {
        "id": "licence",
        "name": "Licence",
        "status": "pass",
        "summary": "MIT, by Agent Loadout; text in the repository LICENSE",
        "details": []
      },
      {
        "id": "starter-build",
        "name": "Starter build",
        "status": "pass",
        "summary": "The notebook-change skill's percent-format example converts to a notebook with Jupytext and executes top to bottom in a fresh kernel; nbstripout removes outputs from a committed notebook",
        "details": [
          "Applied 7 loadout files at every agent's paths",
          "✓ place the example: cp .agents/skills/notebook-change/references/analysis.py notebooks/analysis.py",
          "✓ jupytext: convert and execute from a fresh kernel: uv run --no-project --python 3.12 --with jupytext==1.19.6 --with nbconvert==7.17.2 --with ipykernel==7.4.0 jupytext --to ipynb --execute notebooks/analysis.py",
          "✓ executed notebook has the result: bash -c grep -o 'busiest week: [0-9] with [0-9]* orders' notebooks/analysis.ipynb | head -1",
          "✓ nbstripout removes outputs: bash -c uvx nbstripout@0.9.1 notebooks/with-outputs.ipynb notebooks/analysis.ipynb && python3 check_outputs.py notebooks/with-outputs.ipynb notebooks/analysis.ipynb",
          "✓ ruff on the paired .py: uvx ruff@0.17.1 check --select E,F,I,B --ignore E402 --line-length 120 notebooks/analysis.py",
          "Covers: Percent-format .py as the source of truth, converted by Jupytext",
          "Covers: Runs top to bottom from a fresh kernel with a parameters cell and a seeded random source",
          "Covers: Data-check cell with asserts before the result",
          "Covers: nbstripout leaves no outputs or execution counts"
        ]
      }
    ]
  },
  "install": {
    "claude-code": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "CLAUDE.md",
          "target": "CLAUDE.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/CLAUDE.md",
          "role": "adapter"
        },
        {
          "path": "skills/notebook-change/SKILL.md",
          "target": ".claude/skills/notebook-change/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/notebook-change/references/analysis.py",
          "target": ".claude/skills/notebook-change/references/analysis.py",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/references/analysis.py",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/jupyter-notebooks/claude-code.zip"
    },
    "codex": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/notebook-change/SKILL.md",
          "target": ".agents/skills/notebook-change/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/notebook-change/references/analysis.py",
          "target": ".agents/skills/notebook-change/references/analysis.py",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/references/analysis.py",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/jupyter-notebooks/codex.zip"
    },
    "cursor": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/notebook-change/SKILL.md",
          "target": ".agents/skills/notebook-change/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/notebook-change/references/analysis.py",
          "target": ".agents/skills/notebook-change/references/analysis.py",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/references/analysis.py",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/jupyter-notebooks/cursor.zip"
    },
    "gemini-cli": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "GEMINI.md",
          "target": "GEMINI.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/GEMINI.md",
          "role": "adapter"
        },
        {
          "path": "skills/notebook-change/SKILL.md",
          "target": ".agents/skills/notebook-change/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/notebook-change/references/analysis.py",
          "target": ".agents/skills/notebook-change/references/analysis.py",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/references/analysis.py",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/jupyter-notebooks/gemini-cli.zip"
    },
    "antigravity-cli": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/notebook-change/SKILL.md",
          "target": ".agents/skills/notebook-change/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/notebook-change/references/analysis.py",
          "target": ".agents/skills/notebook-change/references/analysis.py",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/references/analysis.py",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/jupyter-notebooks/antigravity-cli.zip"
    },
    "copilot": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/notebook-change/SKILL.md",
          "target": ".agents/skills/notebook-change/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/notebook-change/references/analysis.py",
          "target": ".agents/skills/notebook-change/references/analysis.py",
          "raw_url": "https://loadout.jnw-it.com/raw/jupyter-notebooks/1.0.0/skills/notebook-change/references/analysis.py",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/jupyter-notebooks/copilot.zip"
    }
  },
  "source_zip_url": "https://loadout.jnw-it.com/download/jupyter-notebooks/source.zip"
}