Skip to content

Spec suite overhaul: lifecycle convention, verification sweep, and pairing presence#209

Merged
nedtwigg merged 18 commits into
mainfrom
spec-refinement
Jul 5, 2026
Merged

Spec suite overhaul: lifecycle convention, verification sweep, and pairing presence#209
nedtwigg merged 18 commits into
mainfrom
spec-refinement

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Jul 5, 2026

Copy link
Copy Markdown
Member

What this is

A ground-up accuracy pass on the spec suite, plus the code fixes it surfaced. Three arcs:

1. Spec lifecycle convention

Specs here are written dream-first, cut to the smallest shippable slice, and keep the remainder for planning. AGENTS.md now codifies how that works without eroding accuracy: everything above a spec's final ## Future section is present-tense truth about the code; unbuilt design lives below it as named scopes (e.g. workspaces-rollout, defined once in layout.md and linked from everywhere else); dream-driven constraints stay in the body marked Reserved:; promoting text above the fold is part of a feature's definition of done. All specs comply, and scripts/spec-lint.mjs (first step of root pnpm test) enforces the mechanical parts: complete index, Future-last, resolving links/anchors, and every backticked repo path existing on disk.

2. Index + structural rework

  • AGENTS.md indexes all specs (four were undiscoverable, including the largest); entries slimmed, with each spec's own Files/Code Map as the exhaustive file map; Architecture/Project Structure merged; dor/ + dor-lib-common/ listed. CLAUDE.md imports AGENTS.md so every session starts with the rules.
  • New standalone.md host spec (symmetric to vscode.md) and mobile-terminal-ui.md (the durable component spec, split from the website-prototype doc).
  • remote-security-model.md rewritten to describe the shipped system with source-of-truth pointers; remote-api.md restructured so its body matches the shipped wire contract exactly (browser surfaces, VR Window, tethering display staged in Future; the VR snapshot hierarchy fixed to match Window ⊃ Workspace ⊃ Pane); server.md de-POC'd; glossary owns mode names and the surface-kind enum mapping.
  • Merged origin/main mid-flight (Pocket PR Remote control ("Pocket"): self-hosted phone terminal #203 + browser/clipboard work) with spec-quality fixes on the incoming content.

3. Spec-vs-code verification sweep + fixes

21 agents (one per spec) checked every normative claim against the code. ~30 drifted claims fixed across 14 specs — highlights: the dual-tap gesture only enters command mode; macOS Ctrl+V is intercepted for paste; terminal.resize/terminal.semantic were never emitted (now deleted from the wire contract and staged in Future); drag-to-paste is inert (dragDropEnabled: false); ACL/enrollment persist in localStorage, not platform saveState. Code-side findings fixed: the bell-dialog attentionDismissedRing rule in WATCHING_DISABLED (with regression test), theme-observer class+style symmetry on both roots, harness layout grant, dead BrowserSidecarHost.pending map, ITERM2_COMPAT_VERSION sync comments, three stale code comments.

Pairing presence (security): the sweep found pairing rode the hours-long session despite pairing.ts's contract demanding fresh presence. Now server-attested: sessions carry lastVerifiedPresence (refreshed by sign-in, re-auth, and verified connects); checkPair rejects requests older than PAIRING_PRESENCE_WINDOW_MS (30s) with stale-presence; new /api/reauth/* endpoints let Pocket re-assert with one WebAuthn prompt on the same session/socket and retry automatically. The Host's human approval modal remains the backstop, which is what bounds a server-compromise forgery of the stamp.

Verification

  • Full workspace pnpm test green (spec-lint + lib 844+ / server 73 / server-lib-common / standalone / dor / website).
  • New tests: pairing stale→reauth→approved end-to-end through the real relay (manual clock); client retry sends the identical request; connect2 presence refresh; bell-dialog regression.

🤖 Generated with Claude Code

nedtwigg and others added 18 commits July 3, 2026 22:51
AGENTS.md gains a "Spec lifecycle" section codifying how dream-first specs
stay accurate: everything above a spec's final "## Future" section is
present-tense truth about the code; unbuilt design lives below it, cut down
into named scopes defined in exactly one spec; dream-driven constraints stay
in the body marked "Reserved:"; and implementing a staged item includes
promoting its text above the fold.

Bring all 20 specs into compliance:

- Consolidate the workspace rollout ledger into layout.md ## Future
  (Scope: workspaces-rollout); glossary, alert, transport, and vscode now
  link to it instead of restating stages.
- Restructure remote-api.md so the body matches the shipped wire contract
  (server-lib-common/src/remote/wire.ts): terminal-only DirectoryEntry, no
  hello capabilities field. Browser surfaces, VR wall, graded grants,
  WebRTC, QoS hardening, and the "tethering to <device>" display (verified
  unimplemented) move under ## Future. "protocol-v1" is now the named scope
  for the shipped protocol, dissolving the v1 ambiguity with server.md.
- Promote completed plans: vscode.md "Dream architecture" becomes ## Future
  (dream commands renamed to command/passthrough), server.md's finished
  five-slice build order becomes a present-tense Testing section,
  pocket-app.md phase 1 (implemented as PocketWall.tsx) is struck.
- Normalize the remote suite to single-H1 headings; move the WebAuthn PRF
  section to ## Future; fold the challenge-reuse property into
  remote-security-model.md Connection Establishment.
- Move remaining forward-looking passages below the fold: dor-cli workspace
  handles (ref grammar stays as Reserved:), terminal-escapes fish
  integration, tutorial follow-up scenarios, shortcuts workspace bindings;
  rename dor-browser/mobile-ui/mouse-and-clipboard future sections to
  ## Future and make them last.
- Accuracy fixes verified against code: layout.md shortcut table lists m/d
  for minimize (handle-pane-shortcuts.ts), stale "Tether prototype" name
  becomes Pocket, server.md tethering-display claim corrected and its two
  fake hosts disambiguated, missing glossary callouts added (theme,
  tutorial, server), AGENTS.md index drift fixed (tutorial storage keys,
  tethering, dream-architecture phrasing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Index all 20 specs (shortcuts, auto-update, mobile-ui, and deploy were
missing — mobile-ui is the largest spec in the repo and defines the
components pocket-app.md ships on). Slim every entry to what-it-owns plus
load-bearing touch points; each spec's own Files/Code Map section is now the
exhaustive file→spec mapping. alert.md, terminal-state.md, and
mouse-and-clipboard.md gain Files sections so the detail dropped from the
index keeps a home.

Merge the duplicated Architecture and Project Structure sections into one
Architecture list, adding the previously unlisted dor/ and dor-lib-common/
packages with a one-line disambiguation of the two *-lib-common names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each shared rule now has one owning spec; the others defer with a link:

- Keystroke fallback / OSC promotion: terminal-state.md is the single owner,
  with the reducer restructured into OSC-driven events vs a dedicated
  Keystroke fallback subsection (shape learning, submit parsing, reconnect
  seeding, swap safety, synthesized transitions, per-pane retirement) plus a
  compact flow sketch. terminal-escapes.md's fallback section slims to the
  injection-failure context and a pointer.
- <idle>/<unnamed> title validation: transport.md owns the
  setTerminalUserTitle rules; layout.md's inline-rename section now defers
  instead of re-explaining the sentinel.
- Theme resolver null-default rules: theme.md owns them; vscode.md's
  restatement of the list.inactiveSelectionForeground case becomes a
  pointer.

Also: move alert.md's cfg.alert source-of-truth anchor up to the Attention
section where T_USER_ATTENTION is first discussed, and footnote that BEL is
not an OSC in terminal-escapes.md's Supported OSCs table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add a Modes section to the glossary: passthrough and command are the
  canonical names (layout.md owns switching behavior); ban the "terminal
  mode" / "normal mode" / "navigation mode" aliases. shortcuts.md drops its
  alias, gains a banner marking it a derived quick-reference (layout.md and
  mouse-and-clipboard.md own the behavior).
- Add the canonical mapping table for the three surface-kind enums
  (persisted surfaceType / renderMode / legacy CLI SurfaceType), previously
  assembled by the reader from three specs; the Naming conventions bullet
  defers to it.
- Soften the Liskov contract to what the code enforces today (documented
  preconditions, caller checks the layer) and stage uniform typed-error
  enforcement under a new glossary ## Future — no marker for it exists in
  the registry code.
- Add "tether" to the overloaded-terms table (remote size-authority sense
  only) and point at remote-api.md for remote-only vocabulary (Viewer).
- layout.md: note that keyboards without a right Meta key rely on the Shift
  gesture (fact), and stage a platform-aware baseboard hint under ## Future
  (the hint is a static LCmd → RCmd string today).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
remote-api.md: the staged VR subscription replicated "the wall" with a
snapshot that nested Workspaces above Windows — inverting the glossary's
Window ⊃ Workspace ⊃ Pane ⊃ Surface containment, and colliding with the
glossary's Wall (renderer of exactly one Workspace). Rename the concept to
the Window it actually replicates: window.watch / window.lease /
WindowSnapshot, with workspaces containing panes directly (a session
connects to one Host, hence one Window) plus an activeWorkspaceRef. The
reserved hello capability and prose references follow.

server.md: all five build slices shipped and selfhost is a permanently
supported mode, so drop the POC framing — retitle to "Server (selfhost)",
"Guardrails", "Running it". The guardrails themselves are unchanged; they
describe real current limits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The spec was the original design pitch: hedged "recommended implementation"
language for code that now exists, no source-of-truth pointers, and message
flows duplicating server.md's sequence diagrams. Now:

- Every implemented claim is anchored: passkey verification (passkey.ts),
  device keys (deviceKey.ts + the IndexedDB store in
  lib/src/remote/client/device-key.ts, including the DEVICE_AUTH_DOMAIN
  domain separation), the real HostAclRecord schema and pair-matching
  semantics (acl.ts), pairing tickets and TTL (pairing.ts), and
  authorizeConnection + HostChallengeIssuer (connection.ts, challenge.ts).
- The pairing and connect wire sequences defer to server.md's diagrams; this
  spec states what they must establish.
- Unimplemented guidance moves below the fold: navigator.storage.persist()
  and iOS install-before-pairing gating (verified absent from the code) are
  staged under ## Future alongside WebAuthn PRF, with the durability facts
  themselves staying in the body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tutorial.md's mouse/clipboard coverage table referenced
  mouse-and-clipboard.md by bare section numbers, so a renumbering there
  would silently break it. Every § ref is now a markdown link to the named
  heading anchor. This also surfaced one already-stale ref: "§8.7
  right-click paste" pointed at what is now Drag-to-Paste; the prose now
  points at the Future section that actually owns both deferrals.
- layout.md and dor-browser.md gain Maintainer checklists (the theme.md
  pattern): cross-spec ownership rules for header elements and shortcut
  tables, persistence-migration routing, the design-token rule, the
  clip-path animation constraint; renderMode canonicity, renderer:'always',
  the AGENT_BROWSER_ALLOWED_SUBCOMMANDS security boundary, spawnAndCapture,
  and closed-session-mark rules.
- dor-browser.md: document the intended UX of normalizeNavUrl defaulting
  bare remote hostnames to https:// in an iframe pane (scheme error + dor ab
  hint steers remote sites to the agent-browser renderer instead of
  proxying them over plain HTTP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mobile-ui.md was titled "Website Prototype Spec" while defining the durable
behavior of MobileTerminalUi/MobileWall — the components the real Pocket app
ships on. Replace it with mobile-terminal-ui.md, a component spec: core
layout and keyboard reserve, touch modes, the radial gesture menu, input
modes, Type-mode rules, touch/keyboard-suppression invariants, Files, and
Future (Recent/Draft reserves are explicitly placeholder-only). Prototype
goals, v0 scope, and success criteria are retired to git history via a short
History note; the fake-shell behavior the playground needs moves into
tutorial.md, which owns the page wiring.

Inbound references updated: the AGENTS.md index entry and the blur-repeat
comment in MobileTerminalUi.tsx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vscode.md gave the VS Code host a single home; the standalone host's details
were scattered across six specs. standalone.md now owns the host layer —
verified against the code: the thin-Rust architecture (lib.rs supervises the
sidecar and bridges invoke commands to JSON-lines stdio; all real logic runs
in the Node sidecar sharing the same lib/src/host modules the VS Code host
runs, bundled by build-sidecar-proxy.mjs), the main.tsx boot sequence
(init-before-resume ordering), the sidecar shutdown ordering
(closePoppedOut race → control socket → killAll) and the parent-PID
watchdog that keeps a force-killed host from orphaning a sidecar holding
ConPTY files open, the AppBar, localStorage persistence through
window-persistence, file drop, logging paths, and the build/dev workflow —
including the vite-only beforeDevCommand caveat (sidecar/host-code edits
need a manual re-stage + restart).

Behavior owned elsewhere stays elsewhere: the spec links to transport,
dor-cli, dor-browser, auto-update, theme, layout, and deploy rather than
restating them. Indexed in AGENTS.md between vscode.md and auto-update.md;
also fixes the pocket-app entry's stale "mobile-ui composition" phrase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reducer restructure left the CWD precedence rules visually nested under
the new Keystroke fallback subheading; promote them to a sibling subsection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLAUDE.md is a one-line @AGENTS.md import so the spec index and lifecycle
conventions are in context from the first turn — until now they only
applied when a session was explicitly pointed at AGENTS.md.

The Design section now points at PRODUCT.md (users, brand personality,
aesthetic anti-references) alongside DESIGN.md, which quotes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ription

theme.md's dynamic-palette coverage knew only door-bg/door-fg/focus-ring;
the code also computes --color-alarm-vs-{header-active,header-inactive,door}
(dynamic-palette.ts). Now specified: the pick rule (plain white or black by
OKLab lightness of the surface the ringing bell sits on, via
pickAlarmColor), the ansiYellow body-level baseline that renders before the
first dynamic pass, the one-pass-behind subtlety for alarm-vs-door (it
derives from door-bg computed by the same pass; the MutationObserver
re-fires on the pass's own body.style write), Storybook decorator coverage,
and pickAlarmColor in the maintainer checklist's no-forking rule.

Two stale claims corrected along the way: theme.md named text-warning as the
ringing-alert exception, a class that no longer exists anywhere in lib/src
(it's text-alarm-vs-*); DESIGN.md described the alarm tint as "per-surface
OKLCH hue-rotation", but pickAlarmColor is a white-or-black lightness pick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A dependency-free node script checks the four mechanically-checkable spec
conventions: every docs/specs/*.md indexed in AGENTS.md, ## Future last in
its file (the fold), every relative markdown link and #anchor resolving,
and every backticked repo path existing on disk (so Source of truth
pointers rot into test failures instead of silently). Build/generated
outputs that don't exist in a clean checkout are skip-listed.

Wired as `pnpm lint:specs` and as the first step of the root `pnpm test`,
which CI already runs. AGENTS.md's Spec lifecycle section names the
enforcement. Verified both ways: the current suite passes clean, and a
synthetic violating spec trips all four check categories.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	docs/specs/pocket-app.md
#	docs/specs/remote-api.md
One verification agent per spec (21 total) checked every normative body
claim against the code; 5 specs came back fully clean (glossary, transport,
pocket-app, dor-cli, terminal-state-modulo-wording). Confirmed drift fixed:

- shortcuts.md + layout.md: the dual-tap gesture only ENTERS command mode
  (exit-only from passthrough), never toggles; untouched panes/doors kill
  without the confirm prompt; the swap chord accepts either modifier on
  every platform; the passthrough selection ring is 1px with no glow; doors
  get no overlay inflation; the baseboard hint is already platform-aware
  (the Future item staging that work is deleted — it shipped); the 440ms
  auto-spawn delay applies to the minimize path only.
- mouse-and-clipboard.md: Ctrl+V is intercepted for paste on every platform
  including macOS (deliberately shadowing readline quoted-insert) — the
  macOS keybinding table, §8.3 title, and the IS_MAC claim now say so; the
  selection outline color chain is focus-ring → active-header → cornflower
  fallback (never terminal fg/selection bg); bracketed paste reads the
  store mirror; image paste writes <uuid>-clipboard.png; drag-to-paste is
  documented as inert (dragDropEnabled=false for HTML5 pane dragging).
- remote-api.md + server.md: terminal.resize / terminal.semantic are
  reserved wire shapes with no emitter — the Host sends only terminal.data
  and terminal.closed; semantic state reaches clients via
  directory.snapshot. server.md also fixes: ACL/enrollment persist in
  webview localStorage (not platform saveState; also fixed in
  remote-security-model.md), scripts/fake-host.mjs serves the full FakeHost
  echo terminals (not hello-only), the Testing section's harness/coverage
  claims, setup/begin is password-gated only, and the pairing credential
  check is account-level.
- remote-security-model.md: "revocation propagation" moved from Server
  responsibilities to ## Future (no callers, no wire frame).
- tutorial.md: Ctrl+C exits the tut runner outright; bundled themes are a
  VS Code set (not "GitHub variants"); theme-delete falls back to the
  page's defaultThemeId; base fake shell only handles Up/Down.
- standalone.md: the AppBar event is dormouse:new-terminal (hyphenated
  CustomEvent, not the VS Code message name); file drop marked inert;
  clipboard-ops.js role corrected (Windows agent-browser writes still use
  it); csp.mjs runs only when DORMOUSE_REMOTE_CONNECT_SRC is set.
- dor-browser.md: no stream-frame fallback exists — hosts without
  agentBrowserScreenshot show the placeholder.
- vscode.md/theme.md/terminal-escapes.md/auto-update.md/deploy.md/
  mobile-terminal-ui.md/terminal-state.md: observer attribute filters
  stated exactly; terminal-theme API is not behind the registry facade;
  debugger doesn't report alarm picks; LC_TERMINAL set unconditionally;
  failure-marker launches skip the update check; the notice is 12px; the
  secrets table matches the script; Type-mode focus is synchronous + rAF
  retries; process-CWD precedence is source-based.

Also fixes three stale code comments the sweep surfaced: the VS Code
parser's colorProvider JSDoc, the agentBrowserScreenshot fallback claim in
platform/types.ts, and IFRAME_SHIM's "posts two things" (it posts four).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verification sweep found these two host→client events defined in
wire.ts and handled (dead) in pocket-client, but emitted by nothing. Events
are dispatched by name, so reserving names buys no compatibility protection
— a future event lands additively and old clients ignore unknown names.
Remove REMOTE_EVENTS.terminalResize/terminalSemantic, the
TerminalResizeEvent interface, pocket-client's dead branch and its unused
TerminalHandlers.onResize hook. The client→host terminal.resize METHOD
(size authority) is untouched and live.

remote-api.md's body now shows the two-event stream as the whole of v1; the
event definitions move fully below the fold into Future item 5 (tethering
display), whose client half they are. server.md's protocol-v1 summary
follows. Verified: server-lib-common build + tests, lib remote tests
(67/67), server tests (72/72) all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Bell click after an attention-dismissed ring now opens the dialog even
  from WATCHING_DISABLED (reachable because protocol/command-exit rings need
  no WATCHING): both AlertManager.dismissOrToggleAlert and the
  session-activity-store mirror consult attentionDismissedRing in that
  branch, matching alert.md's stated rule. Regression test added.
- terminal-theme.ts now observes class + style on BOTH body and html,
  matching the resolver's observer, so a theme signaled via an <html> class
  mutation refreshes the terminal palette. vscode.md/theme.md updated to the
  new symmetric truth.
- fake-host.mjs harness sends grants { layout: false }, mirroring the
  shipped Host and the protocol-v1 invariant.
- BrowserSidecarHost: remove the never-populated pending map and its
  requestId-correlation branch — request/response correlation happens over
  the /invoke HTTP round-trip, and every SSE event fans out to handlers
  (which is what the dead branch already reduced to).
- ITERM2_COMPAT_VERSION gains keep-in-sync mirror comments on both
  definition sites (the OPEN_PORT_TIMEOUT_MS house pattern).
- OSC 99: alert.md clarified rather than code changed — kitty's done-flag
  semantics apply regardless of the final chunk's payload type, so a
  d=1 icon/buttons chunk completing a pending accumulated notification is
  correct; the management payload itself still contributes no content.

Verified: full lib suite 844/844, server 72/72, standalone tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the sweep's remaining security finding: pairing.ts's contract asked
for a fresh passkey assertion before ceremony.begin(), but nothing verified
one — pairing rode the hours-long signed-in session. The model, per
discussion: presence for pairing is server-attested plus Host-approved.

- Session gains lastVerifiedPresence, set at sign-in and refreshed by any
  server-verified assertion — /api/reauth/finish and a verified connect2
  handshake — so "sign in then Pair" and "connect to A, pair B" both cost
  no extra prompt.
- checkPair rejects when the stamp is older than PAIRING_PRESENCE_WINDOW_MS
  (30s, named constant in pairing.ts) with the stale-presence error code,
  before anything reaches the Host. The window gates the request, not the
  approver's deliberation (that stays the 5-minute pairing-ticket TTL).
- New /api/reauth/begin + /api/reauth/finish (session-token auth) re-assert
  presence on the SAME session and relay socket — sign-in's verification,
  extracted into a shared verifyFreshAssertion, without re-minting the token.
- PocketClient.pair() maps stale-presence to one WebAuthn prompt and a
  single retry of the identical request.
- The relay threads the session into the gate (ClientConn.session /
  PresenceSession); pairing.ts's integration contract now documents the
  server-attested + human-approval model, as do remote-security-model.md
  (Pairing Ceremony, with the server-compromise rationale) and server.md.

Tests: end-to-end stale → reauth → approved through the real relay with a
manual clock; client-side retry sends the identical request with
bearer-authorized reauth calls; connect2 presence refresh asserted. Full
workspace suite green (root pnpm test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: bda2824
Status: ✅  Deploy successful!
Preview URL: https://e5408235.mouseterm.pages.dev
Branch Preview URL: https://spec-refinement.mouseterm.pages.dev

View logs

@nedtwigg nedtwigg merged commit f82480e into main Jul 5, 2026
9 checks passed
@nedtwigg nedtwigg deleted the spec-refinement branch July 5, 2026 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants