Skip to main content

Documentation 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.

The public API of pi-napkin consists of the exported functions, interfaces, and classes from the two distill extension modules: extensions/distill/distill-workspace.ts and extensions/distill/auto-setup.ts. These exports are the building blocks the extension factory uses internally, and they are individually testable — the test suite covers them extensively with both unit and integration fixtures. Use them in your own tests, automation scripts, or custom pi extensions that need to interact with napkin’s distill infrastructure without going through the full extension lifecycle.

From extensions/distill/distill-workspace.ts

createDistillWorkspace

function createDistillWorkspace(
  vault: string,
  sourceSessionFile: string,
  parentCwd: string,
): DistillWorkspace
Creates a per-distill git worktree, forks the parent session into it, and writes meta.json.
vault
string
required
Absolute path to the vault’s content root (napkin.vault.contentPath). Used as the input for the vault hash that names the cache directory. Must be a git repo.
sourceSessionFile
string
required
Absolute path to the parent session .jsonl to fork from.
parentCwd
string
required
Absolute path of the parent pi session’s current working directory. Must be absolute and must exist on disk — throws DistillError if either condition is violated. Pinned into the session-fork header so the distill subprocess’s Current working directory: line in the system prompt is byte-identical to the parent’s, preserving prompt-cache hits.
DistillWorkspace
object
Throws DistillError if:
  • vault is not a git repo (.git/ missing)
  • vault has no commits (HEAD unresolvable)
  • git worktree add fails (branch collision, dirty state, etc.)
  • SessionManager.forkFrom does not produce a session file
  • parentCwd is not absolute
  • parentCwd does not exist on disk
Roll-back on failure: if the worktree was created before the error occurred, removeDistillWorktree is called before re-throwing. The function never leaks a dangling worktree.

spawnDistillInWorktree

function spawnDistillInWorktree(opts: SpawnDistillOptions): SpawnDistillResult
Creates a workspace via createDistillWorkspace, builds the distill prompt, and spawns a detached wrapper script that drives the full distill → merge → squash → push → cleanup lifecycle.
export interface SpawnDistillOptions {
  vault: string;           // Absolute path to main vault
  sessionFile: string;     // Absolute path to current session .jsonl
  parentCwd: string;       // Parent pi session's cwd
  model?: string;          // Optional "<provider>/<id>" override
  maxDurationSecs: number; // Hard wall-clock budget for agent (seconds)
  spawnFn?: typeof spawn;  // Override spawn for tests
}
vault
string
required
Absolute path to the vault’s content root.
sessionFile
string
required
Absolute path to the current session .jsonl to fork.
parentCwd
string
required
Parent pi session’s cwd. Used as the spawn cwd so pi’s system prompt cwd line matches the parent’s.
model
string
Optional model override in <provider>/<id> format (e.g. "anthropic/claude-sonnet-4-6"). When absent, the wrapper uses the vault’s configured model.
maxDurationSecs
number
required
Hard wall-clock budget in seconds, wired into the wrapper’s timeout(1) invocation. Derived from distill.maxDurationMinutes (default 600s). Must be a positive integer.
spawnFn
function
Override for child_process.spawn. Used by tests to capture spawn calls without starting real processes.
SpawnDistillResult
object
export interface SpawnDistillResult {
  workspace: DistillWorkspace;
  pid: number;
}
workspace is the handle created by createDistillWorkspace. pid is the PID of the detached wrapper process (-1 if the OS did not assign a PID). The parent must not wait on this process — it is detached and survives parent exit.
Throws DistillError if workspace creation fails. Errors at this layer are unrecoverable — there is no child to clean up because the workspace was rolled back.

cleanupDistillWorkspace

function cleanupDistillWorkspace(
  vault: string,
  workspace: Pick<DistillWorkspace, "worktreePath" | "branchName">,
): void
Removes the worktree and its branch. Idempotent — safe to call on partial state, missing paths, or paths that have already been removed by the wrapper. Used by the JS-side timeout cleanup path.

cleanupStaleWorktrees

function cleanupStaleWorktrees(vault: StaleCleanupVault): number
Sweeps distill worktrees left by crashed pi sessions. Called once per session_start. A worktree is removed if any of the following are true:
  • <wt>/.napkin/distill/meta.json is missing
  • meta.json’s pid is not a running process (process.kill(pid, 0) throws ESRCH)
  • meta.json’s mtime is older than STALE_META_AGE_MS (STALE_WORKTREE_MINUTES * 60 * 1000 = 60 minutes)
Returns the number of worktrees removed. Never throws — per-worktree failures are swallowed so a single bad worktree doesn’t abort the sweep.

resolveCacheRoot

function resolveCacheRoot(vaultContentPath: string): string
Returns the XDG cache directory where this vault’s distill worktrees live:
$XDG_CACHE_HOME/napkin-distill/<sha256(contentPath).slice(0, 16)>/
Falls back to ~/.cache when XDG_CACHE_HOME is unset. Canonicalizes vaultContentPath via fs.realpathSync before hashing so symlink variants of the same path resolve to the same cache directory.

detectDefaultBranch

function detectDefaultBranch(vaultPath: string): string
Detects the vault’s default mainline branch. Resolution strategy (first hit wins):
  1. git symbolic-ref refs/remotes/origin/HEAD — conventional default when a remote origin is configured.
  2. git symbolic-ref --short HEAD — current branch for local-only vaults.
  3. Fallback: "main" — matches git init -b main used by auto-setup.
Never throws. Returns a plain short branch name (no refs/heads/ prefix).

generateDistillBranchName

function generateDistillBranchName(now?: Date, nonceHex?: string): string
Returns a unique branch name for a distill invocation:
distill/<6-hex-nonce>-<epoch-seconds>
The 3-byte nonce prevents collisions when two distills fire in the same second. now and nonceHex are injectable for tests.

getDistillState

function getDistillState(vault: StaleCleanupVault): {
  active: ActiveDistill[];
  unmerged: string[];
}
Combined active + unmerged snapshot in a single git probe pass. Invokes git worktree list --porcelain exactly once (instead of twice for separate active + unmerged calls) and git branch --list 'distill/*' once. Returns { active: [], unmerged: [] } on any error — never throws.

parseWorktreeList

function parseWorktreeList(
  porcelain: string,
): Array<{ path: string; branch: string }>
Parses git worktree list --porcelain output into a list of { path, branch } pairs. Only returns entries that have a branch line (skips detached-HEAD worktrees). The branch value is the short name with refs/heads/ stripped. Exported for unit tests.

getDistillTouchedFilesPostSquash

function getDistillTouchedFilesPostSquash(
  vaultPath: string,
  startSha: string | undefined,
): string[]
Returns files affected by commits in <startSha>..HEAD (i.e., files the distill wrote, after the wrapper’s squash-merge has landed on main). Paths are relative to the vault root. Returns [] if startSha is undefined (pre-Phase-C2 meta files that lack the field) or if git log fails. Overlap detection is best-effort.

findDistillErrorLogForBranch

function findDistillErrorLogForBranch(
  errorDir: string,
  branchShort: string,
): string | null
Searches errorDir for a wrapper-emitted forensic log file matching <timestamp>-<pid>-<branchShort>.log. Returns the absolute path to the most recent matching .log file, or null when none exists. branchShort is the part after distill/ in the branch name. Used by runDistillWith’s success path to detect wrapper failures that don’t surface as a timeout.

findDistillOutcomeForBranch

function findDistillOutcomeForBranch(
  errorDir: string,
  branchShort: string,
): DistillOutcome | null
Searches errorDir for a wrapper-emitted outcome sidecar matching <timestamp>-<pid>-<branchShort>.outcome. Returns a parsed DistillOutcome ({ outcomeClass, outcomePath, recoveryHint }) for the most recent match, or null when none exists. A missing sidecar AND missing error log means abnormal termination (SIGKILL, OOM, set -e). The caller surfaces this as a warning.

resolveDistillErrorDir

function resolveDistillErrorDir(vault: string): string
Returns the absolute path to the distill error/outcome log directory for a given vault: <vault.configPath>/distill/errors/. Falls back to <vault>/.napkin/distill/errors/ if napkin’s vault resolution throws. The returned path may not exist on disk.

Interfaces from distill-workspace.ts

DistillWorkspace

export interface DistillWorkspace {
  worktreePath: string;
  branchName: string;
  sessionForkPath: string;
  metaPath: string;
  startSha?: string;
}
worktreePath
string
Absolute path to the worktree root. This is the cwd for the distill subprocess. Located under $XDG_CACHE_HOME/napkin-distill/<vault-hash>/<branch-suffix>/.
branchName
string
Git branch name created for this distill, e.g. distill/a1b2c3-1715198400.
sessionForkPath
string
Absolute path to the forked session .jsonl inside the worktree (<worktreePath>/.napkin/distill/session.jsonl).
metaPath
string
Absolute path to meta.json inside the worktree (<worktreePath>/.napkin/distill/meta.json).
startSha
string
Vault HEAD SHA at workspace-creation time. Used by per-completion overlap detection. Undefined for repos with no commits at creation time.

DistillMeta

Written to each workspace’s meta.json. Consumed by /distill-status and forensic recovery tooling.
export interface DistillMeta {
  pid: number;
  vault: string;
  branch: string;
  startedAt: string;
  parentSession: string;
  startSha?: string;
}
pid
number
PID of the detached wrapper shell. Initially the parent pi session’s PID; overwritten by the wrapper itself ($$) after it installs its cleanup trap.
vault
string
Absolute path to the main vault (NOT the worktree).
branch
string
Git branch name for this distill, e.g. distill/a1b2c3-1715198400.
startedAt
string
ISO-8601 timestamp when the workspace was created.
parentSession
string
Absolute path to the parent session’s .jsonl. For traceability and forensic recovery.
startSha
string
HEAD SHA at creation time. Absent on pre-Phase-C2 meta files — readers must tolerate undefined.

ActiveDistill

export interface ActiveDistill {
  pid: number;
  branch: string;
  worktreePath: string;
  startedAt: string | null;
  elapsedMs: number;
  sessionPath: string | null;
  alive: boolean;
  startSha?: string;
}
Represents a git worktree currently linked to the vault on a distill/* branch. alive is determined at read time via process.kill(pid, 0).

DistillOutcome

export interface DistillOutcome {
  outcomeClass: string;
  outcomePath: string;
  recoveryHint: string | null;
}
Parsed outcome sidecar written by the wrapper at <vault>/.napkin/distill/errors/<timestamp>-<pid>-<branch>.outcome.
outcomeClass
string
Machine-readable class string (first line of the sidecar). One of merged-content, merged-local, no-content, or failed:<reason>.
outcomePath
string
Absolute path to the sidecar file on disk.
recoveryHint
string | null
Lines 2+ of the sidecar concatenated. Present only for failed:* outcomes; null for happy-path classes.

Key constants from distill-workspace.ts

ConstantValueDescription
GIT_SUBCOMMAND_TIMEOUT_MS30_000 (30 s)Per-subcommand wall-clock ceiling for git invocations from the JS side. Shared with auto-setup.ts via re-export.
STALE_WORKTREE_MINUTES60Minutes past which a worktree with a stale meta.json mtime is considered abandoned by cleanupStaleWorktrees.
STALE_META_AGE_MSSTALE_WORKTREE_MINUTES * 60 * 1000 (3 600 000 ms)Same threshold expressed in milliseconds, used at the mtime comparison site.

From extensions/distill/auto-setup.ts

ensureVaultReadyForDistill

function ensureVaultReadyForDistill(
  vault: SetupVault,
  level: HealthLevel,
  options?: SetupOptions,
): SetupResult
Runs health checks and auto-setup for the vault. Called at every session_start (level: "fast") and again before every worktree-based spawn (level: "full").
vault
SetupVault
required
{ contentPath: string; configPath: string }. For subdir-layout vaults, contentPath is the vault root and configPath is <contentPath>/.napkin/.
level
"fast" | "full"
required
"fast" — file-only checks plus git-init if needed. Target latency ~10 ms; suitable for session_start.
"full" — superset of fast, plus git-state probes, config-tracking checks, cache-root writability, orphan pruning, and stale-branch deletion. Runs before worktree spawn.
options
SetupOptions
Optional dependency-injection seam. Fields:
  • probeWritable?: (dir: string) => WritableProbeResult — override write-probe (tests inject to simulate unwritable cache roots without chmod)
  • now?: () => number — override wall clock (tests inject to pin stale-branch boundary cases)
SetupResult
object
export interface SetupResult {
  initialized: boolean;
  scaffolded: string[];
  error?: string;
  legacyLayout?: { configPath: string };
  seededCommit?: boolean;
  findings: readonly HealthFinding[];
}
  • initializedtrue if a new git init ran.
  • scaffolded — vault-relative paths of files created or modified.
  • error — set on fail-soft paths (git failures, legacy layout). Compare against LEGACY_EMBEDDED_LAYOUT_ERROR to branch on migration.
  • seededCommittrue if an empty initial commit was seeded on an existing-but-empty repo.
  • findings — structured per-invariant outcomes (see HealthFinding).

HealthFinding

export interface HealthFinding {
  kind: "auto-recovered" | "error";
  invariant: string;
  message: string;
  recovery?: string;
}
kind
"auto-recovered" | "error"
"auto-recovered" — the invariant was violated but repaired in place; surface as info and proceed.
"error" — user action required; abort the distill spawn.
invariant
string
Stable string identifier for the check (e.g. "gitignore-block-correct", "config.json-tracked").
message
string
Human-readable description, suitable for notify text.
recovery
string
Present on auto-recovered findings; describes what action was taken.

LEGACY_EMBEDDED_LAYOUT_ERROR

export const LEGACY_EMBEDDED_LAYOUT_ERROR = "legacy-embedded-layout";
String sentinel set in SetupResult.error when auto-setup refuses to scaffold because the vault uses napkin’s legacy embedded layout (configPath === contentPath). Compare against this constant — do not pattern-match on the free-form error string.

parseManagedBlockRange

function parseManagedBlockRange(giPath: string): ManagedBlockRange | null
Reads <giPath> (a .gitignore file) and locates the # BEGIN NAPKIN-DISTILL MANAGED / # END NAPKIN-DISTILL MANAGED markers. Returns { beginLine, endLine } (1-indexed, matching git check-ignore -v output) on success, or null if the file is missing, the markers are absent, or the markers are malformed.

isLineInsideBlock

function isLineInsideBlock(
  source: string,
  line: number,
  range: ManagedBlockRange | null,
): boolean
Returns true if the gitignore rule at <source>:<line> (as reported by git check-ignore -v) lives strictly between the managed-block markers. source must be ".gitignore" — rules from global ignores, parent directories, or info/exclude are always outside the block. Returns false when range is null.

parseCheckIgnoreVerbose

function parseCheckIgnoreVerbose(
  stdoutLine: string,
): { source: string; line: number; pattern: string; pathname: string } | null
Parses one output line from git check-ignore -v. The format is:
<source>:<linenum>:<pattern>\t<pathname>
Returns null when the line does not match the expected shape (conservative — caller should treat as outside-block).

parsePorcelainWorktreeBranches

function parsePorcelainWorktreeBranches(stdout: string): Set<string>
Pure parser for git worktree list --porcelain output. Returns the set of short branch names (with refs/heads/ stripped) for all worktrees that have a branch line. Skips detached-HEAD and bare records. Exported for unit tests that pin the strict-parser behavior independently of git’s runtime output.

parseLiveWorktreeBranches

function parseLiveWorktreeBranches(vaultPath: string): Set<string>
Runs git worktree list --porcelain against vaultPath and returns the set of short branch names checked out in live worktrees. Used by the stale-distill-branch check to avoid deleting branches that are still in use. Returns an empty set when git worktree list fails.

walkToFirstExistingAncestor

function walkToFirstExistingAncestor(dir: string): string
Walks upward from dir until finding an existing ancestor directory. Used by the cache-root-writable probe: on a fresh box where ~/.cache/napkin-distill/ doesn’t exist yet, probing the cache root directly would always fail with ENOENT. Walking up to the first existing ancestor (typically ~/.cache) probes a directory that reflects the real writability state. Stops at the filesystem root.

probeWritable

function probeWritable(dir: string): WritableProbeResult
Probes whether dir is writable by writing and removing a temporary file (filename includes Date.now() plus a random suffix to avoid collisions across concurrent probes). Returns { writable: true } on success, or { writable: false, error: string } on failure — never throws.
export interface WritableProbeResult {
  writable: boolean;
  error?: string;
}

countTrackedFiles

function countTrackedFiles(vaultPath: string): number
Counts the files tracked in the vault’s git index via git ls-files. Returns the file count on success, or -1 if git ls-files fails or the vault is not a git repo. Used by the first-run notify to display a concrete count (e.g. “42 files tracked”) rather than an abstract scaffolding list.

Key constants from auto-setup.ts

ConstantValueDescription
BLOCK_MARKER_BEGIN"# BEGIN NAPKIN-DISTILL MANAGED"Begin marker for the managed .gitignore block.
BLOCK_MARKER_END"# END NAPKIN-DISTILL MANAGED"End marker for the managed .gitignore block.
BLOCK_CONTENTreadonly string[]Canonical content of the managed block. mergeManagedBlock rewrites the bracketed region to match this verbatim on drift. Strict superset of GITIGNORE_LINES.
GITIGNORE_LINESreadonly string[]Deprecated. Snapshot of the v0.3.0 line-by-line .gitignore entries (no markers). Retained as a migration shim; superseded by BLOCK_CONTENT. Will be removed in a future release.
STALE_DISTILL_BRANCH_GRACE_MS24 * 60 * 60 * 1000 (24 h)Grace period before a stale distill/* branch with no live worktree is auto-deleted by the full-level health check.

Notes

DistillError (extends Error) is thrown by workspace-layer operations in distill-workspace.ts. Use instanceof DistillError to distinguish workspace failures (bad git state, missing repo, branch collision, fork failure) from generic Error instances raised by the standard library.
import { DistillError } from "./distill-workspace";

try {
  const workspace = createDistillWorkspace(vault, sessionFile, cwd);
} catch (err) {
  if (err instanceof DistillError) {
    // workspace-layer failure; workspace was rolled back
  } else {
    throw err; // unexpected stdlib error
  }
}
All exported functions from distill-workspace.ts and auto-setup.ts are individually testable without a live pi instance. The test suite in extensions/distill/ covers them using real git repos in temp directories, bash stub fixtures for agent behaviors, and injectable clocks/probes for deterministic boundary tests. Pass spawnFn to spawnDistillInWorktree to capture spawn calls without starting real processes; pass options.probeWritable to ensureVaultReadyForDistill to simulate an unwritable cache root.

Build docs developers (and LLMs) love