Notebook
Project trail system. Initialize, save, recover, migrate, and manage project notes.
Why this exists
You are stateless. Every conversation ends and your context dies. The next agent that opens this project starts from zero — re-reads code, re-discovers constraints, re-makes mistakes you already made. The human becomes a context shuttle between amnesiac agents.
Notebook makes the project remember so you don't have to. When you save a note, you're not journaling — you're leaving a breadcrumb for a future version of yourself that has no idea what happened here. Every failure you record is a loop you prevent. Every constraint you document is a dead end the next session skips. Every decision you capture is an argument that never gets re-litigated.
The index and lessons files are designed to be fast to scan — a future agent can recover full project context in seconds instead of minutes of re-exploration. This is the difference between an agent that compounds knowledge across sessions and one that starts over every time.
Save aggressively. The cost of a note you didn't need is near zero. The cost of a note you didn't write is another wasted session.
When NOT to use this skill
- No active project context (no git repo, no project root) — say so, don't create a floating notebook
- User wants to summarize or extract knowledge from content — use
/eatinstead - Notes already exist and user just wants to read them — read directly with Read tool, skip the skill
Quick Reference
Proactive save rule: Save after every significant decision, failure, or constraint discovered — without being asked. Every ~20 tool calls without a save is too long. If you would say "I should remember this," write it now.
| Command | Action |
|---|---|
/notebook |
Init (new project) or status (existing) |
/notebook save |
Sweep the WHOLE conversation and write every unsaved insight, each as its own note — never prompts, just writes |
/notebook save resolved: <desc> |
Save a note and resolve the matching lesson |
/notebook recover |
Read index + lessons + flagged notes, summarize state — up the whole chain |
/notebook migrate [path] |
Convert messy notes into notebook format |
/notebook split <dir> |
Give a sub-project its own notebook and move its notes there — proposes, you confirm |
Scope: the notebook tree
Projects nest. An engagement repo holds sub-projects; a monorepo holds packages; a studio holds
builds. Each level can have its own _notebook/, and the right ones should — when building
is genuinely happening inside a sub-project, its trail belongs with it, not diluted into the
parent's. But a notebook in every folder with a package.json is noise. So: notebooks form a
tree, and a directory earns a node by a test, never by a marker alone.
Discovery — find the chain, not one root
Walking up from cwd, collect every directory with a project marker (_notebook/,
PROJECT_STATE.md, CLAUDE.md, .git/, package.json / Cargo.toml / pyproject.toml /
go.mod). That ordered list is the chain — e.g. gradient-lab → ego → catcatcat. Status
prints it. Every save names the notebook it wrote to, in one line.
Hard negatives — never a notebook here
Checked before anything else:
- a nested foreign git repo — a
.git/below the current root whose remote is not the user's own identity (a client's repo checked out inside an engagement). Nothing of ours goes in there, not even a marker; its notes belong to the parent. node_modules/, vendored or generated code, build output.- any directory holding a
.no-notebookfile (the explicit opt-out, for places we do own).
The earning test — when a candidate gets its own notebook
A marker makes a directory a candidate. It becomes a notebook when it passes ONE of:
- Own build loop. It has its own dev server / run command / tests and its own source tree —
building happens separately here. A
docs/orscripts/folder does not pass. This is the only test that may create a notebook without asking: when it is unambiguous, the first save inside the sub-project initializes its notebook and says so in the output. - Accumulation. The nearest existing parent notebook already holds ≥ 8 notes about this
directory — its path or name in the filename or body. That is the objective sign the split
is overdue. This test never creates anything by itself; it recommends
/notebook split(see below), unprompted, every time it fires. - The user says so —
/notebookrun inside the directory, or/notebook split <dir>.
A candidate that passes none writes to the nearest existing notebook up the chain.
Placement — which notebook a note goes in
Save the note in the smallest notebook whose scope contains the insight, where "contains" is concrete:
- about code, behaviour, or the working process under that directory → that notebook
- about shared infra, the parent's architecture, people, the engagement, or anything a sibling would need → the parent
- when unsure, prefer the narrower scope — notes promote up more easily than they demote down
⚑ A parent notebook existing is never a reason to write there. The habit that breaks the tree is "the parent's notebook is right there, so I'll use it." If the sub-project passes the earning test and has no notebook yet, initialize it in the same save (see Initializing _notebook/) — and say so.
Numbering across the tree
Each notebook keeps its own sequence. A notebook born from a parent that already holds its
notes continues the parent's numbering (the parent is at 0265 → the child starts at 0266), so
notes moved across keep their numbers and the trail still reads as one timeline. A notebook born
empty starts at 0001. Cross-notebook references carry the notebook's name: ego 0260.
Author Identity
Notebooks are shared. Every note records who wrote it, so a future session can tell whose context it's reading and who to ask.
Resolving the handle — first match wins, resolve once per session and reuse:
git config user.email→ take the local-part (before@) →jon@studio.devbecomesjon- No email configured →
git config user.name→ first whitespace-separated token - Neither available →
unknown
Run these from the project root you chose in Step 1, not from cwd. Git identity is frequently path-scoped — an includeIf "gitdir:..." rule in ~/.gitconfig assigns different identities to different directory trees, and many people set no global identity at all so that a misconfigured repo fails loudly instead of committing under the wrong name. A git config call made outside the repo silently misses those rules and returns the wrong handle or nothing.
Slugify the result: lowercase, non-alphanumeric characters to -. Write it as @handle in prose and index lines, bare handle in filenames.
Never fall back to $USER, whoami, or the hostname. Those are machine identity, not project identity, and they leak local usernames into notes that may be committed.
Never prompt for identity. If resolution lands on unknown, write the note anyway and stamp it @unknown — a note that exists beats a note that blocked on a question.
Always stamp, including solo projects. An unstamped line is ambiguous forever — it could mean "solo" or "identity unresolved" — and the moment a solo project gains a second person, its whole history becomes unattributable. Stamping costs a few characters.
Notes written before this convention stay as they are. Notes are append-only; don't rename or backfill existing files.
Initializing _notebook/
Shared procedure used by both /notebook init and /notebook save (when no notebook exists at the chosen scope).
- Create the directory and three files:
_notebook/ ├── README.md ├── _index.md └── lessons.md README.md:# Project Notebook This directory contains project memory for AI agents. Read `_index.md` first for full project context, then `lessons.md` for known pitfalls._index.md:# Notes Index Project trail — read this first on context recovery. Format: `- NNNN @author type: summary` — ⚑ marks notes every session must read.lessons.md:# Lessons What not to do. Read this every session.- Gitignore based on repo visibility: run
gh repo view --json visibility -q '.visibility'from the project root.PUBLIC→ add_notebook/to.gitignorePRIVATEorINTERNAL→ do not gitignore (notes are safe to track)- Command fails (no remote, no
gh, not a GitHub repo) → gitignore (safe default — assume public until proven private)
/notebook — Init & Status
If _notebook/ doesn't exist (new project):
- Locate project root (see Scope for nested-project handling). If no project root found, tell the user "No project root detected — create a project first."
- Run Initializing _notebook/ at the chosen root.
- Confirm:
- If gitignored:
Notebook initialized at {project_root}/_notebook/ — gitignored (public repo). - If tracked:
Notebook initialized at {project_root}/_notebook/ — tracked in git (private repo).
- If gitignored:
- Retroactive save — scan the current conversation for any decisions, failures, constraints, or learnings that have already occurred. Save each one as a note immediately. If the notebook is being initialized mid-session, there is almost certainly unsaved context. Do not wait to be asked.
If _notebook/ already exists (status):
- Read
_index.md - Read
lessons.md - Report: number of notes, number of lessons, last note date, count of ⚑-flagged notes, and — when the index holds more than one handle — the note count per author
- Show the last 5 index entries
- Print the chain (
gradient-lab → ego → catcatcat, marking which levels have a notebook) and run the accumulation test over this notebook's sub-directories: any candidate with ≥ 8 notes about it gets a one-line recommendation — "14 notes here are aboutexperiments/gradient-lab— run/notebook split experiments/gradient-lab?" The user never has to notice the drift themselves; only the move waits for a yes. - Visibility check: if
_notebook/is gitignored but repo is private (or vice versa), mention it once — e.g._notebook/ is gitignored but this is a private repo — you can remove it from .gitignore to track notes in git.Don't modify.gitignoreautomatically for existing projects.
/notebook save — Write a Note
Never prompt. Never ask questions. Just write.
Step 1: Locate Project Root & Choose Scope
Build the chain walking up from cwd (see Scope: the notebook tree). Apply the hard negatives, then the earning test to the nearest candidate: if it passes the own-build-loop test and has no notebook, this save initializes one there. Then place the note by the placement rule — narrowest scope that contains the insight. If the note lands in a parent and the accumulation test fires for a sub-directory, append the split recommendation to the output.
No root found → "No project root detected. Run /notebook from within a project first."
Step 2: Ensure Infrastructure
If _notebook/ doesn't exist at the chosen scope, run Initializing _notebook/ silently.
Step 3: Next Note Number
Read _index.md, find highest note number, increment. Start at 0001 if empty.
Duplicate numbers are legal and expected. Two people working on separate branches will both take the next number — the author handle in the filename keeps the files distinct, so both survive a merge and neither _index.md line collides. The number means "roughly when," the handle means "who."
Never renumber to remove a duplicate. Numbers are referenced by other notes and by resolved lessons; renumbering silently breaks those links. When numbers repeat, cite them as 0047-jon to disambiguate.
Step 4: Infer Content — ALWAYS a full-conversation sweep
A save is never just one note. Every /notebook save scans the ENTIRE conversation so
far for every decision, failure, constraint, pivot, and learning not yet in the notebook,
and writes EACH ONE as its own note. Check _index.md for what is already recorded; save
everything relevant that isn't.
Priority order for the sweep's focus:
- Explicit argument —
/notebook save constraint: RLS can't do cross-schema joinswrites that note FIRST, then still sweeps the rest of the chat for unsaved insights - Bare
/notebook save— pure sweep: the whole chat, every unsaved insight - Never skip an insight because it feels minor — accidents, wrong turns, and corrections are exactly what future sessions need
Step 5: Classify Type
Infer from content. Never ask.
| Type | Signal |
|---|---|
decision |
Chose between options, committed to approach |
learning |
Non-obvious gotcha, unexpected behavior |
constraint |
Hard limitation (library, API, platform) |
pivot |
Changing direction from previous approach |
investigation |
Research findings, benchmarks, comparisons |
failure |
Tried something, didn't work — and why |
resolved |
A previous lesson/constraint no longer applies |
Default: learning.
Step 6: Write the Note
File: _notebook/NNNN-handle-type-short-title.md
The author handle sits between the number and the type (see Author Identity) — 0047-jon-constraint-rls-cross-schema.md.
Quick format (default):
# NNNN: Short Descriptive Title
**Type:** type | **Date:** YYYY-MM-DD | **By:** @handle
Content. 1-5 lines. Be specific — library names, error messages, version numbers.
What happened, what it means going forward.
Full format (major architectural decisions only):
# NNNN: Short Descriptive Title
**Type:** decision | **Date:** YYYY-MM-DD | **By:** @handle
**Status:** accepted
## Context
What prompted this? (1-3 sentences)
## Decision
What we decided. Be specific.
## Rationale
Why this over alternatives.
## Alternatives Considered
- **Alternative A:** Rejected because X
## Consequences
What this means going forward.
Threshold: meaningful alternatives considered AND hard to reverse → full format. Everything else → quick.
Step 7: Update Index
Append a rich one-line summary to _index.md:
- NNNN @handle type: Detailed one-liner useful standalone
⚑ flag for critical constraints, breaking discoveries, architectural decisions:
- NNNN @handle ⚑ constraint: Critical thing every session must know
The handle holds a fixed position — always after the number, always before the ⚑ and type. Attribution is scanned as a column, so it must not drift to a ragged offset. Keeping it fixed also makes grep '@ocean' _index.md return exactly that person's trail.
Step 8: Update Lessons (conditional)
For types failure, learning, constraint: append a distilled one-liner to lessons.md.
Format — pure signal, no metadata:
- Concise lesson. What not to do and why, or what to do instead.
The full note has context and details. The lesson is the takeaway — one line a model can scan in a second.
For type resolved: write the note as usual, then strike the matching lesson. See Resolving Lessons for matching rules and format.
Step 9: Update PROJECT_STATE.md (conditional)
Only if the note materially changes project state (blocking constraint, architecture change, pivot). If no PROJECT_STATE.md exists, skip.
Output
One confirmation line per note saved, with the notebook's path so the scope is visible:
Saved: experiments/gradient-lab/_notebook/NNNN-handle-type-short-title.md
Saved: experiments/gradient-lab/_notebook/NNNN-handle-type-other-title.md
Plus, only when it applies: one line saying a notebook was initialized, or one split recommendation. Nothing else.
/notebook recover
- Read full
_notebook/_index.md - Read full
_notebook/lessons.md - Open all ⚑-flagged notes in
_notebook/ - Open the last 3 notes in
_notebook/ - Read
PROJECT_STATE.mdif it exists - Read up the chain: for the parent notebook, its
lessons.mdand its ⚑ notes in full; for anything above that, the index only. Work inside a sub-project is still bound by the parent's rules (people, gates, identity walls) and those live one level up. - Summarize: what the project is, key decisions, active constraints, known pitfalls, current state
- If the index carries more than one author handle, add one line naming who has been working here and who wrote the most recent notes — it tells the user whose context they're missing and who to ask
- Ask: "What's changed since this was last updated?"
/notebook migrate
Convert existing messy notes into notebook format. One-time onboarding for projects with pre-existing notes.
- Ensure
_notebook/infrastructure exists at the chosen scope (see Initializing _notebook/) - Scan the target folder (cwd or specified path) for markdown and text files
- Read each file
- Classify each: decision, learning, constraint, failure, investigation, or pivot
- Attribute each — see Migrated authorship below
- Write each as a properly numbered notebook note in
_notebook/ - Build
_index.mdfrom scratch with rich one-line summaries - Extract lessons from anything that looks like a failure, constraint, or learning → populate
lessons.md - Leave originals untouched. User deletes them when satisfied.
If _notebook/ already has notes, continue numbering from the highest existing. Migrate never overwrites existing notes.
After migration, report: how many files found, how many converted, how many lessons extracted, and how many landed as @unknown. Show the generated index.
Migrated authorship
Never stamp yourself as the author of migrated notes. Migration is a bulk mechanical action over someone else's thinking, and the stamp is permanent — a wrong one is worse than no attribution, because it reads as fact to every future session.
Determine the author per source file, first match wins:
- An explicit byline in the file itself (
by: ocean, a signature, an author field) git log --format='%ae' -- <source-file>→ most frequent email → local-part. Pre-existing notes are usually tracked, unlike_notebook/itself, so this often works- Otherwise
unknown
Add a provenance line to the body of every migrated note so the thin attribution is visible rather than implied:
*Migrated from `docs/scratch/auth-notes.md` on YYYY-MM-DD.*
/notebook split
Give a sub-project its own notebook and move its notes out of the parent. Deliberate, never automatic — the recommendation is automatic (status and saves fire it unprompted when the accumulation test passes); the move waits for a yes, because moving notes is the one step that is hard to undo cleanly.
- Confirm
<dir>is under the current notebook's root and passes the hard negatives. - Initialize
_notebook/in<dir>(see Initializing _notebook/), continuing the parent's numbering (see Numbering across the tree). - Propose the move: list every parent note whose filename or body names
<dir>'s path or name, one line each — number, title, and the line that matched. Also list near-misses (matched only by a common word) separately so the user can pull them in. Stop and wait for the user's yes / edits. Never move on inference alone. - Move the confirmed notes — numbers, filenames, contents untouched. Engagement-scoped notes in the same number range stay.
- Carry lessons as copies: every parent lesson whose subject is the sub-project goes into
the child's
lessons.mdunder a dated "carried from …" comment; the parent keeps its copy. - Rebuild both indexes (the parent's by its own script if it has one). Write the child's README to say where the notes came from; add the child to the parent's README.
- Report: how many moved, how many stayed, how many lessons carried, and the child's path.
Resolving Lessons
To mark a lesson as resolved use: /notebook save resolved: <exact phrase or lesson number>
Matching rules:
- Prefer exact phrase match from lessons.md
- Otherwise match on key terms (library names, error descriptions, the constraint being resolved)
- If multiple lessons could match, resolve the best one and state which lesson was resolved in the output
- If no match at all, write the note but warn:
Note saved but no matching lesson found to resolve.
Strike the lesson in lessons.md and reference the note that resolved it, by number and author handle:
~~Prisma doesn't work with edge runtime~~ *(resolved: fixed in Prisma 6.0 — see 0047-jon)*
Strikethrough preserves history — a future model sees what used to be a problem and knows to skip it. The note reference lets anyone find the resolution details, and the handle survives the case where two branches both produced an 0047.
Lessons themselves stay unstamped. They are one line of pure signal read every session; author metadata on the takeaway invites blame-reading and buys nothing the resolving note reference doesn't already give you.
Archiving: When lessons.md exceeds 50 resolved (struck-through) entries, move all resolved lessons to lessons-archive-YYYY-MM.md and remove them from the active file. Keep only open lessons in the active file.
Stale Lesson Detection
When reading lessons.md during context recovery, check if any lessons are outdated — library upgrades, removed constraints, resolved workarounds. Flag to the user: "Lesson [X] may be stale — [reason]. Want me to resolve it?"
Context Recovery Priority
On any new session or /notebook recover, read in this order:
_notebook/_index.md— what is this project, what happened_notebook/lessons.md— what not to do- ⚑-flagged notes — critical details the index can't capture
- Last 3 notes — recent context
PROJECT_STATE.md— current snapshot (if exists)
Rules
- Never prompt on /notebook save. Infer everything from context and arguments.
- Quick format by default. Full format is rare — major architectural decisions only.
- Rich index entries. The index alone should be sufficient for context recovery.
- Be specific. Library names, error messages, versions > vague descriptions.
- Failures are gold. "Tried X, failed because Y" prevents loops. Always save these.
- One insight per note; every insight per save. A save sweeps the whole conversation and writes every unsaved insight as its own file — it is never just one note. Granular files keep lessons greppable; the sweep keeps the record complete (Ocean, 26 Aug 2026).
- ⚑ sparingly. Only for things every future session must read.
- Notes are append-only. New moment = new note. Update existing only to mark superseded.
- Lessons are distilled. One line, pure signal. If you need context, open the note.
- Every note is stamped. Author handle in the filename, the
**By:**field, and the index line — always, solo projects included. Resolve it from git, never prompt for it, never guess. - Check your notes when stuck. Before attempting a workaround or debugging an unexpected failure, read
_notebook/lessons.mdfirst. The answer may already be there. This is the reflex that prevents loops. - A parent notebook existing is never a reason to write there. Place by the tree: the narrowest scope that contains the insight, initializing a sub-project's notebook when it earns one. The failure this prevents: sixty lab notes in an engagement's trail (ego, 27 Aug 2026).
- Recommend splits unprompted; move only on a yes. The accumulation test fires by itself;
/notebook splitproposes the exact list and waits.