Running multiple pi sessions against the same vault — for example, autonomous agent fleets spawning sessions in parallel — or having Obsidian open while a background distill is in flight would race on file writes. pi-napkin uses git worktrees to make concurrent distillation safe: each auto-distill invocation gets its own isolated branch and checkout, writes are routed to that isolated copy, and the agent itself merges everything back toDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/cad0p/pi-napkin/llms.txt
Use this file to discover all available pages before exploring further.
main before cleaning up.
Why Worktrees?
Without isolation, any concurrent writer — a second pi session, a scheduled distill, or Obsidian’s autosave — can clobber in-progress vault edits. A merge or file write mid-distill produces either a corrupted note or a lost commit. Worktrees solve this by giving each distill its own complete checked-out copy of the vault that is fully disconnected from the working tree you (and Obsidian) see. git tracks which branches are live in which worktrees and refuses to check out the same branch twice, so two distills that fire in the same second always land on separate branches in separate directories.Worktree Per Distill
Every auto-distill invocation follows a structured lifecycle that creates, uses, and then destroys an isolated workspace.Create branch and worktree
A unique branch named
distill/<6-hex-nonce>-<epoch-seconds> is created via generateDistillBranchName(), and a corresponding worktree is checked out under $XDG_CACHE_HOME/napkin-distill/<vault-hash>/<branch-suffix>/ (typically ~/.cache/napkin-distill/…). <vault-hash> is sha256(contentPath).slice(0, 16), computed after resolving symlinks via fs.realpathSync. The worktree directory is created outside the vault — see Why Worktrees Live Outside the Vault for the rationale.Detect the default branch
detectDefaultBranch() probes the vault in order: (1) git symbolic-ref refs/remotes/origin/HEAD — when origin is configured, its HEAD points at the conventional default; (2) git symbolic-ref --short HEAD — for local-only vaults the currently checked-out branch is the default; (3) fall back to "main". The result is passed to the wrapper so git merge <default> inside the worktree targets the vault’s actual mainline.Spawn the wrapper at the parent's cwd
The wrapper is invoked as
bash <DISTILL_WRAPPER_SCRIPT> with eleven positional arguments: vault, worktreePath, branchName, sessionForkPath, prompt, errorDir, model, defaultBranch, parentCwd, maxDurationSecs, and cacheRoot. The process is spawned with cwd set to the parent session’s working directory, not the worktree. This keeps the system prompt’s Current working directory: line byte-identical to the parent’s, preserving Anthropic-style prompt-cache hits across the spawn.Route vault writes via a per-distill shim
A napkin shim is installed at
<worktree>/.napkin/distill/bin/napkin and prepended to PATH. Every napkin invocation from inside the distill subprocess transparently receives --vault <worktree>, routing all vault reads and writes to the isolated copy instead of the live vault.Merge, squash, push, and clean up
After distillation is complete, the agent merges the default branch into the distill branch, squash-merges the result back onto default, pushes to origin (if configured), and removes the worktree and branch. The wrapper post-validates the result and writes an outcome sidecar.
Why Worktrees Live Outside the Vault
Worktrees under~/.cache/napkin-distill/ rather than inside the vault avoids four categories of problems:
- Cloud-sync pollution — OneDrive, Dropbox, and similar services do not respect
.gitignore. In-vault worktrees would upload tens of megabytes of duplicated vault content per distill spawn. - Filesystem-walker pollution — Obsidian plugins and tools like
finddescend into every subdirectory. N concurrent distills would surface N full vault copies to every walker, degrading indexing performance and triggering spurious re-indexes. - Autocommit-cron noise — Gitlinks can surface in
git statusoutput under certain command sequences when worktrees are nested inside a repository. External placement eliminates this surface entirely. - Safety escape hatch —
rm -rf ~/.cache/napkin-distill/<hash>/is always safe to run. Anything valuable is already committed tomainor was never going to commit in the first place.
Where Worktrees Live
The path pattern for all distill worktrees for a given vault is:<vault-hash> is sha256(contentPath).slice(0, 16), computed after resolving symlinks via fs.realpathSync. This means:
- Worktrees from different vaults never collide under the same cache directory.
- The hash is stable for a given vault path, so the directory is predictable and inspectable.
- Equivalent paths that differ only in symlink resolution (e.g.
~/workplace→/workplace/user) map to the same cache directory.
How Conflicts Are Resolved
pi-napkin uses an agent-driven merge architecture. The distill agent owns the complete lifecycle rather than delegating conflict resolution to a separate merge driver. The full flow is:- The wrapper sets up the worktree, installs the per-distill napkin shim, and spawns a single
pi -p $PROMPTinvocation undertimeout ${maxDurationMinutes}m. - The agent distills content into the worktree, then runs
git -C <worktree> merge --no-edit <default>. If conflicts surface, the agent edits each conflicted file in place using its conversation history as context. - The agent squash-merges the distill branch into the vault’s default branch (
git -C <vault> merge --squash <distill-branch>thengit commit). - The agent pushes to
origin/<default>if origin is configured. On non-fast-forward failures it recovers withgit pull --no-rebase origin <default>then re-pushes. The agent never uses--forceor--force-with-lease. - The wrapper post-validates: no conflict markers in tracked
*.mdfiles, vault HEAD on default branch, and push (if attempted) did not rewrite shared history. It writes an outcome sidecar and force-cleans the worktree and distill branch.
main.
Branch Naming
Distill branches follow the patterndistill/<6-hex-nonce>-<epoch-seconds>. The six-character hex nonce prevents collisions when two distills fire in the same second, which is otherwise a latent race that pure timestamps can’t prevent. Example: distill/a1b2c3-1715198400.
Inspecting Active Distills
Use/distill-status to inspect active distill processes and any unmerged branches lingering from previous runs. If you need to forcibly clear all distill state for a vault:
main or was never going to commit.
Linear history: every successful distill lifecycle produces exactly one squash commit on
main, with a one-line summary the agent generates from the distill content. This keeps vault history clean and makes it straightforward to undo a bad distill with a single command: