From 9f3ef073229664cf884d08494d91308659c1ce4b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 11 Mar 2026 21:23:02 +0700 Subject: [PATCH] docs: extract guidance into dedicated docs, streamline CLAUDE.md --- CLAUDE.md | 281 +++++---------------------------------- docs/ANDROID.md | 67 ++++++++++ docs/ARCHITECTURE.md | 184 +++++++++++++++++++++++++ docs/CODING_STANDARDS.md | 174 ++++++++++++++++++++++++ docs/DESIGN.md | 73 ++++++++++ docs/DEVELOPMENT.md | 130 ++++++++++++++++++ docs/RELEASE.md | 48 +++++++ docs/TESTING.md | 123 +++++++++++++++++ 8 files changed, 833 insertions(+), 247 deletions(-) create mode 100644 docs/ANDROID.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CODING_STANDARDS.md create mode 100644 docs/DESIGN.md create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/RELEASE.md create mode 100644 docs/TESTING.md diff --git a/CLAUDE.md b/CLAUDE.md index bae925101..41483ba6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,267 +1,54 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. - -**Key features:** -- Real-time streaming of agent output -- Voice commands for hands-free interaction -- Push notifications when tasks complete -- Multi-agent orchestration across projects - -**Not a cloud sandbox** - Paseo connects directly to your actual development environment. Your code stays on your machine. +Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. Connects directly to your actual development environment — your code stays on your machine. **Supported agents:** Claude Code, Codex, and OpenCode. -## Monorepo Structure +## Repository map This is an npm workspace monorepo: -- **packages/server**: The Paseo daemon that runs on your machine. Manages agent processes, provides WebSocket API for real-time streaming, and exposes an MCP server for agent control. -- **packages/app**: Cross-platform client (Expo). Connects to one or more servers, displays agent output, handles voice input, and sends push notifications. -- **packages/cli**: The `paseo` CLI that is used to manage the deamon, and acts as a client to it with Docker-style commands like `paseo run/ls/logs/wait` -- **packages/website**: Marketing site at paseo.sh (TanStack Router + Cloudflare Workers). +- `packages/server` — Daemon: agent lifecycle, WebSocket API, MCP server +- `packages/app` — Mobile + web client (Expo) +- `packages/cli` — Docker-style CLI (`paseo run/ls/logs/wait`) +- `packages/relay` — E2E encrypted relay for remote access +- `packages/desktop` — Tauri desktop wrapper +- `packages/website` — Marketing site (paseo.sh) -## Development Server +## Documentation -The `npm run dev` script automatically picks an available port for the development server. +| Doc | What's in it | +|---|---| +| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System design, package layering, WebSocket protocol, agent lifecycle, data flow | +| [docs/CODING_STANDARDS.md](docs/CODING_STANDARDS.md) | Type hygiene, error handling, state design, React patterns, file organization | +| [docs/TESTING.md](docs/TESTING.md) | TDD workflow, determinism, real dependencies over mocks, test organization | +| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP | +| [docs/RELEASE.md](docs/RELEASE.md) | Release playbook, draft releases, completion checklist | +| [docs/ANDROID.md](docs/ANDROID.md) | App variants, local/cloud builds, EAS workflows | +| [docs/DESIGN.md](docs/DESIGN.md) | How to design features before implementation | +| [SECURITY.md](SECURITY.md) | Relay threat model, E2E encryption, DNS rebinding, agent auth | -When running in a worktree or alongside the main checkout, set `PASEO_HOME` to isolate state: +## Quick start ```bash -PASEO_HOME=~/.paseo-blue npm run dev -``` - -- `PASEO_HOME` – path for runtime state (agent data, sockets, etc.). Defaults to `~/.paseo`; set this to a unique directory when running a secondary server instance. - -For trace+ logs, check $PASEO_HOME/daemon.log - -## Running and checking logs - -Both the server and Expo app are running in a Tmux session. See CLAUDE.local.md for system-specific session details. - -## Debugging - -### Daemon and CLI - -The Paseo daemon communicates via WebSocket. In the main checkout: -- Daemon runs at `localhost:6767` -- Expo app at `localhost:8081` -- State lives in `$PASEO_HOME` - -In worktrees or when running `npm run dev`, ports and home directories may differ. Never assume the defaults. - -Use `npm run cli` to run the local CLI (instead of the globally linked `paseo` which points to the main checkout). Always run `npm run cli -- --help` or load the `/paseo` skill before using it - do not guess commands. - -Use `--host ` to point the CLI at a different daemon (e.g., `--host localhost:7777`). - -### Relay build sync (important) - -When changing `packages/relay/src/*`, rebuild relay before running/debugging the daemon: - -```bash -npm run build --workspace=@getpaseo/relay -``` - -Reason: Node daemon imports `@getpaseo/relay` from `packages/relay/dist/*` (`node` export path), not directly from `src/*`. - -### Server build sync for CLI (important) - -When changing `packages/server/src/client/*` (especially `daemon-client.ts`) or shared WS protocol types, rebuild server before running/debugging CLI commands: - -```bash -npm run build --workspace=@getpaseo/server -``` - -Reason: local CLI imports `@getpaseo/server` via package exports that resolve to `packages/server/dist/*` first. If `dist` is stale, CLI can speak an old protocol (for example, sending `session` before `hello`) and fail with handshake warnings/timeouts. - -### Quick reference CLI commands - -```bash -npm run cli -- ls -a -g # List all agents globally -npm run cli -- ls -a -g --json # Same, as JSON -npm run cli -- inspect # Show detailed agent info -npm run cli -- logs # View agent timeline +npm run dev # Start daemon + Expo in Tmux +npm run cli -- ls -a -g # List all agents npm run cli -- daemon status # Check daemon status +npm run typecheck # Always run after changes ``` -### Agent state +See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for full setup, build sync requirements, and debugging. -Agent data is stored at: -``` -$PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json -``` +## Critical rules -To find an agent by ID: -```bash -find $PASEO_HOME/agents -name "{agent-id}.json" -``` +- **NEVER restart the main Paseo daemon on port 6767 without permission** — it manages all running agents. If you're an agent, restarting it kills your own process. +- **NEVER assume a timeout means the service needs restarting** — timeouts can be transient. +- **NEVER add auth checks to tests** — agent providers handle their own auth. +- **Always run typecheck after every change.** -To find an agent by title or other content: -```bash -rg -l "some title text" $PASEO_HOME/agents/ -rg -l "spiteful-toad" $PASEO_HOME/agents/ -``` +## Orchestrator mode -### Provider session files - -Get the session ID from the agent JSON file (`persistence.sessionId`), then: - -**Claude sessions:** -``` -~/.claude/projects/{cwd-with-dashes}/{session-id}.jsonl -``` - -**Codex sessions:** -``` -~/.codex/sessions/{YYYY}/{MM}/{DD}/rollout-{timestamp}-{session-id}.jsonl -``` - -## Android - -Take screenshots like this: `adb exec-out screencap -p > screenshot.png` - -### Android variants (vanilla Expo) - -Use `APP_VARIANT` in `packages/app/app.config.js` to control app name + package ID (no custom Gradle flavor plugin): - -- `production` -> app name `Paseo`, package `sh.paseo` -- `development` -> app name `Paseo Debug`, package `sh.paseo.debug` - -EAS profiles live in `packages/app/eas.json` as `development`, `production`, and `production-apk`. - -`development` uses Android `debug`. - -### Local build + install (Android device) - -From `packages/app`: - -```bash -# development (debug) -APP_VARIANT=development npx expo prebuild --platform android --non-interactive -APP_VARIANT=development npx expo run:android --variant=debug - -# production (release) -APP_VARIANT=production npx expo prebuild --platform android --non-interactive -APP_VARIANT=production npx expo run:android --variant=release - -# clean native project (when needed) -npx expo prebuild --platform android --clean --non-interactive -``` - -From repo root: - -```bash -npm run android:development -npm run android:production -npm run android:clean -``` - -`npm run android:release` is an alias for `npm run android:production`. - -### Cloud build + submit (EAS Workflows) - -Tag pushes like `v0.1.0` trigger `packages/app/.eas/workflows/release-mobile.yml` on Expo servers. -Tag pushes like `v0.1.0` also trigger `.github/workflows/android-apk-release.yml` on GitHub Actions to publish an APK asset on the matching GitHub Release. - -That workflow does: -- Build iOS with the `production` profile -- Build Android with the `production` profile -- Submit each build with the `production` submit profile - -Useful commands: - -```bash -# List recent mobile workflow runs -cd packages/app && npx eas workflow:runs --workflow release-mobile.yml --limit 10 - -# Inspect one run (jobs, status, outputs) -cd packages/app && npx eas workflow:view - -# Stream logs for all steps in one failed job -cd packages/app && npx eas workflow:logs --non-interactive --all-steps -``` - -## Testing with Playwright MCP - -**CRITICAL:** When asked to test the app, you MUST use the Playwright MCP connecting to Metro at `http://localhost:8081`. - -Use the Playwright MCP to test the app in Metro web. Navigate to `http://localhost:8081` to interact with the app UI. - -**Important:** Do NOT use browser history (back/forward). Always navigate by clicking UI elements or using `browser_navigate` with the full URL. The app uses client-side routing and browser history navigation breaks the state. - -## Expo troubleshooting - -Run `npx expo-doctor` to diagnose version mismatches and native module issues. - -## Release playbook - -Use the scripted release flow from repo root. Avoid manual version bumps, manual tags, or ad hoc publish commands unless debugging. - -```bash -# Recommended: full patch release (bump, check, publish, push branch+tag) -npm run release:patch - -# Manual, step-by-step fallback: -npm run version:all:patch # npm version across all workspaces (creates commit + local tag) -npm run release:check -npm run release:publish -npm run release:push # pushes HEAD and current version tag (triggers desktop + Android APK + EAS mobile workflows) -``` - -### Draft release flow - -```bash -# Stage a draft GitHub release with assets, but do not publish npm yet. -npm run draft-release:patch - -# Publish npm and promote the same GitHub draft release to final. -npm run release:finalize -``` - -Behavior: -- `draft-release:patch` bumps the version, runs release checks, pushes `HEAD` and the new `v*` tag, and creates the matching GitHub Release as a draft so desktop assets, APK uploads, and synced notes attach to that same draft release. -- `release:finalize` requires that the current tag already has a GitHub draft release, publishes the npm packages for that exact version, and promotes the same GitHub Release from draft to published. -- Use the same semver tag for both draft and final states; do not cut a second tag just to publish the release. - -Notes: -- `version:all:*` bumps the root package version and runs the root `version` lifecycle script to sync workspace versions and internal `@getpaseo/*` dependency versions before the release commit/tag is created. -- `release:prepare` refreshes workspace `node_modules` links to prevent stale local package types during release checks. -- If `release:publish` fails after a successful publish of one workspace, re-run `npm run release:publish`; npm will skip already-published versions and continue where possible. -- If a user asks to "release paseo" (without specifying major/minor), treat it as a patch release and run `npm run release:patch`. -- All workspaces share one version by design. Keep versions synchronized and release together. -- The website Mac download CTA URL is derived from `packages/website/package.json` version at build time, so no manual update is required after release. - -Release completion checklist: -- Manually update CHANGELOG.md with release notes, between current release vs previous one, use Git commands to figure out what changed. The notes are user-facing: - - Ask yourself, what do Paseo users want to know about? - - Include: New features, bug fixes - - Don't include: Refactors or code changes that are not noticeable by users -- `npm run release:patch` completes successfully. -- GitHub `Desktop Release` workflow for the new `v*` tag is green. -- GitHub `Android APK Release` workflow for the same tag is green. -- EAS `release-mobile.yml` workflow for the same tag is green (Expo queues can take longer on the free plan). - -## Orchestrator Mode - -- **When agent control tool calls fail**, make sure you list agents before trying to launch another one. It could just be a wait timeout. -- **Always prefix agent titles** so we can tell which ones are running under you (e.g., "🎭 Feature Implementation", "🎭 Design Discussion"). -- **Launch agents in the most permissive mode**: Use full access or bypass permissions mode. -- **Set cwd to the repository root** - The agent's working directory should usually be the repo root - -**CRITICAL: ALWAYS RUN TYPECHECK AFTER EVERY CHANGE.** - -## Agent Authentication - -All agent providers (Claude, Codex, OpenCode) handle their own authentication outside of environment variables. They are authenticated without providing any extra configuration—Paseo does not manage API keys or tokens for agents. - -**Do not add auth checks to tests.** If auth fails for whatever reason, let the user know instead of patching the code or adding conditional skips. - -## NEVER DO THESE THINGS - -- **NEVER restart the main Paseo daemon on port 6767 without permission** - This is the production daemon that launches and manages agents. If you are reading this, you are probably running as an agent under it. Restarting it will kill your own process and all other running agents. The daemon is managed by the user in Tmux. -- **NEVER assume a timeout means the service needs restarting** - Timeouts can be transient network issues, not service failures -- **NEVER add authentication checks to tests** - Agent providers handle their own auth. If tests fail due to auth issues, report it rather than adding conditional skips or env var checks +- Prefix agent titles with "🎭" (e.g., "🎭 Feature Implementation") +- Launch agents in the most permissive mode +- Set cwd to the repository root +- When agent control tool calls fail, list agents first — it may be a wait timeout diff --git a/docs/ANDROID.md b/docs/ANDROID.md new file mode 100644 index 000000000..129f509e6 --- /dev/null +++ b/docs/ANDROID.md @@ -0,0 +1,67 @@ +# Android + +## App variants + +Controlled by `APP_VARIANT` in `packages/app/app.config.js` (vanilla Expo, no custom Gradle plugin): + +| Variant | App name | Package ID | +|---|---|---| +| `production` | Paseo | `sh.paseo` | +| `development` | Paseo Debug | `sh.paseo.debug` | + +EAS profiles: `development`, `production`, and `production-apk` in `packages/app/eas.json`. + +`development` uses Android `debug`. + +## Local build + install + +From repo root: + +```bash +npm run android:development # Debug build +npm run android:production # Release build +npm run android:clean # Clean native project +``` + +Or from `packages/app`: + +```bash +# Debug +APP_VARIANT=development npx expo prebuild --platform android --non-interactive +APP_VARIANT=development npx expo run:android --variant=debug + +# Release +APP_VARIANT=production npx expo prebuild --platform android --non-interactive +APP_VARIANT=production npx expo run:android --variant=release + +# Clean +npx expo prebuild --platform android --clean --non-interactive +``` + +## Screenshots + +```bash +adb exec-out screencap -p > screenshot.png +``` + +## Cloud build + submit (EAS) + +Tag pushes like `v0.1.0` trigger: + +- `packages/app/.eas/workflows/release-mobile.yml` on Expo servers (iOS + Android build + submit) +- `.github/workflows/android-apk-release.yml` on GitHub Actions (APK asset on GitHub Release) + +### Useful commands + +```bash +cd packages/app + +# List recent workflow runs +npx eas workflow:runs --workflow release-mobile.yml --limit 10 + +# Inspect a run +npx eas workflow:view + +# Stream logs for a failed job +npx eas workflow:logs --non-interactive --all-steps +``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..8a2334ce7 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,184 @@ +# Architecture + +Paseo is a client-server system for monitoring and controlling local AI coding agents. The daemon runs on your machine, manages agent processes, and streams their output in real time over WebSocket. Clients (mobile app, CLI, desktop app) connect to the daemon to observe and interact with agents. + +Your code never leaves your machine. Paseo is local-first. + +## System overview + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Mobile App │ │ CLI │ │ Desktop App │ +│ (Expo) │ │ (Commander) │ │ (Tauri) │ +└──────┬───────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + │ WebSocket │ WebSocket │ Managed subprocess + │ (direct or │ (direct) │ + WebSocket + │ via relay) │ │ + └───────────┬───────┴──────────────────┘ + │ + ┌──────▼──────┐ + │ Daemon │ + │ (Node.js) │ + └──────┬──────┘ + │ + ┌────────────┼────────────┐ + │ │ │ +┌─────▼─────┐ ┌───▼────┐ ┌────▼─────┐ +│ Claude │ │ Codex │ │ OpenCode │ +│ Agent │ │ Agent │ │ Agent │ +│ SDK │ │ Server │ │ │ +└───────────┘ └────────┘ └──────────┘ +``` + +## Packages + +### `packages/server` — The daemon + +The heart of Paseo. A Node.js process that: + +- Listens for WebSocket connections from clients +- Manages agent lifecycle (create, run, stop, resume, archive) +- Streams agent output in real time via a timeline model +- Exposes an MCP server for agent-to-agent control +- Optionally connects outbound to a relay for remote access + +**Key modules:** + +| Module | Responsibility | +|---|---| +| `bootstrap.ts` | Daemon initialization: HTTP server, WS server, agent manager, storage, relay | +| `websocket-server.ts` | WebSocket connection management, hello/welcome handshake, binary multiplexing | +| `session.ts` | Per-client session state, timeline subscriptions, terminal operations | +| `agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management | +| `agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` | +| `agent/mcp-server.ts` | MCP server for sub-agent creation, permissions, timeouts | +| `providers/` | Provider adapters: Claude (Agent SDK), Codex (AppServer), OpenCode | +| `relay-transport.ts` | Outbound relay connection with E2E encryption | +| `client/daemon-client.ts` | Client library for connecting to the daemon (used by CLI and app) | + +### `packages/app` — Mobile + web client (Expo) + +Cross-platform React Native app that connects to one or more daemons. + +- Expo Router navigation (`/h/[serverId]/agents`, etc.) +- `DaemonRegistryContext` manages saved daemon connections +- `SessionContext` wraps the daemon client for the active session +- `Stream` model handles timeline with compaction, gap detection, sequence-based deduplication +- Voice features: dictation (STT) and voice agent (realtime) + +### `packages/cli` — Command-line client + +Commander.js CLI with Docker-style commands: + +- `paseo agent ls/run/stop/logs/inspect/wait/send/attach` +- `paseo daemon start/stop/restart/status/pair` +- `paseo permit allow/deny/ls` +- `paseo provider ls/models` +- `paseo worktree ls/archive` + +Communicates with the daemon via the same WebSocket protocol as the app. + +### `packages/relay` — E2E encrypted relay + +Enables remote access when the daemon is behind a firewall. + +- ECDH key exchange + AES-256-GCM encryption +- Relay server is zero-knowledge — it routes encrypted bytes, cannot read content +- Client and daemon channels with identical API (`createClientChannel`, `createDaemonChannel`) +- Pairing via QR code transfers the daemon's public key to the client + +See [SECURITY.md](../SECURITY.md) for the full threat model. + +### `packages/desktop` — Desktop app (Tauri) + +Tauri wrapper for macOS, Linux, and Windows. + +- Can spawn the daemon as a managed subprocess +- Native file access for workspace integration +- Same WebSocket client as mobile app + +### `packages/website` — Marketing site + +TanStack Router + Cloudflare Workers. Serves paseo.sh. + +## WebSocket protocol + +All clients speak the same binary-multiplexed WebSocket protocol. + +**Handshake:** + +``` +Client → Server: WSHelloMessage { id, clientId, version, timestamp } +Server → Client: WSWelcomeMessage { clientId, daemonVersion, sessionId, capabilities } +``` + +**Message types:** + +- `agent_update` — Agent state changed (status, title, labels) +- `agent_stream` — New timeline event from a running agent +- `workspace_update` — Workspace state changed +- `agent_permission_request` — Agent needs user approval for a tool call +- Command-response pairs for fetch, list, create, etc. + +**Binary multiplexing:** + +Terminal I/O and agent streaming share the same connection via `BinaryMuxFrame`: +- Channel 0: control messages +- Channel 1: terminal data +- 1-byte channel ID + 1-byte flags + variable payload + +## Agent lifecycle + +``` +initializing → idle → running → idle (or error → closed) + ↑ │ + └────────┘ (agent completes a turn, awaits next prompt) +``` + +- **AgentManager** tracks up to 200 timeline items per agent +- Timeline is append-only with epochs (each run starts a new epoch) +- Events stream to all subscribed clients in real time +- Agent state persists to `$PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json` + +## Agent providers + +Each provider implements a common `AgentClient` interface: + +| Provider | Wraps | Session format | +|---|---|---| +| Claude | Anthropic Agent SDK | `~/.claude/projects/{cwd}/{session-id}.jsonl` | +| Codex | CodexAppServer | `~/.codex/sessions/{date}/rollout-{ts}-{id}.jsonl` | +| OpenCode | OpenCode CLI | Provider-managed | + +All providers: +- Handle their own authentication (Paseo does not manage API keys) +- Support session resume via persistence handles +- Map tool calls to a normalized `ToolCallDetail` type +- Expose provider-specific modes (plan, default, full-access) + +## Data flow: running an agent + +1. Client sends `CreateAgentRequestMessage` with config (prompt, cwd, provider, model, mode) +2. Session routes to `AgentManager.create()` +3. AgentManager creates a `ManagedAgent`, initializes provider session +4. Provider runs the agent → emits `AgentStreamEvent` items +5. Events append to the agent timeline, broadcast to all subscribed clients +6. Tool calls are normalized to `ToolCallDetail` (shell, read, edit, write, search, etc.) +7. Permission requests flow: agent → server → client → user decision → server → agent + +## Storage + +``` +$PASEO_HOME/ +├── agents/{cwd-with-dashes}/{agent-id}.json # Agent state + config +├── projects/projects.json # Project registry +├── projects/workspaces.json # Workspace registry +└── daemon.log # Daemon trace logs +``` + +## Deployment models + +1. **Local daemon** (default): `paseo daemon start` on `127.0.0.1:6767` +2. **Managed desktop**: Tauri app spawns daemon as subprocess +3. **Remote + relay**: Daemon behind firewall, relay bridges with E2E encryption diff --git a/docs/CODING_STANDARDS.md b/docs/CODING_STANDARDS.md new file mode 100644 index 000000000..ba06083e6 --- /dev/null +++ b/docs/CODING_STANDARDS.md @@ -0,0 +1,174 @@ +# Coding Standards + +These standards apply to all code changes: features, bug fixes, refactors, and performance work. + +## Core principles + +- **Zero complexity budget** — justify every abstraction with specific benefits +- **Fully typed TypeScript** — no `any`, no untyped boundaries +- **YAGNI** — build features and abstractions only when needed +- **Functional and declarative** over object-oriented +- **`interface`** over `type` when possible +- **`function` declarations** over arrow function assignments +- **Single-purpose functions** — one function, one job +- **Design for edge cases through types** rather than explicit handling +- **Don't catch errors** unless there's a strong reason to +- **No index.ts barrel files** that only re-export — they create unnecessary indirection +- **No "while I'm at it" improvements** — stay focused on the task + +## Type hygiene + +### Infer from schemas + +Never hand-write a TypeScript type that can be inferred from a Zod schema. + +```typescript +// Bad: duplicate type that can drift +const schema = z.object({ procedure: z.string(), args: z.record(z.unknown()) }); +type RPCArgs = { procedure: string; args: Record }; + +// Good: infer from schema +type RPCArgs = z.infer; +``` + +### Named types over inline + +No complex inline types in public function signatures. + +```typescript +// Bad +function enqueueJob(input: { userId: string; priority: "low" | "normal" | "high" }) {} + +// Good +interface EnqueueJobInput { userId: string; priority: "low" | "normal" | "high" } +function enqueueJob(input: EnqueueJobInput) {} +``` + +### Object parameters + +If a function needs more than one argument, use a single object parameter. + +```typescript +// Bad: positional args +function createToolCall(provider: string, toolName: string, payload: unknown) {} + +// Good: object param +interface CreateToolCallInput { provider: string; toolName: string; payload: unknown } +function createToolCall(input: CreateToolCallInput) {} +``` + +### One canonical type per concept + +Don't redefine the same concept in different layer-specific shapes (`RpcX`, `DbX`, `UiX`). Keep one canonical type and add explicit layer wrappers that reference it. + +```typescript +// Bad: duplicated fields across layers +type RpcToolCall = { toolName: string; args: Record; requestId: string }; +type DbToolCall = { toolName: string; args: Record; id: string; createdAt: Date }; + +// Good: canonical type + wrappers +type ToolCall = { toolName: string; args: Record }; +type ToolCallRequest = { requestId: string; toolCall: ToolCall }; +type ToolCallRecord = { id: string; createdAt: Date; toolCall: ToolCall }; +``` + +## Make impossible states impossible + +Use discriminated unions instead of bags of booleans and optionals. + +```typescript +// Bad +interface FetchState { isLoading: boolean; error?: Error; data?: Data } + +// Good +type FetchState = + | { status: "idle" } + | { status: "loading" } + | { status: "error"; error: Error } + | { status: "success"; data: Data }; +``` + +## Optionality is a design decision + +Don't mark fields optional to avoid migrations. Decide deliberately: + +1. Is optionality actually needed? +2. If there are distinct valid states → discriminated union +3. If value can be intentionally empty → explicit `null` +4. Keep optionality at real boundaries (external input), then resolve it + +## Validate at boundaries, trust internally + +Parse external data once at the boundary with schema validation. Then use typed values everywhere else. + +```typescript +// Bad: optional chaining because shape is unclear +const value = response?.data?.items?.[0]?.name; + +// Good: validate at boundary, trust the types +const parsed = responseSchema.parse(rawResponse); +const value = parsed.data.items[0].name; +``` + +## Error handling + +- **Fail explicitly** — if caller requests X and X is unavailable, throw rather than silently returning Y +- **Use typed domain errors** — not plain `Error`. Carry structured metadata for handling, logging, and user messaging +- **Preserve error semantics** — don't collapse meaningful typed errors into generic `Error` + +```typescript +class TimeoutError extends Error { + constructor( + public readonly operation: string, + public readonly waitedMs: number, + ) { + super(`${operation} timed out after ${waitedMs}ms`); + this.name = "TimeoutError"; + } +} +``` + +## Keep logic density low + +Avoid packing branching, lookup, and transformation into single dense expressions. + +```typescript +// Bad: nested ternaries + inline lookups +const billing = shouldUseLegacy(account) + ? getLegacy(account) + : buildBilling(account, rates.find((r) => r.region === account.region)); + +// Good: named steps, then assemble +const rate = rates.find((r) => r.region === account.region); +if (!rate) throw new MissingRateError(account.region); +const billing = shouldUseLegacy(account) ? getLegacy(account) : buildBilling(account, rate); +``` + +## Centralize policy + +When the same discriminator (`plan`, `provider`, `kind`, `status`) is checked across multiple files, centralize it into a policy model. A new case should require editing one place, not many. + +## React: keep components dumb + +- Components render state and dispatch events — they don't compute transitions +- If a component has more than two interacting `useState` calls, extract a state machine or reducer +- `useRef` for mutable coordination state (flags, timers) is a smell — model states explicitly +- Never mirror a source of truth into local state; derive from it +- Test state logic as pure functions without rendering + +## File organization + +- Organize by domain first (`providers/claude/`), not by technical type (`tool-parsers/`) +- Name files after the main export (`create-toolcall.ts`) +- Use `index.ts` as an entrypoint, not a dumping ground +- Collocate tests with implementation (`thing.ts` + `thing.test.ts`) + +## Refactoring contract + +Refactoring is structure work, not feature work. + +- Preserve behavior by default, especially user-facing behavior +- Do not remove features to simplify code without explicit approval +- Have a verification strategy before you start +- Fully migrate callers and remove old paths in the same refactor +- No fallback behavior by default — prefer explicit error over silent degradation diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 000000000..b44f5b7be --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,73 @@ +# Designing Features + +How to think through a feature before writing code. + +## Start from the user + +Even for backend work, start from the user's perspective: + +- What problem does this solve? +- What triggers it? User action, schedule, event? +- What does success look like from the user's perspective? +- What data does it need? Where does that data come from? + +## Map existing code + +Before designing anything new, understand what exists: + +- Where does similar functionality live? +- What patterns does the codebase already use? +- What layers exist? (See [ARCHITECTURE.md](./ARCHITECTURE.md)) +- What types and data shapes are already defined? + +New features rarely mean only new code. Usually they require modifying existing interfaces, extending existing types, or refactoring to accommodate the new functionality. Identify what needs to change, not just what needs to be added. + +## Define verification before implementation + +Before designing the solution, define how you'll know it works: + +- What tests will prove this feature is correct? +- At what layer? Unit, integration, E2E? +- What's the simplest way to verify the core behavior? + +If you can't define verification, you don't understand the feature well enough yet. + +## Design the shape + +### Data + +- What types are needed? +- Use discriminated unions — make impossible states impossible +- One canonical type per concept (see [CODING_STANDARDS.md](./CODING_STANDARDS.md)) + +### Layers + +- What belongs in each layer? +- Where are the boundaries? +- What does each layer expose to the layer above? + +### Interactions + +- How does data flow through the system? +- What triggers what? +- Where do side effects happen? + +### Refactoring + +- What existing code needs to change? +- Is existing code testable enough? If not, that's part of the plan. + +## Create a concrete plan + +Once the design is clear: + +1. **Acceptance criteria** — specific, verifiable outcomes (not "should work well" but "returns X when given Y") +2. **Ordered steps** — what to build first (usually: types, then lowest layer, then up) +3. **What to refactor** before adding new code +4. **How to verify** each step + +## Principles + +- **Fit, don't force** — new code should fit existing patterns, or refactor first +- **Simple** — the best design is the simplest one that works +- **Verify early** — define how to test before designing the implementation diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 000000000..30f617ccc --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,130 @@ +# Development + +## Prerequisites + +- Node.js (see `.tool-versions` for exact version) +- npm workspaces (comes with Node) + +## Running the dev server + +```bash +npm run dev +``` + +The dev script automatically picks an available port. Both the server and Expo app run in a Tmux session — see `CLAUDE.local.md` for system-specific session details. + +### Running alongside the main checkout + +Set `PASEO_HOME` to isolate state when running a second instance (e.g., in a worktree): + +```bash +PASEO_HOME=~/.paseo-blue npm run dev +``` + +- `PASEO_HOME` — path for runtime state (agents, sockets, etc.). Defaults to `~/.paseo`. + +### Default ports + +In the main checkout: +- Daemon: `localhost:6767` +- Expo app: `localhost:8081` + +In worktrees or with `npm run dev`, ports may differ. Never assume defaults. + +### Daemon logs + +Check `$PASEO_HOME/daemon.log` for trace-level logs. + +## Build sync gotchas + +### Relay → Daemon + +When changing `packages/relay/src/*`, rebuild before running the daemon: + +```bash +npm run build --workspace=@getpaseo/relay +``` + +The Node daemon imports `@getpaseo/relay` from `packages/relay/dist/*`, not `src/*`. + +### Server → CLI + +When changing `packages/server/src/client/*` (especially `daemon-client.ts`) or shared WS protocol types, rebuild before running CLI commands: + +```bash +npm run build --workspace=@getpaseo/server +``` + +The CLI imports `@getpaseo/server` via package exports resolving to `dist/*`. Stale `dist` means the CLI speaks an old protocol and fails with handshake warnings or timeouts. + +## CLI reference + +Use `npm run cli` to run the local CLI (instead of the globally installed `paseo` which points to the main checkout). + +```bash +npm run cli -- ls -a -g # List all agents globally +npm run cli -- ls -a -g --json # Same, as JSON +npm run cli -- inspect # Show detailed agent info +npm run cli -- logs # View agent timeline +npm run cli -- daemon status # Check daemon status +``` + +Use `--host ` to point the CLI at a different daemon: + +```bash +npm run cli -- --host localhost:7777 ls -a +``` + +## Agent state + +Agent data lives at: + +``` +$PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json +``` + +Find an agent by ID: +```bash +find $PASEO_HOME/agents -name "{agent-id}.json" +``` + +Find by content: +```bash +rg -l "some title text" $PASEO_HOME/agents/ +``` + +## Provider session files + +Get the session ID from the agent JSON (`persistence.sessionId`), then: + +**Claude:** +``` +~/.claude/projects/{cwd-with-dashes}/{session-id}.jsonl +``` + +**Codex:** +``` +~/.codex/sessions/{YYYY}/{MM}/{DD}/rollout-{timestamp}-{session-id}.jsonl +``` + +## Testing with Playwright MCP + +Use Playwright MCP connecting to Metro at `http://localhost:8081` for UI testing. + +Do NOT use browser history (back/forward). Always navigate by clicking UI elements or using `browser_navigate` with the full URL — the app uses client-side routing and browser history breaks state. + +## Expo troubleshooting + +```bash +npx expo-doctor +``` + +Diagnoses version mismatches and native module issues. + +## Typecheck + +Always run typecheck after changes: + +```bash +npm run typecheck +``` diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 000000000..1492a87c6 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,48 @@ +# Release + +All workspaces share one version and release together. + +## Standard release (patch) + +```bash +npm run release:patch +``` + +This bumps the version across all workspaces, runs checks, publishes to npm, and pushes the branch + tag (triggering desktop, APK, and EAS mobile workflows). + +If asked to "release paseo" without specifying major/minor, treat it as a patch release. + +## Manual step-by-step + +```bash +npm run version:all:patch # Bump version, create commit + tag +npm run release:check # Validate release +npm run release:publish # Publish to npm +npm run release:push # Push HEAD + tag (triggers CI workflows) +``` + +## Draft release flow + +```bash +npm run draft-release:patch # Bump, push tag, create draft GitHub Release +npm run release:finalize # Publish npm, promote draft to published +``` + +- `draft-release:patch` creates the GitHub Release as a draft so desktop assets, APK uploads, and synced notes attach to it +- `release:finalize` publishes npm and promotes the same draft release +- Use the same semver tag for both; don't cut a second tag + +## Notes + +- `version:all:*` bumps root + syncs workspace versions and `@getpaseo/*` dependency versions +- `release:prepare` refreshes workspace `node_modules` links to prevent stale types +- If `release:publish` partially fails, re-run it — npm skips already-published versions +- Website Mac download CTA URL derives from `packages/website/package.json` version at build time + +## Completion checklist + +- [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors) +- [ ] `npm run release:patch` completes successfully +- [ ] GitHub `Desktop Release` workflow for the `v*` tag is green +- [ ] GitHub `Android APK Release` workflow for the same tag is green +- [ ] EAS `release-mobile.yml` workflow for the same tag is green diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 000000000..18d51a3c2 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,123 @@ +# Testing + +## Philosophy + +Tests prove behavior, not structure. Every test should answer: "what user-visible or API-visible behavior does this verify?" + +## Test-driven development + +Work in vertical slices: one test, one implementation, repeat. Each test responds to what you learned from the previous cycle. + +``` +RIGHT (vertical): + RED→GREEN: test1→impl1 + RED→GREEN: test2→impl2 + RED→GREEN: test3→impl3 + +WRONG (horizontal): + RED: test1, test2, test3, test4, test5 + GREEN: impl1, impl2, impl3, impl4, impl5 +``` + +Writing all tests first then all implementation produces bad tests — you end up testing imagined behavior instead of actual behavior. + +## Determinism first + +Tests must produce the same result every run: + +- No conditional assertions or branching paths +- No reliance on timing, randomness, or network jitter +- No weak assertions (`toBeTruthy`, `toBeDefined`) +- Assert the full intended behavior, not fragments + +```typescript +// Bad: conditional and weak +it("creates a tool call", async () => { + const result = await createToolCall(input); + if (result.ok) { + expect(result.id).toBeDefined(); + } +}); + +// Good: deterministic and explicit +it("returns timeout error when provider times out", async () => { + const result = await createToolCall(input); + expect(result).toEqual({ + ok: false, + error: { code: "PROVIDER_TIMEOUT", waitedMs: 30000 }, + }); +}); +``` + +## Flaky tests are a bug + +Never remove a test because it's flaky. Find the variance source (time, randomness, race condition, shared state, non-deterministic output, environment drift) and fix it. + +## Real dependencies over mocks + +Mocks are not the default. They require an explicit decision. + +- **Database**: real test database, not a mock +- **APIs**: real APIs with test/sandbox credentials, not request mocks +- **File system**: temporary directory that gets cleaned up, not fs mocks + +Ask: "will this still hold with real dependencies at runtime?" If no, don't mock. + +### Use swappable adapters instead + +When you need test isolation, design code so dependencies are injectable: + +```typescript +interface EmailSender { + send(to: string, body: string): Promise; +} + +// Production +const realSender: EmailSender = { send: sendgrid.send }; + +// Test: in-memory adapter +function createTestEmailSender() { + const sent: Array<{ to: string; body: string }> = []; + return { + send: async (to: string, body: string) => { sent.push({ to, body }); }, + sent, + }; +} +``` + +## End-to-end means end-to-end + +When a test is labeled end-to-end, it calls the real service. No environment variable gates, no conditional skipping, no mocking the external dependency. + +## Test organization + +- Collocate tests with implementation: `thing.ts` + `thing.test.ts` +- Extract complex setup into reusable helpers +- Test bodies should read like plain English +- Build a vocabulary of test helpers that make complex flows simple + +## Agent authentication in tests + +Agent providers handle their own auth. Do not add auth checks, environment variable gates, or conditional skips to tests. If auth fails, report it. + +## Debugging with tests + +Use the test as your debugging ground: + +1. Add temporary logging to the code under test +2. Run the test, observe actual values +3. Trace the flow end-to-end through test output +4. Confirm each assumption with actual output +5. Remove logging when done + +The test output is the source of truth, not your reading of the code. + +## Design for testability + +If code isn't testable, refactor it. Signs: +- You want to reach for a mock +- You can't inject a dependency +- You need to test private internals +- Setup requires too much global state + +Aim for deep modules: small interface, deep implementation. Fewer methods = fewer tests needed, simpler params = simpler setup.