02 · Decisions

Why it's built this way

The durable ledger. Each call is one or two lines — detail lives one link away in the canonical doc.

D-026

Claude and Codex are first-party adapters over one Sandpaper truth

Accepted

Use an internal provider registry, install both integrations by default, and keep page/provider sessions and transcripts independent. The toolbar may switch providers explicitly but never transfers hidden context or falls back silently; repository files and the brain remain shared authority. Keep the registry internal in v0.3.0 and document the requirements for a later public plugin API. approved design →

D-025

v0.2.1 requires package-level release evidence

Accepted

Keep the sweep a patch release and gate it on shipped-JavaScript syntax, Node and browser suites, doctor, packed-package installation, one disposable Claude smoke, and a human-confirmed tag. Release is complete only after registry, provenance, fresh npx, and clean-install verification; corrections use a new immutable patch. release contract →

D-024

Layer tests around a deterministic provider-neutral runner

Accepted

Expose a testable server constructor, inject a fake runner, and cover policy, lifecycle, undo, setup, and browser contracts with Node integration tests plus dev-only Playwright on Chromium. Keep live agents out of CI; record one disposable Claude smoke before release while preserving zero runtime dependencies. current tests →

D-023

One hardened browser contract; repair existing semantics

Accepted

Centralize authenticated API handling, make Sand/Hands and undo state truthful, preserve intentional rich HTML edits, and fix verified CSS/search/mode regressions. Repair baseline semantics and focus now; keyboard/touch reorder-delete remains the explicit later task t-0024. toolbar →

D-022

Derive mechanical brain state; stamp editorial judgment

Accepted

HTML entries remain canonical: browser code derives numerical rollups, while NOW, focus, worklog, decisions, and learnings stay stamp-maintained. doctor independently validates both layers, metadata, references, and serving policy; the sweep reconciles current docs without rewriting history. plan model →

D-021

The server owns one truthful turn lifecycle

Accepted

Reserve turns before body reads, retain/replay current SSE state, let runners emit normalized frames, and derive Saved/Undo from actual file change rather than requested tools. Per-tab client IDs replace global reload suppression; errors are terminal and snapshots are useful, bounded state. runtime →

D-020

Keep repo-root presentation; default-deny secret paths

Accepted

Preserve whole-repo serving because the brain intentionally links to canonical specs, source, assets, and .github workflows outside brain/. Deny runtime/secret paths and unknown hidden namespaces by default, allow safe hidden prefixes narrowly, and make doctor detect blocked brain references before they break. server boundary →

D-019

Stabilize first; make Claude + Codex first-class second

Accepted

Ship a risk-based v0.2.1 sweep before first-class provider support in v0.3.0. The sweep fixes verified defects, drift, and regression coverage plus self-derived state; net-new product surfaces remain explicit roadmap choices. tracked work →

D-018

Versioning is a Sandpaper feature — /sandpaper:release + a tag-triggered publish

Accepted
Decision
A new /sandpaper:release command drafts release notes and a semver bump straight from brain/log.html (never from scratch), writes CHANGELOG.md, then runs npm version + git push --follow-tags. The pushed tag hands off to .github/workflows/release.yml: tests → verify-publish (the tarball-safety checks that used to be manual — no site/, no secrets, a size envelope) → npm publish --provenance → a GitHub Release cut from the changelog. .github/workflows/ci.yml runs the same tests + verify-publish on every push/PR across Node 18/20/22; .github/dependabot.yml keeps pinned Actions patched. The agent never publishes directly — only the release workflow does, after it re-verifies.
Because
The brain already narrates every session in user-facing language — it's a better release-notes source than commit messages, and generating them by hand would duplicate work the brain already does. A human still confirms every semver bump (AskUserQuestion in the command) — the agent proposes, never silently decides. Automating the safety checks (forbidden paths, secret patterns, size envelope) turns three turns of manual npm pack --dry-run vigilance into a standing CI gate.
Instead of
Hand-editing package.json's version and publishing locally each time (what shipped 0.1.0) — works, but re-litigates the EOTP problem every release and has no changelog discipline. CI publishing needs an npm Automation token (repo secret NPM_TOKEN) or Trusted Publishing — a regular Publish token still demands interactive 2FA, which is exactly what blocked the first two publish attempts.
/sandpaper:release →   release.yml →
D-017

The npm package is @nynb/sandpaper, scoped

Accepted
Decision
npm publish of the unscoped sand-paper (D-016) was rejected by the registry — 403 Package name too similar to existing package sandpaper, npm's own suggested fix. Republished scoped as @nynb/sandpaper (the owner's npm username, no hyphen — scoping sidesteps the similarity check entirely). package.json simplifies back to a single bin entry, sandpaper — with the local name now exactly sandpaper, npx's default trailing-name match works without the dual-alias workaround D-016 needed.
Because
npm view sand-paper returning 404 only checks exact-name registration; it says nothing about npm's separate anti-squatting policy, which blocks unscoped names confusable with an existing package regardless of availability. Scoping under your own account is npm's documented escape hatch.
Instead of
Unscoped sand-paper (D-016, superseded here — never registered, the publish 403'd) or hunting for a third unscoped name, which risked the same block for anything sandpaper-adjacent.
package.json →   the gotcha →
D-016

The npm package is sand-paper — the CLI stays sandpaper

Superseded
Decision
sandpaper is taken on npm (v1.4.6, unrelated). Publishing as sand-paper (owner's pick over scoped @codevalley/sandpaper or sandpaperjs) — verified available. package.json keeps two bin entries pointing at the same bin/cli.js: sandpaper (the friendly command after a global install) and sand-paper (guarantees npx sand-paper … resolves to the right binary without depending on npx's single-bin fallback behavior).
Because
An unscoped name needs no --access public flag and no scope-ownership question; keeping the CLI's own name as sandpaper preserves the brand in every command a user actually types day to day — only the npm registry entry differs.
Instead of
@codevalley/sandpaper (scoped — keeps the exact name but adds the scope prefix + a publish flag) or an unscoped alt like sandpaperjs.
package.json →   superseded by D-017 — the publish 403'd
D-015

The blueprint identity — one visual world, product-wide

Accepted
Decision
The as-built engineering-drawing system (cyanotype prussian · drafting grid · working blue + inspection red · Archivo + JetBrains Mono · sharp radii · the owner's diamond-sheet logomark) is Sandpaper's identity everywhere: the landing (site/), this brain (via a theme.css token surgery — the engine needed one line changed), and therefore the default skin every new install ships with. Titling is SANDPAPER, one word, mark first.
Because
The owner called the warm-cream/serif/clay look what it was — the recognizable AI-page cliché — and the drawing-set metaphor is subject-true: a brain is the repo's as-built documentation, and “stamped” is a drafting word. A fresh-eyes critic confirmed: “not a known AI cliché — an ownable spine.”
Instead of
Keeping two visual worlds (blueprint landing, warm brain) — clicking “live brain” would have changed universes; or keeping warm as the shipped default.
the skin →   the mark →
D-014

brain/ is always publishable — out-links resolve at runtime

Accepted
Decision
Out-of-brain refs stay relative on disk (link-never-copy, local-first). Every page head carries <meta name="sandpaper:source"> (base = origin + /blob/HEAD/, credential-stripped, monorepo-prefixed; + expected package name); brain.js probes ../package.json per load (no cache, name-checked) and, when detached, resolves out-links at click time — rewrite, navigate, restore. No meta → dim + tooltip, never a silent 404. Doctor lints refs + meta consistency; a deploy guide ships in brain/README.md.
Because
The owner's invariant — deployable as-is, any moment, no build step — killed both alternatives under an adversarial panel (board 007): write-time absolute URLs break local-first; a publish step is the banned step.
Instead of
The original t-0019 publish/export build (demoted: an optional snapshot exporter, later, if rendered detached specs are ever wanted).
the resolver →   the deploy guide →
D-013

The cover hosts the canvas — Claude's thinking, elevated

Accepted
Decision
When Claude works through something substantial, the rich version is written as a board onto the cover's canvas feed (the BRAIN:CANVAS region) — not left in the terminal scrollback. The canvas is the cover's hero (a scrollable feed of boards); the lens doors demote to navigation below it. Boards are freeform rich HTML (theme-skinned, refinable with the toolbar); only durable ones are promoted into the curated brain, which the canvas never writes to directly.
Because
The founding bet (l-parity): the browser is the eyes, not a worse mouth. Rich explanations die in a terminal — they can't be diagrams, re-read, or refined. Hosting them on the cover makes Sandpaper a live second screen and closes the loop it's named for. A separate "Canvas" tab would make it a feature you visit; on the cover it's the centre.
Instead of
A peripheral canvas tab, or routing explanations straight into the wiki — which would wreck the wiki's job as the small, settled layer.
the canvas →   the principle →
D-012

install-skill auto-wires the hooks

Accepted
Decision
npx sandpaper install-skill merges the two hooks into .claude/settings.json automatically — preserving existing settings, deduping by command; --no-hooks prints the snippet to add by hand instead. Paired with the /sandpaper:init intro + closing summary and the on-page first-run tour, install→generate→refine is now a guided loop.
Because
A user running the install CLI is installing — that is not the agent silently editing its own config (which D-010 rightly keeps opt-in). Asking people to hand-merge JSON means most never enable it, so the auto-update system never runs.
Instead of
Printing a manual-merge snippet for every install (the old default) — correct for the agent, needless friction for a deliberate user-invoked setup.
setup.js →   the plan →
D-011

npx is the plumbing; /sandpaper is the intelligence

Accepted
Decision
The npx sandpaper CLI does the deterministic plumbing — install-skill / init / doctor / open (copy templates, write the manifest, health-check) with NO AI. The /sandpaper:* slash commands do the intelligence (the agent's discover→wizard→generate, stamp, sync). Fresh repo: npx sandpaper install-skill/sandpaper:initnpx sandpaper open.
Because
Keeping AI out of the CLI makes install + doctor deterministic, testable, and runnable anywhere — not just inside Claude; the agent handles the reasoning where it belongs.
Instead of
A monolithic AI-driven CLI, or pure slash commands that can't set up the environment / ship the templates.
the README →   the plan →
D-010

The brain auto-updates via hooks — no prodding

Accepted
Decision
Two Claude Code hooks keep the brain current automatically: SessionStart (bin/brain-inject.js) surfaces the digest so a fresh session rehydrates; Stop (bin/brain-stamp-check.js) blocks once if a turn changed code but didn't stamp the brain. Self-limiting (honors stop_hook_active, idempotent). The skill ships them; installing is opt-in (merge into .claude/settings.json).
Because
A brain that relies on the agent remembering will drift. A Stop hook makes stamping non-skippable without the user having to prod — the difference between a discipline and a system.
Instead of
The CLAUDE.md contract alone + the user reminding the agent. (Auto-installing the hooks was correctly blocked as a self-config change — opt-in is right.)
the skill →   the plan →
D-009

Two phases: make it work for us, then package it

Accepted
Decision
Phase 0 — make it work for us: the loop + the brain, dogfooded on this repo (Sand · Hands · Sling · the living brain). Phase 1 — package for distribution: the skill (/init, /plan…), the npm sidecar, a themeable theme.css for any project. Finish 0 before 1.
Because
It has to earn its keep on our own work before it's worth anyone else's — dogfooding surfaces the real gaps.
the plan →
D-008

The brain is a 3-lens portal

Accepted
Decision
Everything organizes under three lenses — Product, Engineering, Project. The cover leads with three tabbed "lens doors"; a persistent sticky shell (identity + state + a 5-item lens rail) and a 3-column footer make every lens reachable by 2+ paths. The shell owns identity; the dark plate becomes a compact NOW "dateline".
Because
The cover's job is "pick one of three lenses", not "show a brand wordmark". Doors-as-hero + rail + footer give the multiple-paths-to-the-same-place navigation the IA needs.
Instead of
The old browse-cards cover (a brand page), and a single linear nav. (Chosen via a 3-direction design panel — TRI-TAB, hardened for contract-safety.)
the plan →   the cover →
D-007

One theme.css is the single skin

Accepted
Decision
All design tokens live in theme.css; brain.css only @imports and consumes them via var(). The toolbar is dev chrome injected into arbitrary host pages, so it keeps its OWN #sp-panel-scoped copy and never depends on a host theme.
Because
A project owner re-skins their whole Sandpaper by editing one file — and the toolbar must render identically on any page it lands in.
Instead of
Tokens triplicated across brain.css + the docs (drift-prone), or a toolbar that breaks on un-themed host pages.
the plan →   cascade gotcha →
D-006

Pivot: the browser is the eyes, not the chat

Accepted
Decision
The browser becomes a living, navigable project brain (state) + in-place manipulation; deep steering stays in the terminal.
Because
An in-browser chatbox is a worse terminal — the user steers from the terminal even while we build the box.
Instead of
Chasing chat parity in the page (H-A).
spec →
D-001

The name is Sandpaper

Accepted
Decision
Call it Sandpaper — sand a rough draft into a finished spec.
Because
Everyday word, instant recall, "-paper" cadence, names the craft.
Instead of
Tweaq (too small), Illume / Gilden (too rare), Vivid (less ours).
spec §08 →
D-002

The document on disk is the single source of truth

Accepted
Decision
Both the user and Claude read/write the same file; the page reflects disk, never ahead of it.
Because
Rules out a server-side document model, HTML-from-stdout, and diffing — the bridge stays a dumb pipe.
engg →
D-003

claude -p is the right primitive

Accepted
Decision
Drive Claude via headless claude -p --output-format stream-json --resume; never tmux/screen-scraping.
Because
It's the same engine with structured output; verified it still draws from the subscription. amux's scraping pain confirms it.
spec §02 →   amux →
D-004

The bridge is a dumb pipe

Accepted
Decision
Claude edits the file with its own tools; the watcher reloads. Never assemble HTML from stdout.
Because
Keeps the bridge ~200 lines and avoids fragile partial-DOM parsing.
engg →
D-005

Zero runtime dependencies

Accepted
Decision
Node ≥18 built-ins only, ESM. No npm install to run.
Because
Instant to run, nothing to break, matches the local-first ethos.
engg →

Open questions

A question is just a decision not yet made. Resolving one flips data-status — never deletes.

open · blocking

How do annotations survive a full HTML regeneration?

If Claude rewrites the doc and drops data-cids, open annotations + brain links dangle. Plan: a CLAUDE.md contract that never strips cids, plus text-snippet / selector fallback. Risks §07 →

resolved · built

Make "undo?" real

The chip promises undo; a pre-turn snapshot exists. Wire it to the change card + a one-click restore. engg →

open · watch

The paused billing change

The June-15 Agent-SDK credit-pool split is paused; claude -p still bills the plan. Keep the brain swappable in case it un-pauses.

open · watch

What keeps sensitive internals out of a published brain?

The owner asked whether the canvas (conversation-derived boards) should even publish, and how to keep the whole brain leak-safe. Call so far: the canvas ships — it's content like decisions/learnings, not a capability like the toolbar, and publishing a brain is already opt-in + local-first. The honest gap is a publish preflight: a doctor pass that scans brain/ for secrets, emails, internal URLs before a deploy — tracked as t-0035. deploy guide →

resolved · built

Make brain/ always publishable — resolver designed, awaiting the go

Detached (subfolder-only) deploys 404 the canonical out-refs; the owner's invariant is always publishable, no build step. An adversarial panel (07-02) killed write-time absolute links (breaks local-first) and the publish build step (is the step the invariant bans); the survivor is a runtime resolver — relative links + a per-head source meta (blob/HEAD/ + expected package name) + a name-checked probe and click-time rewrite in brain.js, dim-with-tooltip when no base. Resolved by D-014 — built, panel-reviewed, browser-verified (t-0019 done). board 007 →