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.
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.
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.
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.
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).
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.
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.
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:init → npx 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.
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.)
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.
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.)
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.
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).
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).
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.
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.
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.
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.
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 →