# Pad — Development Guide ## What This Is Pad is a project management tool for developers and AI agents. Single Go binary with embedded SvelteKit web UI, SQLite storage, and multi-agent skill support (Claude Code, Cursor, Windsurf, Codex, OpenCode, Copilot, Amazon Q, Junie). **Related repo:** The marketing website (getpad.dev) lives at `../pad-web` — a separate SvelteKit site deployed to Vercel. ## Architecture - **Backend:** Go (cmd/pad/main.go) → REST API (internal/server/) → SQLite (internal/store/) - **Frontend:** SvelteKit 2 + Svelte 5 (web/src/) → static build embedded in Go binary - **Data model:** Workspaces → Collections (typed with JSON schemas) → Items (structured fields + rich content) - **CLI:** Cobra commands in cmd/pad/main.go, HTTP client in internal/cli/ - **Agent skill:** Single natural-language `/pad` skill in skills/pad/SKILL.md ## Build & Install ```bash make build # Build web UI + Go binary (./pad) make install # Build, kill server, install to ~/.local/bin/pad, restart make build-go # Build Go only (skip web — faster when only backend changes) make test # Run Go tests make web # Build web UI only make dev-web # Run SvelteKit dev server (hot reload on :5173) ``` **After making changes, always run `make install`** to rebuild the binary, install it, and restart the server. The web UI at http://localhost:7777 will reflect the changes. ### Quick iteration loop - **Backend only:** `make install` (skips web rebuild if no frontend changes — edit Makefile to use `build-go` instead of `build` in the install target) - **Frontend only:** `make web && make install` or use `make dev-web` for hot reload during development - **Full rebuild:** `make install` ## Key Directories ``` cmd/pad/main.go — CLI entry point, all Cobra commands internal/ server/ — HTTP API handlers, SSE, middleware store/ — SQLite CRUD, migrations, FTS models/ — Go types (Collection, Item, View, etc.) items/ — Field validation against schemas collections/ — Default definitions, workspace templates cli/ — HTTP client, formatting helpers events/ — EventBus for real-time SSE config/ — Workspace detection, .pad.toml diff/ — Version diff storage webhooks/ — Webhook dispatcher with HMAC signing email/ — Transactional email via Maileroo links/ — Wiki-link parsing web/src/ routes/ — SvelteKit pages lib/api/client.ts — TypeScript API client lib/types/index.ts — TypeScript types lib/stores/ — Svelte 5 rune stores lib/components/ — Reusable UI components skills/pad/SKILL.md — Claude Code skill (embedded in binary) ``` ## API REST API at `/api/v1/`. Key endpoints: - `GET/POST /workspaces/{ws}/collections` — collection CRUD - `GET/POST /workspaces/{ws}/collections/{coll}/items` — item CRUD - `GET/PATCH/DELETE /workspaces/{ws}/items/{slug}` — item by slug - `POST /workspaces/{ws}/items/{slug}/copy/preflight` — cross-workspace copy dry run: what would carry / drop / need a value, plus the full warning set. Read-only and safe to call repeatedly (PLAN-2357) - `POST /workspaces/{ws}/items/{slug}/copy` — cross-workspace copy; with `archive_source` it is the move. Same request shape as the preflight. **Never retry it automatically** — there is no idempotency key, so a retry duplicates the item - `GET /workspaces/{ws}/dashboard` — computed project overview (active items, plans, attention, blockers) - `GET /workspaces/{ws}/activity` — workspace activity feed (enriched with item titles + change details) - `GET/POST/DELETE /workspaces/{ws}/webhooks` — webhook management - `GET /workspaces/{ws}/items/{slug}/children` — child items linked to a parent - `GET /workspaces/{ws}/items/{slug}/progress` — child item completion progress - `GET/POST /workspaces/{ws}/items/{slug}/links` — item relationships (blocks/blocked-by, parent/child) - `GET /search?q=query&workspace=slug` — full-text search - `GET /api/v1/events?workspace=slug` — SSE real-time events - `GET /api/v1/collab/{itemID}?schema_version=N` — WebSocket upgrade for real-time collaborative editing (Yjs binary protocol; client must announce schema version) - `GET /workspaces/{ws}/members` — list members + pending invitations - `POST /workspaces/{ws}/members/invite` — invite user to workspace - `GET /api/v1/auth/session` — auth status (`setup_required`, `setup_method`, `auth_method`, `authenticated`, `email_configured`, `user`) - `POST /api/v1/auth/bootstrap` — create the first admin account from localhost on a fresh instance - `POST /api/v1/auth/register` — create account (admin-created or invitation-based after setup) - `POST /api/v1/auth/login` — email/password login (returns session token) - `POST /api/v1/auth/logout` — destroy session - `GET/PATCH /api/v1/auth/me` — current user profile (GET) and update name/password (PATCH) - `POST /api/v1/auth/forgot-password` — request password reset email - `POST /api/v1/auth/reset-password` — reset password with token - `POST /api/v1/auth/local-reset` — localhost-only account recovery (self-host, non-cloud). Loopback-gated, no auth — the bootstrap trust model. Returns a single-use reset link, or a temp password with `{"temp_password": true}`. Backs `pad auth reset-password`. - `GET/POST/DELETE /api/v1/auth/tokens` — user-scoped API tokens - `GET/PATCH /api/v1/admin/settings` — platform settings (admin-only) - `POST /api/v1/admin/test-email` — send test email (admin-only) - `POST /api/v1/invitations/{code}/accept` — accept workspace invitation - `GET /api/v1/workspaces/{ws}/agent/bootstrap` — one-round-trip agent context (workspace + user + collections + always-on conventions + roles + playbook metadata + dashboard + `needs_onboarding` flag). Same payload as the MCP `pad://workspace/{ws}/bootstrap` resource and the `pad_set_workspace` embed. ## Authentication User-based authentication with email/password. When no users exist (fresh install), everything works without auth until the instance is initialized with `pad auth setup`. Once the first admin exists, all API requests require authentication. ```bash # First-time setup pad auth setup # Create the first admin account on the server host # Subsequent logins pad auth login # Email + password prompt pad auth whoami # Show current user pad auth logout # Sign out pad auth reset-password user@example.com # Recover a locked-out account (run ON THE SERVER HOST) pad auth reset-password user@example.com --temp-password # ...set a temp password instead of a reset link # Credentials stored in ~/.pad/credentials.json (0600 permissions) # CLI auto-attaches auth token to all API requests ``` ### Locked-out account recovery (self-host, no email) When a self-hosted instance has no email provider, a forgotten password can't be reset by email. Two host-side recovery paths (both require shell access to the server — the same trust boundary as `pad auth setup`): - **`pad auth reset-password `** — run it **on the server host**. It calls the loopback-only `/api/v1/auth/local-reset` endpoint (no login required — that's the point) and prints a single-use reset link. Add `--temp-password` to instead set a random temporary password printed to the terminal (headless boxes with no browser). The endpoint refuses proxied/remote requests and is disabled in cloud mode. - **Server log** — if a user submits the web `/forgot-password` form on a non-cloud instance with no email, the server logs the reset path (`slog.Info ... reset_path=/reset-password/`). Paste it after the instance's base URL to finish the reset by hand. The web `/forgot-password` page detects `email_configured == false` (from the session payload) and shows the `pad auth reset-password` recovery instructions instead of a dead "we emailed you a link" message. Code: `internal/server/handlers_auth.go::handleLocalReset` (loopback + non-cloud gates), `cmd/pad/main.go::resetPasswordCmd`, `web/src/routes/forgot-password/+page.svelte`. After any workspace is created (via `pad init` or `pad workspace init` — note that `pad auth setup` only creates the admin account, not a workspace), the success output points new users at the canonical onboarding entry point. Open a fresh agent session in the workspace's directory and say: ``` /pad onboard ``` Every new workspace ships with the `onboard` playbook auto-activated (PLAN-1496 / TASK-1499 / TASK-1500). The playbook walks the agent through an interview that adapts the workspace's collections, conventions, roles, and seeded playbooks to match the actual project. Works regardless of which template the user picked (or no template — see the `blank` template). The pre-PLAN-1496 design seeded `IDEA-1` / `PLAN-2` / `TASK-3` / `DOC-4` (and `BACK-1` / `FEAT-1` siblings for scrum/product) as first-person-future-self notes; that pattern was retired in TASK-1501 / TASK-1502 in favor of the playbook-driven flow. ### Workspace membership ```bash pad workspace members # List workspace members pad workspace invite user@example.com # Invite (adds directly if user exists, creates join code if not) pad workspace invite user@example.com --role viewer # Invite with specific role pad workspace join # Accept a workspace invitation ``` Roles: `owner` (full access), `editor` (CRUD items), `viewer` (read-only). ### Email (optional) Transactional email via Maileroo. When configured, workspace invitations are sent by email. Without it, everything works via CLI-based join codes. ```bash # Environment variables (or ~/.pad/config.toml) PAD_MAILEROO_API_KEY=your-sending-key # Required to enable email PAD_EMAIL_FROM=noreply@yourdomain.com # Sender address (default: noreply@getpad.dev) PAD_EMAIL_FROM_NAME=Pad # Sender display name (default: Pad) ``` ## CLI Items are referenced by **issue ID** (e.g. `TASK-5`, `BUG-8`) wherever a `` argument appears. Slugs also work but issue IDs are preferred. ```bash pad item create "title" [--status X] [--priority X] [--parent REF] pad item list [collection] [--status X] [--parent REF] [--all] pad item show # e.g. pad item show TASK-5 pad item update [--status X] [--priority X] pad item delete pad item move pad item copy --to-workspace --collection [--dry-run] [--archive-source] [--field k=v] # Cross-workspace copy; --archive-source makes it a move. # --dry-run previews the field mapping + warnings. # Refuses rather than guessing when a destination field needs a value, # and NEVER retries the mutating call (no idempotency key — PLAN-2357 DR-13). # Content semantics: markdown is copied verbatim except `pad-attachment:` # refs that resolve to a LIVE attachment in the SOURCE workspace — those are # repointed at the clones (+ variants). Foreign / soft-deleted / dangling ids # are left literal and counted as unresolvable, never cloned. # `[[wiki-links]]` are NOT rewritten — they re-resolve in the DESTINATION, # so a link can silently retarget to a different item or break; # `[[workspace::REF]]` stays a genuine cross-workspace reference. # The web dialog (item pane ⋯ → "Copy or move to workspace…") says the same. pad item search "query" pad project dashboard # Project dashboard pad project next # Recommended next task pad project standup [--days N] # Daily standup report pad project changelog [--days N] [--parent REF] # Release notes from completed items pad item block # e.g. pad item block TASK-5 TASK-8 pad item blocked-by pad item deps # Show dependencies pad item unblock pad collection list # List collections pad collection create "Name" --fields "key:type[:opts]; ..." # compact DSL for simple schemas pad collection create "Name" --schema '' # full CollectionSchema (terminal_options, defaults, computed, relations) pad item edit # Open in $EDITOR pad workspace init [--template X] # Create workspace pad agent install [tool] # Install /pad skill for AI tools # Workspace onboarding: run `/pad onboard` from an agent session inside the # workspace (Claude Code, MCP, etc.). The /pad onboard playbook is # auto-seeded into every new workspace. pad server open # Open web UI in browser pad project watch # Real-time activity stream pad github link [item-ref] # Link current branch's PR to item pad github status [item-ref] # Show PR status for linked items pad github unlink # Remove PR link from item pad item bulk-update --status done TASK-5 TASK-8 # Batch operations pad webhook list/create/delete/test # Webhook management pad auth setup # Initialize a fresh instance with the first admin pad auth login # Log in pad auth logout # Sign out pad auth whoami # Show current user pad workspace members # List workspace members pad workspace invite [--role X] # Invite user to workspace pad workspace join # Accept workspace invitation ``` Collection names accept singular forms: `task`→`tasks`, `idea`→`ideas`, `doc`→`docs`. ## MCP server Pad runs as a local Model Context Protocol server so Claude Desktop / Cursor / Windsurf can call non-interactive `pad` commands as tools. The tool surface is a **hand-curated catalog** (currently v0.15) in `internal/mcp/catalog_*.go` — one ToolDef per resource (`pad_item`, `pad_workspace`, `pad_collection`, `pad_project`, `pad_role`, `pad_search`, `pad_meta`, `pad_playbook`, `pad_library`, `pad_attachment`) with an `action` enum dispatching to underlying CLI commands. v0.15 adds the `pad_item.list` `unparented` boolean, mutually exclusive with `parent`, for items with no parent or implements relationship (TASK-2096). v0.2 introduced the catalog (PLAN-969 / TASK-981); v0.3 added `pad_playbook`, `pad_meta.action: bootstrap`, `pad_set_workspace`'s embedded-bootstrap response, and the `pad://workspace/{ws}/bootstrap` resource (PLAN-1377 / TASK-1380); v0.4 trimmed the bootstrap payload by ~40% (PLAN-1410) — slim `BootstrapCollection` + `BootstrapRole` projections (no UUIDs/timestamps/settings; nested `schema` object; redundant labels omitted), removed top-level `recent_activity` duplicate, dropped convention `slug`, and added a `BootstrapDashboard` wrapper that caps five sub-arrays (`attention`, `recent_activity`, `active_items`, `active_plans`, `by_role`) at 5 entries each with parallel `*_overflow_count` fields. The pre-catalog v0.1 cmdhelp leaf walker is retired. cmdhelp is still consumed at dispatch time — `BuildCLIArgs` reads individual command schemas to translate the catalog's snake_case input map into CLI args. cmdhelp no longer drives tool naming or count. **When adding a new `pad` command, decide whether it belongs on the MCP surface.** If yes, add an action to the appropriate `pad_` ToolDef in `internal/mcp/catalog_.go`. The action's handler — usually `passThrough([]string{"resource", "subcommand"})` — wires it through to dispatch. Don't expose interactive (prompts the user), destructive (mutates auth / filesystem state), long-running (streaming watcher), or recursive (would spawn another MCP server) commands. ```bash pad mcp serve # JSON-RPC over stdio (called by clients) pad mcp install # Write the client's mcp.json entry pad mcp uninstall # Remove the entry pad mcp status # Install state across supported clients ``` Surface: - **Tools:** the v0.15 catalog — ten resource × action tools (`pad_item`, `pad_workspace`, `pad_collection`, `pad_project`, `pad_role`, `pad_search`, `pad_meta`, `pad_playbook`, `pad_library`, `pad_attachment`) plus `pad_set_workspace` (takes a `workspace` slug only — no action enum). The ten resource × action tools take `action: ` to choose what they do. `pad_item` (v0.15) adds the `unparented` list parameter; v0.14 added `history` + `expected_updated_at`. `pad_project` (v0.13) adds `ready` (actionable backlog) + `stale` (items needing attention); `pad_project.activity` (v0.12) is the non-streaming, bounded activity feed — catch up on what other agents/users changed since you last worked. `pad_attachment` is the read-only attachment-metadata surface — `list`/`show` (upload/download/view stay CLI-only). `pad_library` is the convention+playbook library surface — `list`/`get`/`activate`. `pad_playbook` is the playbook surface from PLAN-1377 — `list`/`get`/`run` mirror the CLI's `pad playbook` subcommands; `run` is side-effect-free and returns the body + bound args for the agent to execute. v0.4 (PLAN-1410) didn't change the tool/action surface; it trimmed the bootstrap JSON those tools/resources return — see the Stability contract subsection below for details. - **Resources:** `pad://workspace/{ws}/items/{ref}`, `pad://workspace/{ws}/items`, `pad://workspace/{ws}/dashboard`, `pad://workspace/{ws}/collections`, `pad://workspace/{ws}/attachments/{id}` (bounded base64 image via `thumb-md`; non-images and image bytes over 1 MiB (pre-base64) rejected), `pad://workspace/{ws}/bootstrap` (one-shot workspace overview — user + collections + always-on conventions + roles + playbook metadata + dashboard + recent activity), plus the server-wide `pad://_meta/version`. - **Prompts:** `pad_plan`, `pad_ideate`, `pad_retro`, `pad_onboard` — multi-step workflows lifted from `skills/pad/SKILL.md`. **`pad_set_workspace`** pins the session default workspace; its response embeds the bootstrap blob so agents pin + load workspace context in one round-trip. The same payload is available on demand via `pad_meta.action: bootstrap` and the `pad://workspace/{ws}/bootstrap` resource. **Stability contract.** Two version constants live in `internal/mcp/version.go`, advertised in the handshake under `capabilities.experimental.padCmdhelp` and `capabilities.experimental.padToolSurface`: - `CmdhelpVersion` (currently `"0.1"`) — the cmdhelp CLI help-tree contract. Bump when CLI flag/arg schemas change incompatibly. - `ToolSurfaceVersion` (currently `"0.15"`) — the MCP tool catalog contract. Bump when tool names, action enums, or parameter shapes change incompatibly. **v0.15** adds the `unparented` boolean to `pad_item.list`, mutually exclusive with `parent`, for structural loose-item filtering (TASK-2096). **v0.14** added a `history` action to `pad_item` (read-only item version history — newest-first metadata; content body omitted for token thrift) and an `expected_updated_at` param for optimistic concurrency on `update` (round-trip the `updated_at` you last read; a stale value fails with a structured 409 `code=update_conflict`). The `update` action's field writes are now a server-side field-level MERGE (only the keys you set change) rather than a full-blob replace, closing the concurrent-update lost-write race (IDEA-1480 / TASK-2022) — pure addition to the action enum + param vocabulary; existing `pad_item` actions/params are unchanged and backwards-compatible. **v0.13** adds `ready` + `stale` actions to `pad_project`, mirroring the existing CLI `pad project ready` / `pad project stale` (TASK-2019): `ready` (read-only) returns the actionable backlog — the query-oriented counterpart to `next`, reusing the dashboard's suggested-next logic; `stale` (read-only) lists items needing attention (stalled, blocked, overdue, or out of the active workflow). Both HTTP dispatchers already existed (`dispatch_http_project.go`); this just wires them onto the catalog. `pad project reconcile` stays CLI-only (shells out to `gh` for live PR state — a local-git dependency MCP agents lack). Pure addition of two read-only actions — existing actions unchanged; backwards-compatible for v0.12 consumers that don't enumerate the new actions. **v0.12** adds an `activity` action to `pad_project`, mirroring the new CLI `pad project activity [--limit N] [--actor user|agent] [--since DATE]` (TASK-2018) — the non-streaming, bounded query counterpart to the CLI-only `pad project watch` SSE stream. Read-only snapshot of the workspace's enriched activity feed (item refs, titles, field-level change details) backed by the existing `GET /workspaces/{ws}/activity` endpoint (previously web-UI-only, now extended with a server-side `since` date filter so `limit`/`actor`/`since` behave identically across CLI, stdio MCP, and cloud HTTP), so agents can catch up on what other agents/users did since they last worked. Adds `actor` + `limit` params to the `pad_project` vocabulary (`since` already existed for changelog); pure addition — existing actions unchanged; backwards-compatible for v0.11 consumers that don't enumerate the new action. **v0.11** adds the read-only `pad_attachment` tool (the tenth resource × action tool) with `list` + `show` actions, mirroring the CLI `pad attachment list` / `pad attachment show` (TASK-2017): `list` enumerates a workspace's attachments (optional filters: item / category / collection / attached / unattached / sort / limit / offset); `show` returns one attachment's metadata (MIME, size, filename, ETag, last-modified) via a HEAD request without transferring bytes. Both HTTP dispatchers already existed (`dispatch_http_attachments.go`); this just wires them onto the catalog. Upload / download / view stay CLI-only (filesystem-bound, excluded per the catalog's exclusion rules). Pure addition — existing tools/actions unchanged; backwards-compatible for v0.10 consumers that don't enumerate the new tool. The base64 image RESOURCE for multimodal agents (`pad://workspace/{ws}/attachments/{id}`) shipped later in TASK-2077 (PR #930) as a bounded, image-only resource; TASK-2101 brought it — and the full read-only resource set — to the remote /mcp transport via the in-process `HTTPResourceFetcher`, so resources are no longer local-stdio-only. **v0.10** enforces the draft-playbook gate server-side: `pad_playbook.run` (and the underlying `POST /playbooks/{ref}/run`) now refuses a playbook whose `status` isn't `active` with a structured `playbook_not_active` error, adds an `allow_draft` boolean param (bareword `--allow-draft` on the CLI) as the escape hatch, and echoes the playbook `status` on both the `run` and `get` responses (BUG-2020). **v0.9** makes `pad_item.list` summary-shaped by default (drops item `content`, adds a default result limit of 50 / hard max 300 on MCP; CLI `--full` restores the complete shape) — a behavior change to the tool's return shape, hence the bump, though tool names, action enums, and parameter shapes are unchanged (TASK-2000). **v0.8** adds `restore` + `deleted` actions to `pad_workspace`, mirroring the CLI `pad workspace restore` / `pad workspace deleted` (TASK-1972): `deleted` (read-only) lists the caller's soft-deleted workspaces still inside the 30-day restore window; `restore` (mutating, not destructive, owner-only) un-soft-deletes a workspace by `slug` while it's still restorable. Both reuse the existing `slug` param — no new params; pure addition. **v0.7** adds `export` + `import` actions to `pad_item`, mirroring the CLI `pad item export` / `pad item import` (covers playbooks AND conventions). `export` (read-only) takes `ref` and returns the portable artifact text — it forces the CLI's stdout sink (`-o -`) so the bytes come back as the result instead of a file. `import` (mutating, not destructive) takes a new `artifact` param (the full artifact text) and returns `{ref, slug, warnings}`; the ExecDispatcher can't pipe stdin, so it spills the artifact to a temp file and dispatches `item import `. v0.6 added the `pad_item.backlinks` action; v0.5 added `pad_library`. v0.3 (PLAN-1377 / TASK-1380) introduced `pad_meta.action: bootstrap`, `pad_set_workspace`'s embedded-bootstrap response, and the `pad://workspace/{ws}/bootstrap` resource. **v0.4 (PLAN-1410)** is a comprehensive bootstrap-payload trim — same tool catalog, slimmer JSON shape inside bootstrap responses: `BootstrapCollection` projection drops `id`/`workspace_id`/timestamps/`settings` and emits `schema` as a nested object; `BootstrapRole` projection drops UUIDs/timestamps/`tools`; convention `slug` dropped; top-level `recent_activity` (a duplicate of `dashboard.recent_activity`) removed; new `BootstrapDashboard` wrapper caps five sub-arrays (`attention`, `recent_activity`, `active_items`, `active_plans`, `by_role`) at 5 entries each with parallel `*_overflow_count` fields; redundant schema labels omitted when `label == TitleCase(key)`. Cumulative size reduction: ~40% on a representative workspace, ~54% on the fixture (see PLAN-1410's Result section for per-section deltas). Compatibility: most changes are subtractive (dropped fields) or additive (overflow counts), but **one type change is breaking**: `collections[].schema` went from a JSON-encoded string to a nested JSON object — clients that JSON.parse()'d the string need to consume it directly as an object now. The dropped fields (UUIDs, timestamps, settings, duplicate `recent_activity`, convention `slug`) have canonical alternatives (slugs for addressing; `pad collection list` / `pad role list` for the full models when needed). Both are also returned by `pad://_meta/version` and `pad_meta.action: version`. **Dispatchers.** Two ship in `internal/mcp/`: - `ExecDispatcher` — shells out to the `pad` binary; subprocess inherits credentials from `~/.pad/credentials.json`. Used by `pad mcp serve` for local stdio MCP. - `HTTPHandlerDispatcher` — calls pad-cloud's HTTP handlers in-process with the requesting user attached via `server.WithCurrentUser`. Backs the **live** remote MCP server on the dedicated `mcp.getpad.dev` vhost (PLAN-943), where the dispatcher serves multiple OAuth users from a single process. The Streamable HTTP transport is mounted by `Server.SetMCPTransport` / `registerMCPRoutes` (cloud-mode-gated; self-hosted binaries leave it unmounted) — see `internal/server/handlers_mcp.go`. Tools are wired into the route table at `internal/mcp/dispatch_http.go` (`routeTable`); add a `RouteMapper` per command — `mapItemCreate` is the seed entry from TASK-965. **Resource fetchers.** The read-only resource templates (`RegisterResources` in `internal/mcp/resources.go`) are transport-agnostic — they parse the pad CLI's `--format json` output, and a `ResourceFetcher` supplies those bytes. Two implementations mirror the dispatchers: - `ExecResourceFetcher` — shells out to `pad` (stdio MCP), same credential model as `ExecDispatcher`. - `HTTPResourceFetcher` (`internal/mcp/resources_http.go`, TASK-2101) — the in-process equivalent for remote /mcp. It translates each resource's fixed CLI-arg vector into an in-process HTTP read through the same handler chain (reusing `HTTPHandlerDispatcher`'s user resolution + `buildAuthedRequest` auth/scope/consent perimeter), reproducing the CLI shape the handlers expect (e.g. `item list` → `cli.ToItemSummaries`, `workspace list` → `{slug,name,updated_at}`, `attachment show` → HEAD-header synthesis). Attachment bytes flow through a `cappedResponseWriter` that preserves PR #933's 1 MiB download bound. Because it satisfies `ResourceFetcher`+`BinaryResourceFetcher`, all resource handlers register unchanged on both transports. Code lives in `internal/mcp/` (built on `github.com/mark3labs/mcp-go`). Public docs at `getpad.dev/mcp/local`. ## Data Model - **Collections** have JSON schemas defining typed fields (select, text, date, number, etc.) - **Items** have structured `fields` JSON + optional rich `content` (markdown) - **Parent/child links:** Any item can be a parent of child items (`--parent REF`). Children get progress tracking, burndown charts, and nested rendering. Plans are the most common parent, but Ideas, Docs, or Tasks can also have children. - **Wiki-links** `[[Title]]` resolve across all items, rendered as clickable links - **Default collections:** Tasks, Ideas, Plans, Docs (software / `startup` template) - **Templates** are grouped by category so Pad supports more than just software workflows: - **Software:** `startup` (default), `scrum`, `product` - **People:** `hiring` (company-side: Requisitions → Candidates → Loops → Feedback), `interviewing` (candidate-side: Applications, Interviews, Companies, Contacts) - **Custom:** `blank` — system collections only (Conventions, Playbooks), no user-facing seeds. Designed as the entry point for the `/pad onboard` agent-driven flow (see [Onboarding](#onboarding) below). PLAN-1496 / TASK-1498. - *Research / Content / Operations / Personal are reserved categories awaiting their first templates.* - Each non-blank template ships a curated starter pack (conventions + playbooks) appropriate to its domain — trigger vocabularies vary (`on-commit` vs `on-candidate-advance` vs `on-interview-scheduled`). - **The IDEA-1 / BACK-1 / FEAT-1 first-person seed-item pattern was retired in PLAN-1496** (TASK-1501 / TASK-1502). Templates no longer seed sample items; the `/pad onboard` playbook (auto-seeded into every workspace, TASK-1500) drives setup conversationally instead. - Set the template via `pad workspace init --template `. Running `pad init` with no flag in a TTY opens an interactive picker grouped by category. Run `pad workspace init --list-templates` to see the current catalog. - See `PLAN-609` and `IDEA-583` for original design history; `PLAN-1496` for the onboarding refactor. ## Playbooks Playbooks are first-class invokable procedures. They live in the `playbooks` collection (typed item, just like Tasks/Ideas/Plans) but carry two extra fields that make them user-callable: - **`invocation_slug`** — optional, workspace-unique, kebab-case (regex `^[a-z0-9][a-z0-9-]*[a-z0-9]$`, 2+ chars). When set, the playbook is invokable by intent (NL is canonical) and via the per-surface slug shortcut — `/pad ` in Claude Code, `$pad ` in Codex, `pad_playbook action=run ref=` via MCP ("slug routing"). Leave blank for trigger-only playbooks (e.g. `trigger=on-release` that auto-load on intent match). - **`arguments`** — JSON array of `{name, type, required, default, description, enum}` entries. Types: `ref`, `string`, `flag`, `enum`, `number`. Mirrors the playbook body's `## Arguments` section; the structured field is the queryable form (used by `pad playbook run`'s strict parser) and the markdown is the human-readable mirror. **Invocation model.** Three surfaces, one playbook: - **Claude Code (agent NL):** `/pad ship PLAN-1377 stop-after-each` — the `/pad` skill matches the first token against the bootstrap's playbook slug list and binds the rest with flexible NL parsing. - **CLI (strict positional):** `pad playbook run ship TASK-10,TASK-11 merge-strategy=rebase` — the server applies strict positional + bareword-flag + `key=value` parsing. - **MCP:** `pad_playbook` tool with `action: list | get | run`. `run` accepts either a pre-parsed `args` map or raw CLI tokens via `raw_args`. **Bootstrap returns metadata at startup.** `pad bootstrap` (CLI + `GET /api/v1/workspaces/{ws}/agent/bootstrap` + `pad://workspace/{ws}/bootstrap` resource + `pad_set_workspace` response embed) returns the workspace's playbook metadata in one round-trip — `ref`, `title`, `slug`, `invocation_slug`, `trigger`, `scope`, `status`, `has_arguments`, `summary` per entry. **No bodies** in the bootstrap blob; the agent loads the full body via `pad playbook show ` only when invoking. Keeps context light while still letting the agent route `/pad ship` without a tool call. **Seeded `ship` playbook.** The `startup` template ships a generic `ship` playbook (`invocation_slug=ship`) derived from the personal `/ship-tasks` slash command. Fresh `pad workspace init --template startup` workspaces get it as PLAYB-N out of the box. See `internal/collections/templates_startup_ship.go` for the body + de-personalization choices. **Library — discovery surface for invokable playbooks.** Per PLAN-1397's invokable-first overhaul, the playbook library (web UI: `/[username]/[workspace]/library?tab=playbooks`; JSON: `GET /api/v1/playbook-library`) carries the three canonical invokable workflow playbooks — **ship**, **plan**, **decompose** (invokable by intent; `/pad ` · `$pad ` · the `pad_playbook` MCP form are per-surface shortcuts) — under a single `agent-workflows` category. Each library card surfaces a `▶ ` invoke chip (with an NL-canonical tooltip listing the per-surface shortcuts) and an `N args` badge so the invocation model is visible before activation. Software templates auto-seed `plan` + `decompose` via `softwareStarterPlaybookTitles`; `startup` separately prepends `ship` so all three land together at workspace init. The pre-PLAN-1377 trigger-only checklist entries (Implementation Workflow, Code Review Process, Plan Creation, Bug Triage, Retrospective, Onboarding to a Project, Release Process, Deployment, Incident Response) are stashed in `playbook_library_archive.go::archivedPlaybooks()` — compiled but not surfaced; per-entry "convert / promote to convention / retire" decisions tracked in IDEA-1396. **Web UI editor.** `web/src/routes/[username]/[workspace]/playbooks/[slug]/+page.svelte` is the dedicated playbook editor — kebab-case slug input with debounced uniqueness check, structured arguments builder that round-trips with the body's `## Arguments` section, trigger selector with custom-trigger escape, and a "Test invocation" helper that renders `/pad`, `pad playbook run`, and `pad_playbook` MCP JSON forms from a slug + sample inputs. The reusable component lives at `web/src/lib/components/playbooks/PlaybookFormFields.svelte` and the shared parser/generator at `web/src/lib/playbooks/arguments.ts`. **Code map:** - `internal/server/handlers_playbooks.go` — `pad playbook list|show|run` HTTP handlers; `ParsePlaybookCLIArgs`, `resolvePlaybook`. - `internal/server/handlers_bootstrap.go` — `pad bootstrap`; embeds playbook metadata. - `internal/mcp/catalog_playbook.go` — `pad_playbook` MCP tool catalog entry. - `internal/collections/templates.go` — playbooks collection schema (`invocation_slug` + `arguments` fields); `softwareStarterPlaybookTitles` (auto-seed lineup for software templates). - `internal/collections/templates_startup_ship.go` — the seeded `ship` playbook (`ShipPlaybook()`, `shipPlaybookBody`, `shipPlaybookArguments`). - `internal/collections/playbook_library.go` — the invokable-first library (`PlaybookLibrary()`, `LibraryPlaybook` struct with `InvocationSlug` + `Arguments`). - `internal/collections/playbook_library_plan.go` — the `plan` library entry (`PlanPlaybook()`). - `internal/collections/playbook_library_decompose.go` — the `decompose` library entry (`DecomposePlaybook()`). - `internal/collections/playbook_library_archive.go` — retired pre-PLAN-1377 bodies; not surfaced, but compiled for future migrations (IDEA-1396). - `web/src/lib/playbooks/arguments.ts` — `## Arguments` parser/generator, `INVOCATION_SLUG_PATTERN`, `buildTestInvocation`. See `PLAN-1377` (invocation model) and `PLAN-1397` (library overhaul) in this workspace for the design history. ## Onboarding Workspace setup is driven by the canonical **onboard** invokable library playbook (PLAN-1496 / TASK-1499) — invoked by intent ("set up my workspace") or the per-surface shortcut (`/pad onboard` in Claude Code, `$pad onboard` in Codex, the `pad_onboard` MCP prompt). Pad does not run a baked-in CLI onboarding wizard; the playbook body IS the onboarding script, and any agent that can dispatch a playbook (Claude Code, MCP client, CLI) can run it. **Auto-seeded everywhere.** `pad workspace init` (with any non-blank `--template`) seeds the onboard playbook into the new workspace as `status=active, invocation_slug=onboard` (TASK-1500). The `blank` template ships it as the workspace's ONLY user-facing content. Empty-template-name workspace creation (`SeedCollectionsFromTemplate(ws, "")` — used by tests and direct API callers) intentionally skips the seed; see `internal/store/collections.go::SeedCollectionsFromTemplate` for the gating logic. **Surface-agnostic body.** The playbook body (`internal/collections/playbook_library_onboard.go::onboardPlaybookBody`) describes intent, not specific CLI commands. It instructs the agent to use whatever surface it has — `pad_item` MCP, `pad item` CLI, `pad_collection` MCP, etc. — and works for pure-MCP agents (no shell) the same as for Claude Code. The body covers four modes: `build` (blank workspace, build from scratch), `audit` (templated workspace, adapt seeded items), `revisit` (already-onboarded, change something specific), and `defaults` (escape hatch — pick sensible defaults and report). **Adaptation posture, not curation.** The body explicitly tells the agent: library entries are STARTING POINTS, not finished artifacts. Read the rule, rewrite using the project's actual commands and vocabulary. Invent when the library has nothing close. If the template seeded something that doesn't fit, edit or delete it. This is the core posture PLAN-1496 codifies — software templates seed generic "run the test suite" conventions, and `/pad onboard` rewrites them to `make test` / `go test ./...` / whatever the project actually uses. **Mutation primitives.** The adaptation posture depends on agent-facing mutation tools, exposed by TASK-1510 / TASK-1511 / TASK-1512: - `pad collection update ` + `pad_collection.action: update` — rename collections, swap icons, reshape schemas (TASK-1510) - `pad collection delete ` + `pad_collection.action: delete` — remove user-created collections that don't fit (TASK-1511) - `pad role update ` + `pad_role.action: update` — rewrite role descriptions and icons (TASK-1512) Server handlers existed pre-PLAN-1496; these tasks just wired CLI subcommands and MCP catalog actions to the existing HTTP endpoints. All three are owner-only server-side. **`needs_onboarding` bootstrap flag.** `AgentBootstrap.NeedsOnboarding` (PLAN-1496 / TASK-1504) is true when the workspace has zero items with `source != 'template'` — i.e. nothing beyond what the template seeded. The agent skill (`skills/pad/SKILL.md`) and the MCP server instructions render an active, NL-canonical offer when true (PLAN-1847): *"This workspace is brand new and isn't set up yet. Want me to set it up?"* — an offer, not an auto-run. The flag flips to false the moment any user/agent-created item exists; the offer stops firing past that point. Computed per-request via `Store.WorkspaceHasUserCreatedItems(workspaceID)` (EXISTS-backed). PLAN-1496 / TASK-1505 also retired the standalone "Onboarding" workflow section from the skill — the playbook body owns that script now. **Retired surfaces.** The pre-PLAN-1496 design had several surfaces that the playbook replaces; all retired: - `pad onboard` Cobra subcommand (was: codebase scan + convention suggestions) — TASK-1502. - `OnboardingPrimaryRef` field on `WorkspaceTemplate` (was: named IDEA-1 / BACK-1 / FEAT-1 per template) — TASK-1502. Dashboard banner auto-discovers seeds via `item_number=1 + source='template'` if a future template ever wants to reintroduce them. - The `*OnboardingItems()` generators in `internal/collections/templates_onboarding*.go` (deleted files) — TASK-1501. - The skill's standalone "Onboarding" workflow section — TASK-1505. Replaced by a one-paragraph pointer at the playbook. **Code map:** - `internal/collections/playbook_library_onboard.go` — the canonical playbook body + `OnboardPlaybook()` library entry + `OnboardSeedPlaybook()` auto-seed. - `internal/collections/templates_blank.go` — minimal trigger/scope vocabularies for the blank template's seeded system collections. - `internal/store/collections.go::SeedCollectionsFromTemplate` — wires the auto-seed for every non-empty templateName. - `internal/store/items.go::WorkspaceHasUserCreatedItems` — the `needs_onboarding` query predicate. - `internal/server/handlers_bootstrap.go::AgentBootstrap.NeedsOnboarding` — the bootstrap field. - `skills/pad/SKILL.md` — the nudge-rendering rule in Context Loading; the routing entry under "set up my workspace". ## Testing ```bash go test ./... # All Go tests go test ./internal/store/ # Store tests only cd web && npm run build # Verify frontend compiles cd web && npm run test # Web unit tests (vitest, run once) ``` ## Common Tasks ### Add a new API endpoint 1. Add handler in `internal/server/handlers_*.go` 2. Register route in `internal/server/server.go` setupRouter() 3. Add store method in `internal/store/` if needed 4. Add CLI client method in `internal/cli/client.go` 5. Add TypeScript type in `web/src/lib/types/index.ts` 6. Add API method in `web/src/lib/api/client.ts` 7. `make install` ### Add a new CLI command 1. Add the command constructor to the matching resource file under `cmd/pad/` — `cmd_item.go`, `cmd_collection.go`, `cmd_workspace.go`, `cmd_auth.go`, `cmd_project.go`, `cmd_playbook.go`, `cmd_role.go`, `cmd_tag.go`, `cmd_github.go`, `cmd_webhook.go`, `cmd_agent.go`, `cmd_server.go`, `cmd_attachment.go`, `cmd_db.go`, `cmd_library.go`, `cmd_bootstrap.go` (all `package main`, so helpers are shared across files). Create a new `cmd_.go` if none fits. Keep `main.go` for `main()`, `newRootCmd()`, and top-level wiring only — don't grow it back into a god file. 2. Wire it into the resource group in `cmd/pad/groups.go` (or `rootCmd.AddCommand()` in `main.go` for a new top-level group) 3. `make install` ### Modify the database schema 1. Add migration file in `internal/store/migrations/` 2. Update models in `internal/models/` 3. Update store methods in `internal/store/` 4. `make install` (migrations run automatically on server start) ## Real-time collaboration (Yjs / Tiptap) Collab is wired through `/api/v1/collab/{itemID}` (WebSocket, Yjs binary protocol). The relevant code lives in: - `internal/collab/` — RoomManager, room lifecycle, dumb-relay - `internal/store/yjs_updates.go` — op-log persistence - `web/src/lib/collab/wsProvider.svelte.ts` — client provider - `web/src/lib/collab/schemaVersion.ts` — client schema-version stamp **Collab requires no additional container deps; the single Go binary remains the self-hosted shape.** The dumb-relay design (server persists raw Yjs binary updates without parsing them) means there's no Yjs Go port to vendor and no separate sync-server process to run. The op-log lives in the same SQLite/Postgres as everything else, and the WebSocket relay is part of the main HTTP listener. Multi-instance Redis fanout is deliberately out of scope for v1 (single-instance everywhere); when horizontal scaling is needed it lands as a separate IDEA, not a self-host complication. ### Tiptap multi-package coordinated bumps The Y.Doc/ProseMirror schema is shared across three Tiptap packages: - `@tiptap/core` - `@tiptap/extension-collaboration` - `@tiptap/y-tiptap` **Rule: bump all three together, exact-pinned to the same version.** Mixing minor versions across these can change the persisted Y.Doc shape silently — peers running mismatched bundles produce divergent ops that the relay can't reconcile. The `web/package.json` pins each one explicitly (e.g. `"@tiptap/extension-collaboration": "3.22.5"`) rather than using `^` ranges so npm can't slide one out of sync. A coordinated bump that changes the ProseMirror node-spec MUST also bump `web/src/lib/collab/schemaVersion.ts::SCHEMA_VERSION` AND `internal/collab/manager.go::DefaultSchemaVersion` in lockstep. The client announces the version on every WS connect; mismatch returns HTTP 400 and the room manager prunes the per-item op-log so the new client doesn't replay incompatible old-schema ops. items.content is canonical and untouched, so no edit history is lost. Pure UI/CSS/behavioural changes that don't alter the persisted document shape DO NOT bump the schema version. When in doubt, load an item edited under the old version after your change and confirm the rendered tree is identical.