xarmian e91c4fc261 fix(store): include the cursor's own second in /changes deltas (BUG-2539) (#1086)
* fix(store): include the cursor's own second in /changes deltas (BUG-2539)

items.updated_at / items.deleted_at are RFC3339 whole-second strings
(store.now()), while the /changes cursor is a unix-millisecond value —
normally the previous response's server_time. ItemsModifiedSince
formatted that cursor with the same second precision, truncating it
DOWN, then compared with a strict `>`. Every change landing in the
cursor's own second compared equal and was dropped, permanently: the
caller advances its cursor past that second and nothing reaches back.

User-visible symptom: a bulk archive ~450ms after a page seeded its
cursor left the item rendering as LIVE indefinitely — no banner, no
redirect — while the server had deleted_at set. It was never
archive-specific (updates were dropped identically); a missed update is
usually re-delivered by the next event, a missed deletion never is.

Compare inclusively against the truncated second instead. The boundary
second may be re-delivered, which every consumer of this endpoint
applies idempotently, and it is bounded to one second of changes per
sync. Sub-second storage is the other fix and is a migration, not a
one-liner: these comparisons are lexicographic on TEXT columns and
mixing precisions inverts them ("…20.451Z" sorts BEFORE "…20Z").

Verified against a live instance with four cursors all strictly earlier
than the archive in real time: two inside its second MISS, two in
earlier seconds HIT.

Tests:
- TestItemsModifiedSince_SameSecondCursor — same-second leg plus a
  previous-second control. Fails 3/3 unfixed, passes 3/3 fixed; the
  control passes on both.
- e2e bug-2539-sync-window — the banner must appear in the already-open
  page AND follow a /changes delta that carried the deletion, so a
  reload cannot satisfy it. The 450ms leg fails unfixed; 1200ms control
  passes on both.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* test(store,e2e): close the review gaps in the BUG-2539 counterfactuals

Codex review of 7905ed06 found no P1s and three P2s, all on whether the
tests actually measure the fix. Each was right.

Store test:
- It derived "same second" from a wall-clock reading taken BEFORE the
  writes, so a leg whose writes drifted into the next second would pass
  under the unfixed query and still be counted as evidence. It now reads
  the timestamps the writes actually STORED and compares those against
  the cursor's second.
- Misalignment retried instead of skipped, so the leg cannot silently
  stop testing anything.
- It never asserted the archived row comes back in `updated`, leaving
  the `(deleted_at IS NULL OR deleted_at >= ?)` arm free to regress to
  `>` unnoticed. Now asserted.

E2E:
- It accepted ANY /changes response carrying the deletion, including the
  cursor-seeding request setWorkspace fires during load, and never
  established that the page had loaded a live row. Both holes let it
  pass without exercising the incremental path. It now requires the
  page's own item GET to have seen deleted_at null AND the deletion to
  arrive on a /changes that resolved after the archive POST completed.
- Assert check.ok() before reading deleted_at; delete the scratch
  workspace at the end.

Also softened the comment's claim about re-delivery: `>=` makes the
endpoint at-least-once at the boundary and rows can repeat across
several rapid syncs, not just one. What makes that safe is that the
payload is server state rather than an increment, so the comment now
says that instead of "bounded to one second".

Counterfactuals re-run against a genuinely reverted query (the earlier
stash-based attempt was a no-op once the fix was committed, and passed
for that reason): store test fails 3/3 with all three assertions firing,
e2e 450ms leg fails, both controls pass.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* test(e2e): decide BUG-2539's oracle by request order, not response timing

Codex's second pass kept one P2 on the e2e, and it was right on both
halves. The oracle compared response arrival against the moment the
archive POST completed, which is racy in both directions: the server
publishes the SSE event BEFORE the bulk handler finishes writing its
response, so the incremental /changes can resolve first and be scored as
"not after the archive" (false failure); and a slow cursor-seeding
response can resolve after it and be scored as incremental (false pass).
The live-row check was existential — any live read of the row counted,
including one issued after the archive.

Both are now decided on the REQUEST side, where ordering is not racy:

- `/changes` requests are numbered as they are issued. The seed that
  setWorkspace fires on mount is number 0; only a LATER one carrying the
  deletion satisfies the assertion.
- Item GETs record their issue time, and `archiveSentAt` is stamped
  immediately BEFORE the POST goes out, so "the page read a live row
  before the archive" is decidable without waiting on anything.
- The async response handlers are collected and awaited before the
  assertions read their flags, instead of racing them.

Cleanup moved into a finally so a failing assertion no longer leaks the
scratch workspace, and its response is checked.

Counterfactual re-run at repeat-each=3 against a genuinely reverted
query: the 450ms leg fails 3/3, the 1200ms control passes 3/3, and both
legs pass 3/3 with the fix in.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* test(e2e): anchor BUG-2539's legs to preconditions, not a fixed delay

The fixed 450ms/1200ms offsets measured from navigation start were a
proxy for the real condition, and a machine-speed-dependent one. Under
parallel workers a slow load put the page's own item read AFTER the
archive, so the page rendered an already-archived row and the sync path
was never exercised — the live-row assertion then failed, correctly
reporting that the leg had not reproduced the scenario.

The legs now wait for the two things that actually have to be true —
the page has READ a live row (server-attested deleted_at null) and its
EventSource is subscribed (the /events response headers have arrived;
the handler subscribes before writing them) — and are named after the
mechanism: archive inside the cursor's own second vs after crossing the
next second boundary.

Also from Codex's third pass:
- the /changes URL match is anchored so it cannot also match
  /items-changes, whose requests would otherwise consume ordinals and
  let the cursor seed pass as the incremental sync;
- the incremental check now requires ordinal > 0 AND issue time at/after
  the archive, so a retried seed cannot pass on ordinal alone;
- the live-row check no longer compares clocks at all — a response
  carrying deleted_at null cannot come back after the archive applied,
  so the server attests it;
- the response-handler drain loops until no new handler was queued while
  awaiting, instead of snapshotting the array once;
- cleanup is recorded in `finally` and asserted after it, so a cleanup
  failure cannot replace the real one, and setup now runs inside the try
  so a failed setup cannot leak the scratch workspace.

Counterfactual against a genuinely reverted query: the same-second leg
now fails 6/6 (the delay-based version managed 7/8), control passes 6/6.
130 runs green with the fix in.

Both assertions carry the recorded state in their message: this leg
flaked twice in ~70 runs of an earlier revision and the artifacts were
cleared by the next run before they could be read, so a recurrence has
to explain itself from the failure text.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* test(e2e): make BUG-2539's leg PROVE it hit the second it is named after

Codex's fourth pass: the same-second leg never checked that it actually
landed in the cursor's second. It archived immediately after the
preconditions and assumed. On a slow machine that drifts across the
boundary, at which point the reproducing leg quietly becomes a second
control and passes against the very query it exists to convict — the
same "a test that silently stops testing anything" shape the store test
was already hardened against, which is what makes the point land.

Each attempt now compares two SERVER values: the `server_time` of the
page's first /changes (the seed — that value IS the client's
lastSyncTime, the cursor the failing sync used) against the `deleted_at`
the server stored. Same-second leg requires equality, boundary leg
requires difference, and a misaligned attempt is retried on a fresh
workspace (up to 6) instead of asserted on. Exhausting the attempts
fails with both seconds in the message.

Also from that pass:
- cleanup swallows a rejected delete rather than replacing the real
  failure with a transport error;
- `createdSlug` is set from the requested slug BEFORE parsing the
  response, so a malformed success cannot leak a workspace;
- request/response listeners are removed per attempt, so retries do not
  stack handlers;
- the live-row comment now says "carries no deleted_at" — a live item
  omits the field (omitempty) rather than sending null. The previous
  commit message said `deleted_at: null`; the check was always a falsy
  one, so only the wording was wrong.

Counterfactual against a genuinely reverted query: same-second leg fails
4/4, control passes 4/4. 12/12 green with the fix.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* test(e2e): scope BUG-2539's matchers per attempt; don't accept an unknown seed

Codex's fifth pass, both findings real:

- Retry ordinals were not scoped to the attempt's workspace. A retry
  reuses the page, which is still showing the previous attempt's
  workspace when the listeners go on, so an in-flight /changes from that
  one could take ordinal 0 and be mistaken for the new seed. The
  /changes, item-GET, and /events matchers are now built per attempt
  against that attempt's slug.

- A seed response that was never observed left seedServerTime null,
  which the alignment check folded into "different second". For the
  same-second leg that already meant a retry, but the CONTROL leg would
  proceed on an unknown and claim it had proven a difference it never
  saw. An unknown seed is now its own retry.

Counterfactual re-run after the change: same-second leg fails 4/4
against a reverted query, control passes 4/4; 10/10 green with the fix.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
2026-08-13 12:18:26 -04:00
2026-08-11 17:37:53 +00:00
2026-03-26 01:52:36 +00:00

Pad

Project Management for the agent era.

CI Release Go Report Card Container image on GHCR License GitHub Sponsors

Website  ·  Docs  ·  Blog  ·  Changelog  ·  X  ·  Bluesky


One binary. Local-first. No accounts required. Pad gives you a CLI, a web UI, and an AI agent skill — all backed by SQLite, all running on your machine. Your project data stays on your laptop — unless you take it to Pad Cloud.

Pad dashboard showing collection summaries, active work, an active plan with progress, and a recent activity feed

Quick Start

brew install PerpetualSoftware/tap/pad
cd your-project
pad init                    # configure, auth, workspace, AI skill — all in one
pad server open             # opens the web UI at localhost:7777

pad init is the smart entry point — it auto-detects what's needed, walks you through each step, and is safe to re-run anytime (it skips finished steps and prints a status summary).

Then, in a fresh agent session in your project, say:

/pad onboard

Your new workspace ships with the canonical onboard playbook auto-activated. The agent walks an interview, inspects your codebase if it has shell access, and adapts your workspace's collections, conventions, roles, and playbooks to match the project. It's the fastest way to go from empty workspace to "okay, this is mine."

Why Pad?

Tools like Linear, Jira, and Notion are built for teams on the cloud. Pad is built for developers on their machine — and for the AI agents working alongside them. When you do want your projects on every device or a teammate on the board, Pad Cloud hosts the same product with sync, workspace invites, and role-based access.

Pad Linear / Jira Notion
Setup pad init Create account, invite team, configure Create account, pick template
AI agents Native /pad skill for 7+ tools Third-party integrations Third-party integrations
Data Local SQLite you own — or opt-in Pad Cloud Their cloud Their cloud
Offline Full functionality Read-only cache at best Limited
CLI First-class Afterthought None
Price Free, open source Per-seat pricing Per-seat pricing

Features

For Developers

CLI that doesn't get in your way. Create tasks, search items, check status — without leaving the terminal.

pad item create task "Fix OAuth redirect" --priority high
pad item create idea "Real-time collaboration" --category infrastructure
pad item list tasks --status in-progress
pad item search "authentication"
pad project dashboard                   # Project dashboard
pad project next                        # What should I work on?
pad server info                         # How this client is connected to Pad

Web UI that stays out of your way. A clean, dark-themed interface at localhost:7777 with:

  • Board, list, and table views — drag-and-drop between status columns
  • Keyboard navigationj/k to move, Enter to open, Esc to go back, Cmd+K to search
  • Rich text editor — Tiptap-based with markdown, formatting toolbar, and auto-save
  • Wiki-links — type [[Title]] to link between items
  • Real-time updates — agent creates a task in the terminal, it appears in the browser instantly (via SSE)
  • Dashboard — collection overview, active work, plan tracking, activity feed

Pad tasks board view: kanban columns for Open, In-Progress, Done, Cancelled with task cards in each

For AI Agents

Your agent becomes a project partner. Install the /pad skill once, and your AI coding tool can read, create, and update project items through natural language.

pad agent install        # Auto-detects your tools and installs the skill

Works with Claude Code, Cursor, Windsurf, Codex, OpenCode, GitHub Copilot, Amazon Q, and JetBrains Junie.

Then just talk to your project:

> /pad what should I work on next?
> /pad I finished the OAuth fix
> /pad create a task to add rate limiting
> /pad let's brainstorm about the API redesign

Conventions and playbooks teach agents how your project works:

  • Conventions — trigger-based rules like "run tests before marking a task done" or "use conventional commits"
  • Playbooks — multi-step workflows like "when implementing a feature: read the spec, create a branch, write tests first, then implement". Playbooks can declare a kebab-case invocation_slug so users can invoke them directly: /pad ship PLAN-42, /pad release 0.5.0. Fresh startup workspaces ship a generic ship playbook out of the box.
pad item create convention "Run tests before completing tasks" \
  --field trigger=on-task-complete \
  --field scope=all \
  --field priority=must

Agents load relevant conventions automatically. All agent actions are attributed in the activity feed, so you always know what the AI changed.

Onboard agents to a new codebase:

Open an agent session in the workspace directory and run /pad onboard. The agent walks an interview, detects your build/test/CI tooling, and adapts your workspace's collections, conventions, roles, and playbooks to match the project. Works for any agent that speaks Pad — Claude Code, MCP-only agents, etc.

Collections & Custom Fields

Pad organizes work into collections — typed containers with structured fields.

Built-in collections:

Collection Purpose
Tasks Work items with status, priority, assignee, effort, due date
Ideas Feature ideas with impact and category
Plans Project milestones with progress tracking
Docs Documentation, decisions, reference material
Conventions Project rules that guide agent behavior
Playbooks Multi-step workflows for agents to follow

Create your own with typed fields — select, text, date, number, url, relation, checkbox:

pad collection create "Bug Reports" \
  --fields "severity:select:low,medium,high,critical; browser:text; reproducible:checkbox"

Items get reference numbers automatically (TASK-5, BUG-12) and can be moved between collections with field migration.

Installation

Homebrew (macOS and Linux)

brew install PerpetualSoftware/tap/pad

Build from Source

git clone https://github.com/PerpetualSoftware/pad
cd pad
make build
cp pad ~/.local/bin/   # or /usr/local/bin/

Requires Go 1.26+ and Node.js 22+. Alternatively, nix develop provides a shell with the exact Go and Node versions pinned — see the Nix section below.

The go install github.com/PerpetualSoftware/pad/cmd/pad@latest path is not supported for the full Pad binary, because the web UI must be built and embedded during the source build.

Docker

docker run -p 127.0.0.1:7777:7777 -v pad-data:/data ghcr.io/perpetualsoftware/pad

This publishes Pad to localhost:7777 on the host machine, which is the recommended default for local use.

First run — create the first admin. Open http://localhost:7777 and you'll hit a setup page asking for a bootstrap token. On first start with no users, Pad logs a one-time setup URL to stderr (captured by docker logs) — grep it and open the printed link:

docker logs <container> 2>&1 | grep -A6 'Pad first-run setup'
# → http://<your-host>:7777/setup#token=<one-time-token>

Open that URL, create your admin account, and the token is consumed (the banner stops appearing). If you'd rather stay on the CLI, docker exec -it <container> pad auth setup works too — running inside the container counts as loopback, which the bootstrap gate allows. On a network you already trust, set PAD_BYPASS_SETUP_TOKEN=true to skip the token and create the admin straight from http://<your-host>:7777/setup (only safe when the port isn't reachable from the open internet).

Single user, more than one device? Publish to all interfaces so you can reach Pad from your phone, tablet, or another machine on the same LAN, Tailscale network, or home VPN:

docker run -p 7777:7777 -v pad-data:/data ghcr.io/perpetualsoftware/pad

For multi-instance deployments, Pad supports Postgres + Redis via docker-compose.yml — see docs/deployment.md for the full setup.

Nix

Run without installing:

nix run github:PerpetualSoftware/pad

Or install into your profile:

nix profile install github:PerpetualSoftware/pad

A flake devShell (Go, Node, and friends, pinned to the same versions CI uses) is also available for contributors:

nix develop

A nixpkgs package (nix-shell -p pad / environment.systemPackages) is planned but not yet merged upstream. Until then, use the github:PerpetualSoftware/pad flake reference above.

Binary Download

Pre-built binaries for macOS, Linux, and Windows are available on the releases page.

Pad Cloud (hosted)

Don't want to run anything? Pad Cloud is the managed option — same product, same CLI, same /pad skill, free during beta. Sign up on the web, then connect a project directory:

pad init --url https://app.getpad.dev --workspace my-workspace

Self-hosting stays first-class: the binary is unchanged and no features are Cloud-only.

Upgrading Pad

Pad ships a new binary on a roughly weekly cadence. Upgrades are designed to be boring: install the new binary and restart. Database migrations run automatically at startup, only the ones your database is missing are applied, and each migration commits atomically (a failed migration rolls back cleanly and is retried next boot).

The one rule: only ever move forward. Newer binaries know how to migrate an older database; older binaries do not understand a newer schema. Since Pad added its schema-ahead guard, a downgraded binary that finds a database newer than itself refuses to start rather than silently running old code against a newer schema (which can corrupt data):

database schema is newer than this pad binary: ... This almost always means the
binary was DOWNGRADED (e.g. brew/docker rollback) ... Upgrade pad back to a build
that includes those migrations, or re-run with `pad start --force`.

To recover, reinstall the newer binary (brew upgrade pad, pull the newer Docker tag, etc.). If you have intentionally downgraded and accept the risk, start with pad start --force (or set PAD_ALLOW_SCHEMA_AHEAD=1) to override the guard.

Automatic pre-migration snapshot (SQLite). Whenever a SQLite-backed instance has pending migrations to apply, Pad first copies the database file to pad.db.pre-<version> next to it. If an upgrade ever goes wrong, stop the server and copy that snapshot back over pad.db. This is a convenience net, not a backup strategy — keep your own backups (see docs/backup.md). PostgreSQL instances are skipped here; use pg_dump or a provider snapshot before upgrading.

Recommended upgrade flow:

# 1. Back up first (SQLite shown; see docs/backup.md for Postgres)
pad db backup -o pad-backup-$(date +%Y%m%d).db

# 2. Stop the server, install the new binary, restart
#    (migrations + the pre-migration snapshot run automatically on start)
brew upgrade pad        # or: docker pull, binary download, make install

# 3. Confirm it's healthy
pad --version
curl -s localhost:7777/api/v1/health

Getting Started

1. Set up Pad

cd ~/projects/myapp
pad init "My App"

pad init is the smart entry point that handles everything in one command:

  • Configures this client's connection (local server, remote, or Docker)
  • Auto-starts the local server
  • Creates the first admin account on a fresh local install (Docker / remote hosts run pad auth setup on the server instead)
  • Logs you in if needed
  • Creates or links a workspace for the current directory (writes .pad.toml)
  • Installs the /pad skill for any AI tools detected in the project

Run from your project root. Safe to re-run anytime — it skips finished steps and prints a status summary if nothing's needed.

Choose a template with --template, or omit it for an interactive picker grouped by category (Software / People / …):

pad workspace init --list-templates                   # See the full catalog grouped by category
pad init "My App" --template scrum                    # Scrum-style with sprints
pad init "My App" --template product                  # Product management focused
pad init "My Hiring" --template hiring                # Company-side: requisitions, candidates, interview loops, feedback
pad init "Job Search" --template interviewing         # Candidate-side: applications, interviews, companies, contacts
pad init "My App" --template blank                    # Custom: system collections only — let /pad onboard build the rest

Pad ships templates for software (startup / scrum / product), people workflows (hiring, interviewing), and a custom blank template — system collections (Conventions, Playbooks) only, with the /pad onboard playbook as its sole seeded content. blank is the entry point for the agent-driven /pad onboard flow: it walks you through shaping collections, conventions, and roles to match your actual project. Reserved categories for research, content, operations, and personal use await their first templates, so the same project-management primitives fit well beyond code projects. There's also a hidden demo template — the startup layout pre-loaded with realistic sample data — that's kept out of the picker but can be built explicitly with --template demo.

2. Start working

# From the CLI
pad item create task "Set up CI pipeline" --priority high
pad item create idea "Add WebSocket support" --category infrastructure
pad project dashboard

# From the web UI
pad server open              # Opens localhost:7777 in your browser

# From your AI agent
# Just use /pad in Claude Code, Cursor, etc.

3. Teach your agents the rules

In an agent session inside the workspace:

/pad onboard

The agent walks an interview, detects your tooling, and adapts the workspace's collections, conventions, roles, and playbooks. To browse the library directly:

pad library list --type conventions  # Pre-built conventions you can adopt
pad library list --type playbooks    # Pre-built multi-step workflows

4. Optional — connect a desktop AI app via MCP

Pad ships an MCP (Model Context Protocol) server so Claude Desktop, Cursor, Windsurf, Claude Code, or Codex can manage items, plans, ideas, and dependencies as native tools, read workspace state by URL, and load multi-step workflows as prompts.

pad mcp install claude-desktop   # or: cursor, windsurf, claude-code, codex, --all
# Restart the client; pad shows up as the "pad" MCP server.

pad mcp install writes each client's native config: JSON mcpServers for Claude Desktop / Cursor / Windsurf, a project-local .mcp.json in the current directory for claude-code, and an [mcp_servers.pad] table in ~/.codex/config.toml (TOML) for codex. Because Claude Code's config is project-scoped, it's install-on-request only — --all and pad mcp status cover the per-user clients (including Codex) and skip it.

Tool catalog (v0.15) — ten resource × action tools plus pad_set_workspace (eleven total), no flat verb explosion. pad_item.list accepts unparented: true (mutually exclusive with parent) to select items with no parent or implements relationship:

Tool Actions
pad_item create, update, delete, get, list, move, restore, link, unlink, deps, star, unstar, starred, comment, list-comments, backlinks, bulk-update, note, decide, export, import, history
pad_workspace list, members, invite, storage, audit-log, create, claim, deleted, restore
pad_collection list, create, update, delete
pad_project dashboard, next, ready, stale, standup, changelog, report, activity
pad_role list, create, update, delete
pad_search query
pad_playbook list, get, run
pad_library list, get, activate
pad_attachment list, show
pad_meta server-info, version, tool-surface, bootstrap
pad_set_workspace session-default workspace pinning (response embeds the bootstrap blob)

Plus resources at pad://workspaces, pad://workspace/{ws}/dashboard, pad://workspace/{ws}/items, pad://workspace/{ws}/items/{ref}, pad://workspace/{ws}/collections, pad://workspace/{ws}/attachments/{id} (bounded image bytes), pad://workspace/{ws}/bootstrap, and pad://_meta/version.

Stability contract — two version constants, both advertised in the initialize handshake under capabilities.experimental.padCmdhelp and capabilities.experimental.padToolSurface (and queryable at pad://_meta/version):

  • cmdhelp_version: "0.1" — CLI help-tree contract (used at dispatch time)
  • tool_surface_version: "0.15" — MCP tool catalog contract (v0.5 added pad_library; v0.6 pad_item.backlinks; v0.7 pad_item export/import; v0.8 pad_workspace deleted/restore; v0.9 made pad_item.list summary-shaped by default with a default+max result cap; v0.10 enforced the draft-playbook gate server-side on pad_playbook.run with an allow_draft escape hatch; v0.11 added the read-only pad_attachment tool (list/show); v0.12 added pad_project.activity (agent-accessible non-streaming activity feed); v0.13 added pad_project ready/stale (agent-oriented backlog + attention queries); v0.14 added pad_item history + optimistic concurrency (TASK-2022); v0.15 added the pad_item.list unparented parameter (TASK-2096); see internal/mcp/version.go for the full changelog)

External agents pin against these so a future rename doesn't break them silently. Errors come back as structured envelopes ({error: {code, message, hint, available_workspaces, ...}}) with a closed eight-code taxonomy.

Full guide at getpad.dev/mcp/local — install paths, action enums per tool, error taxonomy, troubleshooting.

On Pad Cloud? Skip the install: add https://mcp.getpad.dev as a remote MCP server in Claude Desktop, Claude.ai, Cursor, or Windsurf and sign in with OAuth — same tool surface, no local binary. Setup guide at getpad.dev/mcp/remote.

CLI Reference

pad auth configure                    Configure how this client connects to Pad
pad auth setup                        Initialize the first admin account
pad auth login                        Sign in
pad auth whoami                       Show current user

pad server start                      Start the Pad API server
pad server stop                       Stop the Pad server
pad server info                       Show client, connection, and local server status
pad server open                       Open web UI in browser

pad workspace init [name]             Initialize workspace in current directory
pad workspace link <workspace>        Link current directory to an existing workspace
pad workspace list                    List all workspaces
pad workspace switch <workspace>      Switch active workspace
pad workspace context                 Show structured workspace context
pad workspace context set --file X    Update structured workspace context from JSON
# Workspace onboarding: run `/pad onboard` from an agent session inside the workspace
pad workspace members                 List workspace members
pad workspace invite <email>          Invite a workspace member
pad workspace join <code>             Accept an invitation
pad workspace export                  Export workspace data
pad workspace import <file>           Import workspace data

pad project dashboard                 Project dashboard
pad project next                      Recommended next task
pad project ready                     Query actionable next items
pad project stale                     Query stalled or attention-worthy items
pad project standup [--days N]        Daily standup report
pad project changelog [--days N]      Release notes from completed items
pad project watch                     Real-time activity stream
pad project reconcile                 Reconcile item and PR state

pad item create <coll> "title"        Create item (task, idea, plan, doc, ...)
pad item list [collection]            List items (filters: --status, --priority, --all)
pad item show <ref>                   Show item detail
pad item open <ref>                   Open item in web UI
pad item update <ref>                 Update item fields
pad item delete <ref>                 Delete item
pad item move <ref> <collection>      Move item between collections
pad item edit <ref>                   Open item in $EDITOR
pad item search "query"               Full-text search across all items
pad item comment <ref> "text"         Add comment to an item
pad item comments <ref>               View item comments
pad item note <ref> "summary"         Append an implementation note to an item
pad item decide <ref> "decision"      Append a decision log entry to an item
pad item block <src> <target>         Create dependency
pad item blocked-by <item> <blk>      Mark item as blocked
pad item deps <ref>                   Show dependencies
pad item unblock <src> <target>       Remove dependency
pad item related <ref>                Show direct relationships for an item
pad item implemented-by <ref>         Show incoming implementers for an item
pad item bulk-update --status X       Batch update multiple items

pad collection list                   List collections with item counts
pad collection create <name>          Create a custom collection

pad library list                      Browse convention and playbook library
pad library activate <title>          Activate a convention or playbook

pad agent install [tool]              Install /pad skill for AI coding tools
pad agent status                      Show supported tools and installation status
pad agent update                      Update installed tool integrations

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 <item-ref>          Remove PR link from item

pad webhook list             List workspace webhooks
pad webhook create <url>     Create webhook

All commands accept --format json for machine-readable output and --workspace to target a specific workspace.

Shell completion

pad ships completion scripts for bash, zsh, fish, and PowerShell:

# Bash — current session only
source <(pad completion bash)
# Bash — persistent
pad completion bash > /etc/bash_completion.d/pad                   # Linux
pad completion bash > $(brew --prefix)/etc/bash_completion.d/pad   # macOS (Homebrew)

# Zsh (make sure compinit runs in your ~/.zshrc)
pad completion zsh > "${fpath[1]}/_pad"

# Fish
pad completion fish > ~/.config/fish/completions/pad.fish

# PowerShell (append the output to your $PROFILE)
pad completion powershell | Out-String | Invoke-Expression

Beyond command and flag names, completion is context-aware: collection arguments (e.g. pad item list <TAB>) complete against your workspace's collections, --workspace completes configured workspace names, and --status / --priority complete their valid values.

Authentication

Pad runs without authentication by default for frictionless local use. For local installs, pad init creates the first admin account inline. The lower-level commands are useful when you're hosting a Pad server (Docker / remote) and need to set up auth on the server host directly:

pad auth setup         # Initialize the first admin account (server host, non-local mode)
pad auth login         # Sign in
pad auth whoami        # Show current user
pad auth logout        # Sign out

Once a user exists, all API requests and web UI access require authentication. Credentials are stored in ~/.pad/credentials.json. Multiple users can be invited to workspaces with role-based access control (owner, editor, viewer).

pad workspace members               # List workspace members
pad workspace invite user@example.com
pad workspace join <code>

Architecture

┌──────────────────────────────────────────────┐
│              pad (single binary)              │
│                                               │
│  ┌──────────┐  ┌──────────┐  ┌────────────┐  │
│  │   CLI    │  │  REST    │  │  Embedded  │  │
│  │ (Cobra)  │  │  API     │  │  Web UI    │  │
│  └────┬─────┘  └────┬─────┘  │ (SvelteKit)│  │
│       │    HTTP      │        └────────────┘  │
│       └──────────────┤                        │
│                ┌─────▼─────┐                  │
│                │  SQLite   │                  │
│                │  + FTS5   │                  │
│                └───────────┘                  │
└───────────────────────────────────────────────┘
  • Go backend — chi router, SQLite via modernc.org/sqlite (pure Go, no CGO), FTS5 full-text search, SSE for real-time updates
  • SvelteKit frontend — Svelte 5, Tiptap editor, drag-and-drop, adapter-static, embedded via go:embed
  • Single binary — serves the API and web UI, runs on macOS, Linux, and Windows
  • Workspace-per-project — each project gets its own workspace linked by a .pad.toml file

Self-hosted, all data lives in ~/.pad/pad.db. Your data. Your machine. No telemetry, no accounts required — cloud only if you opt in.

Contributing

See CONTRIBUTING.md for the development guide.

make build      # Build web UI + Go binary
make test       # Run Go tests
make dev-web    # SvelteKit dev server with hot reload
make install    # Build, install to ~/.local/bin, restart server

Security

See SECURITY.md for reporting vulnerabilities.

License

Apache License 2.0

Languages
Go 67.1%
TypeScript 18.7%
Svelte 13.6%
Shell 0.3%
CSS 0.1%