Spanner is a local-first, multi-surface coding-agent workbench. A single supervised daemon (@echohello/server) owns agent lifecycle, worktree management, and remote access. Three renderers consume the daemon over WebSocket: @echohello/web (desktop + PWA, Vite + React), @echohello/desktop (Electron shell), @echohello/app (Expo + React Native, mobile).
The architectural decisions are documented in docs/ — read them before making non-trivial changes:
docs/architecture.md— overall architecture, package boundaries, build orderdocs/onboarding-relay.md— pairing, relay, settings sync modeldocs/providers.md— agent runtime integrations (OpenCode, Claude Code, Cursor, MCP)
If you need to know why a decision was made, the build-research notes in the operator's Obsidian vault (/Users/johnny/projects/github.com/johnnyhuy/one-obsidian/Notes/2026-06-26 - Spanner*.md) contain the full reasoning.
mise install # node 22.23 + bun 1.3.14
bun install # install all workspaces
bun run build # full build (protocol → client → server-deps → server → web → desktop)
bun run typecheck # tsgo over all workspaces
bun run lint # oxlint
bun run format # oxfmt
bun test # vitest tests across all packages
# Dev loops
mise run dev:daemon # daemon only
mise run dev:web # Vite renderer
mise run dev:desktop # Electron shell (requires daemon running)
mise run dev:all # all three concurrentlyprotocol ← client ← server ← cli, web, desktop, app
↑
relay (post-MVP)
@echohello/protocolhas zero deps on other packages. It owns Zod schemas and binary frame codecs.@echohello/clientdepends on@echohello/protocol. It owns the WS driver.@echohello/serverdepends on@echohello/protocol+@echohello/client. It owns the daemon.@echohello/clidepends on@echohello/protocol+@echohello/client. It is a thin CLI over the daemon's WS API.@echohello/webdepends on@echohello/protocol+@echohello/client. It is the Vite + React renderer.@echohello/desktopdepends on@echohello/server+@echohello/web. It is the Electron shell that supervises the daemon.@echohello/appdepends on@echohello/protocol+@echohello/client. It is the Expo + RN mobile renderer.@echohello/relay(post-MVP) depends on@echohello/protocol. It is the relay server + client SDK for E2E-encrypted remote access.
Never import backwards — if you find yourself needing to import @echohello/web from @echohello/server, you have a design problem.
- TypeScript:
strict: true,noUncheckedIndexedAccess: true,exactOptionalPropertyTypes: true. ESM ("type": "module"). - Runtime: Node 22 LTS, Bun for package management + scripts, tsgo (native TypeScript) for type checking, tsc for emits.
- Logging: Pino + pino-pretty. Child loggers with
{module: "..."}. Structured fields over message templating. - Validation: Zod for all wire types. Define schemas once in
@echohello/protocol, derive types withz.infer<typeof Schema>. Never useany; reach forunknown+ Zod parse at boundaries. - Errors: Custom error classes per layer (e.g.
DaemonError,AgentError,ProviderError). Never throw plainError. - Async:
async/awaitonly. No raw.then()chains outside of event handlers. - Naming:
camelCasefor variables/functions,PascalCasefor types/classes,SCREAMING_SNAKE_CASEfor constants,kebab-casefor filenames. - Comments: Code comments only when the intent isn't obvious from reading the code. No banner comments.
- Pretext: Used in the web renderer for long-transcript performance. NOT a replacement for any renderer — sits under the React tree as a measurement primitive.
- CodeMirror 6: Used in the web renderer's file editor pane. Different concern from Pretext.
- Do NOT use OAuth. Do NOT roll crypto. Do NOT manage API keys for agent CLIs — each agent reads its own creds.
- Vitest across all packages.
- Test files colocated:
*.test.tsnext to source. - E2E tests for the daemon under
packages/server/src/server/daemon-e2e/. - Playwright for web E2E (post-MVP).
- Branch from
main. Conventional commits (release-please manages versions). - PRs target
main. CI runs typecheck + lint + test. - Never commit
.env,dist/,node_modules/,*.log,daemon-keypair.json,server-id.
Spanner ships agent skills in two locations, both using the Paseo/Spanner SKILL.md convention:
| Location | Audience | Installed via | Tracked? |
|---|---|---|---|
skills/<name>/SKILL.md (root) |
External users — published for the world to install | npx skills add echohello-dev/spanner |
✅ committed |
.agents/skills/<name>/SKILL.md |
Internal — auto-installed machine-local copies of third-party skills (e.g. impeccable, vendor-specific tooling) |
Tool hooks (lefthook, opencode, claude, codex) | ✅ committed, with scoped .gitignore inside |
The .agents/skills/ convention keeps every skill in the same shape regardless of which tool installed it, so the same SKILL.md frontmatter (name + description) is machine-parseable everywhere. Do not blanket-ignore .agents/ — only ignore caches/node_modules/dist inside the per-skill subdirs (the gitignore already does this scoped).
When you auto-install a third-party skill, drop it under .agents/skills/<name>/ with a SKILL.md and any scripts/references it needs, then commit. The root skills/ tree stays curated; the .agents/skills/ tree is the sink for everything auto-installed.
The skill content teaches an external AI agent how to drive the Spanner daemon via its MCP server tools (planned post-MVP).