How Sandpaper works under the glass: one authenticated server-owned global lifecycle, an internal static registry with Claude Code and Codex adapters, the brain truth contract, and the open anchoring problem.
One rule governs everything else: the HTML file on disk is the document. The server serves it verbatim, the selected provider edits it, the watcher reloads it — and the page always reflects exactly what is on disk, never a step ahead. No server-side document model and no HTML assembled from stdout. engg →
You, Claude Code, and Codex converge on one artifact and one durable brain. Page/provider resume IDs and project/page/provider browser transcripts are accelerators, not authority; switching performs no hidden handoff. disk decision → · provider decision →
Everything below is downstream of this one invariant — the bridge, the contract, and the Hands all exist to keep disk authoritative.
The server owns lifecycle, snapshots, hashes, locks, reload routing, and terminal truth. The static registry dispatches claude -p or controlled codex exec; each adapter normalizes native events without owning turn IDs or Saved/Replied. Provider validation happens before reservation, unavailable choices fail explicitly, and there is no silent fallback. engg §03 →
Server frames carry validated provider, turn, page, and phase identity. Claude shows supplied cost; Codex shows supplied total tokens. The toolbar keeps provider histories separate, lets a tab select explicitly or Make default, and resets only one page/provider after server success. protocol →
Two gotchas here are load-bearing. Scrub the child env before spawning: an exported ANTHROPIC_API_KEY silently bills the API even on a plan, so the runner drops it and nested-Claude markers. And the watcher fires loose: a null filename must count as a possible match; after debounce the server routes reloads by page and initiating client instead of suppressing them globally. env scrub → · null fname →
This brain separates two kinds of truth. Mechanical facts — task/phase progress, accepted decisions, open questions, learnings, and verified components — are derived in the browser from canonical HTML entries. Stamped fallbacks remain readable without JavaScript, and doctor independently recomputes them. Editorial judgment — NOW, digest focus, newest worklog, and the curated open list — stays stamp-maintained and cross-checked. truth model →
Every durable fact is an .entry: a block carrying data-cid (mirrored to its id), data-kind, data-status, data-date, and at least one data-ref into a canonical anchor — plus an optional data-lens for the three-lens axis. Status is a lifecycle flipped on the block, never a rewrite — a reversal is a new entry that supersedes the old. The governing rule is link, never copy: the living books point at sandpaper.html / engg-spec.html rather than restating them. the ledger → · the map →
Skin is one file. All design tokens live in theme.css; brain.css only @imports and consumes them via var(), so re-skinning a whole Sandpaper is a one-file edit (the injected toolbar keeps its own scoped copy so it renders on any host page). The catch that bit us: @import loads first, so a leftover :root in the consumer silently wins — drop it entirely. decision → · cascade gotcha →
Not every edit needs Claude. The Hands ship direct rich-text edits, sibling moves, deletion, and one-level direct undo, each serialized to the file with no AI call. Optimistic DOM changes roll back exactly when the server rejects the write. Keyboard/touch structural editing remains t-0024. the plan → · engg §09 →
The same byte-preserving edit layer powers direct writes, while server-owned pre-turn snapshots power AI undo only after a real disk change. Reply-only and no-op turns say Replied and expose no false undo. resolved undo question →
This is another writer on one file — your keystrokes, the selected provider's tools, and the page's own edits all targeting a single source of truth.
The bridge keeps one direction of dependency — cli → server → registry → adapters, plus the on-page toolbar — and zero runtime dependencies (Node ≥18 built-ins, ESM). Roles and live build status live on the component map; authoritative status stays in the engineering spec. component map → · how the pieces fit →
The one genuinely open engineering problem is anchoring. data-cid is both the brain's link target and the unit of scoping, so when Claude regenerates a document wholesale and drops the cids, every open annotation and brain link dangles. The plan: a CLAUDE.md contract that forbids stripping cids, backed by a text-snippet / selector fallback. Until that lands, durable anchoring is the gating risk for P2. open question → · risks §07 →
v0.3.0 is qualified and publishednpm run check:syntax, npm run test:unit, npm run test:browser, npm run test:package, node bin/cli.js doctor, npm run verify-publish, npm pack --dry-run --json, and git diff --check exited zero. Evidence: 340 unit, 101 Chromium, and an exact 58 files at 130,041 packed bytes / 488,236 unpacked bytes. candidate notes →
Disposable real-provider qualification passed Claude 2/2 and Codex 2/2 selected-document edit/resume turns, exact disk-byte checks, separate provider scopes, scoped reset, and explicit unavailable-provider recovery. A bounded real Codex post-reset turn spawned without resume and persisted a fresh opaque scope. Self-hosted legacy installation also passed exact hook migration, counter derivation, and populated-brain open routing. Packed dual, solo, upgrade/rebuild, CLI/open, and desktop/375px/320px behavior passed their release gates. CI published @nynb/sandpaper@0.3.0 to npm with provenance and created the public GitHub Release →
The readable deep-dive — flow, modules, one turn.
Modules, build status, glossary.
The durable ledger — filter “engineering”.
Hard-won, never re-learned.
What the bridge serves, and why.
The plan — what's built, what's next.
The 600-line source of truth.