* feat(web): surface required/default/suffix/relation field controls
Closes TASK-596 in PLAN-593.
Expose field capabilities that already round-tripped through
EditableField but had no UI. Controls live in a collapsible Advanced
section on each field card.
FieldEditor
- New exported CollectionOption type for the relation picker input.
- Advanced section (collapsed by default, auto-expanded when any of
required/default/suffix/collection is already set) containing:
* Required checkbox (all types)
* Default value — type-appropriate input:
text/url -> text input
number -> number input (+ Suffix row below it)
date -> date picker
checkbox -> "Checked by default" toggle
select -> dropdown restricted to the field's options
multi_select / relation -> deliberately skipped
* Relates to dropdown (relation type only), populated from the
workspace collections list passed in via props. Shows a helpful
empty-state when no other collections exist.
- Computed fields render a muted "computed" badge and the advanced
inputs are disabled (changing defaults / suffix / required on a
computed field is nonsensical). Label / type / remove remain
editable to preserve current behavior.
- Typed input handlers coerce field.default into the right shape
(string / number / boolean) so the polymorphic value stays clean.
CreateCollectionModal + EditCollectionModal
- Both fetch api.collections.list(ws) lazily on open and pass the
result down to every FieldEditor as `collections`.
- Both new-field save paths now emit required / computed / suffix /
collection / default onto the serialized FieldDef. Existing-field
save path in EditCollectionModal already handled these; this brings
the new-field path to parity and adds equivalent handling in the
Create modal.
Behavior notes
- Values round-trip: set in Advanced -> save -> reopen -> still there.
- Emit-when-set keeps payloads compact and compatible with existing
schemas that don't carry these fields.
- Known visual quirk: the taller card may exacerbate the type-select
alignment already tracked on TASK-598; deferred to the visual pass.
* fix(web): gate advanced field properties by current field type
Codex P2 (PR #135): the save paths emitted `suffix`, `collection`,
and `default` for every new field regardless of f.type, so a user
could set a number default/suffix, switch the field to `relation` or
`multi_select`, and still persist the hidden value — producing schema
defaults that don't match the final type and are then auto-applied
to new items by ValidateFields.
Fix: gate type-specific advanced-value emission by the current type
at save time. This keeps the user's in-memory state intact (no
surprise clears on type toggle) but prevents stale values from
leaking into the saved schema.
- suffix: only when type === 'number'
- collection (relation target): only when type === 'relation'
- default: only when typeSupportsDefault(type) returns true
Apply the gating in all three save paths:
- CreateCollectionModal.handleCreate (new fields)
- EditCollectionModal.handleSave addedFields (new fields)
- EditCollectionModal.handleSave updatedExisting (existing fields) —
same pre-existing risk if the user changes an existing field's
type and hits save
Extract the default-support check into typeSupportsDefault() in
field-editor-types.ts so FieldEditor (which gates the rendered
default input) and the save paths share one predicate. Adjust
FieldEditor's local `supportsDefault` derived to call through it.
* fix(web): coerce and normalize default values at save time
Two related Codex findings on PR #135:
P1: Coerce default values to active field type before save
Type-switch drift — user sets a boolean default on a checkbox, then
switches the type to `text`, the stale boolean was previously
serialized as the text default. ValidateFields later auto-applies
it to new items without re-validating the value type.
P2: Trim select defaults to match normalized option values
Option text is trimmed on save ("open " -> "open"), but the select
default handler stored raw option text, producing schemas with
`options:["open"]` + `default:"open "` — defaults that aren't in
the allowed set and get auto-injected as invalid values.
Fix: add coerceDefault(raw, type, options?) to field-editor-types.ts.
Returns undefined when the raw value can't be represented in the
target type (caller drops it). Handles:
- text/url -> must be a non-empty string
- number -> number, or parseable non-empty numeric string
- date -> non-empty string (server validates format)
- checkbox -> must be boolean
- select -> trimmed string that exists in normalized options
Wire through all three save paths:
- CreateCollectionModal.handleCreate (new fields)
- EditCollectionModal.handleSave addedFields (new fields)
- EditCollectionModal.handleSave updatedExisting (existing fields,
where the same type-switch risk applies)
The select-options branch passes the already-normalized `def.options`
into coerceDefault so whitespace drift is caught in the same step as
type coercion.
* fix(web): tighten date coercion, preserve opaque defaults, stable keys
Three Codex findings on PR #135:
P1: Validate date defaults before persisting them
coerceDefault was accepting any non-empty string for the date type,
so switching a field from text/select to date could serialize stale
garbage like "soon" as the date default even though the date input
renders blank. Tighten the date branch to require ISO 8601 format
(YYYY-MM-DD, optionally followed by a T-prefixed datetime tail).
Server still performs stricter parsing; this guard blocks obvious
invalid strings from leaking through.
P1: Preserve unsupported field defaults during edit saves
The existing-fields save path dropped `default` whenever
typeSupportsDefault(f.type) returned false. Opening and saving a
collection that contained a multi_select or relation default (e.g.
from an API import) would silently strip those defaults as a side
effect of unrelated edits — schema-mutating regression.
Fix: in the existing-fields branch, if the active type isn't UI-
editable for defaults, pass field.default through verbatim instead
of dropping it. Types that *are* UI-editable still run through
coerceDefault. New-field paths are unchanged because new fields
never carry a pre-existing opaque default.
P2: Use stable unique keys for select default options
The default-value dropdown for select fields keyed its <option>s by
text, but duplicate option labels aren't prevented anywhere in the
editor or save path. A collection with duplicate options would hit
Svelte's keyed-each duplicate-key behavior and break the control.
Switch to keying by index for display stability.
* fix(web): clear stale relation options before async reload
Codex P2 (PR #135): loadCollectionOptions() awaited the fetch before
replacing collectionOptions, so a reopened modal — especially after
a workspace switch — briefly showed the previous workspace's
relation targets. A fast user could pick one and persist a slug that
doesn't exist in the current workspace.
Fix: clear collectionOptions = [] synchronously at the start of
loadCollectionOptions(), before awaiting the request. If the fetch
fails the picker falls back to its empty-state hint. Applied in both
CreateCollectionModal and EditCollectionModal.
* fix(web): token-guard collection fetch + checkbox default clear
Two Codex findings on PR #135:
P2: Ignore stale collection-list responses before setting options
The previous fix cleared collectionOptions at fetch start but still
unconditionally applied whichever response resolved last. Rapid
reopens or slow networks could let an older response land after a
newer one and overwrite it, letting a user persist a relation slug
from the wrong workspace.
Fix: add a monotonic collectionsRequestToken in both modals. Bump it
on each fetch, capture the current value, and drop the response if
the token has moved on when it resolves. Applied in both success
and error paths.
P2: Allow clearing checkbox defaults instead of forcing false
The checkbox default was tri-state at the schema level (no default
/ default false / default true) but the UI only toggled between
true and false. Unchecking stored `false`, and there was no way to
get back to `undefined` — so ValidateFields would auto-inject
`false` into new items even when the user meant "no default".
Fix: add an explicit "Clear" affordance next to the checkbox that
shows only when field.default is set. Clears to undefined, leaving
schema with no default for that field. Preserves the intentional
`false` case (user wants new items to default to unchecked).
* fix(web): calendar-validate date defaults instead of regex shape only
Codex P2 (PR #135): the date branch of coerceDefault accepted any
string matching the YYYY-MM-DD shape, so impossible dates like
"2026-99-99" or "2026-01-32" could be persisted when users switched
a field from text/select to date. ValidateFields later auto-applies
these as defaults on new items without re-checking, propagating
invalid dates silently.
Replace the shape-only regex with real calendar validation:
- Plain date branch (YYYY-MM-DD): parse month/day, then round-trip
through Date.UTC and verify the resulting year/month/day match
the input. Rejects out-of-range components (month > 12) and
overflow cases (day 32 rolling to next month).
- RFC3339 datetime branch: keep the shape check (stricter than a
loose `T.+` suffix — rejects "2026-01-01Tnot-a-time"), then confirm
Date.parse yields a finite timestamp.
Both branches return undefined on rejection so the caller drops the
default rather than persisting garbage.
* fix(web): strict datetime coercion + drop select defaults w/ empty opts
Two follow-up Codex findings on PR #135:
P1: Reject non-RFC3339 datetime defaults in coercion
Previous fix did shape + Date.parse, but `new Date(...)` silently
rolls calendar-invalid dates (e.g. "2026-02-31T10:00:00Z" becomes
March 3) so impossible timestamps still passed. Switch the datetime
branch to the same component-parse + round-trip technique as the
YYYY-MM-DD branch:
- Extract Y/M/D + h/m[/s] from the regex capture groups
- Range-check each component (month 1–12, day 1–31, h ≤ 23, m/s ≤ 59)
- Construct a UTC Date from Y/M/D and verify the resulting
components match the input to catch day overflow
Date.parse is no longer trusted alone. Out-of-range days,
impossible calendar dates, and non-RFC3339 strings are all dropped.
P2: Drop select defaults when normalized options are empty
The save paths passed `def.options` into coerceDefault, but
`def.options` is omitted when the normalized list is empty, so a
select field with no options would skip the membership check and
keep a stale string default. ValidateFields would then auto-apply
a default that doesn't exist in any allowed set.
Fix: in all three save paths, pass the already-normalized opts
array (including []) to coerceDefault when the type is select.
Non-select types continue to pass undefined since they don't
consult the options parameter.
- CreateCollectionModal: use the local `opts` variable
- EditCollectionModal addedFields: use the local `opts` variable
- EditCollectionModal updatedExisting: extract a
`normalizedOpts` local (options were previously inlined) and
reuse it for both def.options and the coerceDefault call
* fix(web): drop stale default on type switch to multi_select/relation
Two Codex findings on PR #135:
P1: Drop stale default when existing field switches to relation/multi_select
The existing-fields save path preserved f.default verbatim for every
UI-unsupported type. That's correct when the field was loaded with a
pre-existing opaque default (API/import). But it misfires when the
user sets a default while the field is text/number/select and then
switches the type to relation or multi_select — the default UI
hides, but the stale value persists and gets saved.
Fix: track the load-time type as `originalType` on EditableField and
only fall through to the verbatim-preserve branch when the active
type still matches the original. In-session type switches to a
UI-unsupported type now drop the default instead. New-field paths
don't need this because new fields never carry pre-existing
opaque defaults.
P2: Enforce strict RFC3339 datetime shape in default coercion
The previous datetime regex accepted optional timezone and
offsets without the colon, so "2026-01-01T10:00" and
"2026-01-01T10:00+0100" round-tripped as defaults even though the
backend's time.RFC3339 parser requires seconds + a colon in the
offset. That lets defaults survive here that the server rejects.
Fix: require seconds, require timezone, require colon in offset.
Matches strict RFC3339 / Go time.RFC3339.
* fix(web): validate RFC3339 timezone offsets in date coercion
Codex P2 (PR #135): the datetime regex enforced the `±hh:mm` shape
but never validated the numeric ranges of the offset, so values like
"2026-01-01T10:00:00+99:99" were treated as valid and serialized.
Go's time.RFC3339 (backend parser) rejects those, and defaults are
auto-applied to new items without re-validation, so an invalid
offset would silently propagate.
Add explicit offset bounds: hours 0–23, minutes 0–59 (matching Go's
time.RFC3339 acceptance of ±23:59). `Z` skips the check. Applied
after the regex match in the datetime branch.
* fix(web): raw string number default + defaults-equal type switch check
Two Codex findings on PR #135:
P2: Preserve raw number input until commit
The number-default input called Number(v) on every oninput and
wrote the coerced value back to field.default. Because the input
was controlled by `value={defaultAsString}`, partial typing states
like "1." collapsed to "1" on each keystroke (Number("1.") === 1),
making it impossible to type decimals. Negative signs had the same
problem.
Fix: keep the raw string in field.default while editing.
coerceDefault already handles string→number conversion at save
time and drops garbage strings, so no save-path change is needed.
P2: Track any type switch before preserving hidden defaults
The existing-fields unsupported-type fallback preserved f.default
whenever the active type matched originalType. That missed the
round-trip case: relation → text → relation with a new default
injected in the middle. Type matches at save but the default is
stale and un-editable through the UI.
Fix: snapshot originalDefault at load alongside originalType, and
only preserve the default when BOTH are unchanged. Otherwise drop.
Add defaultsEqual() helper to field-editor-types.ts for
polymorphic comparison (JSON-stringify-based — fine for schema
defaults, which are always JSON primitives/arrays).
* fix(web): truncate datetime defaults to YYYY-MM-DD for date input binding
Codex P2 (PR #135): <input type="date"> only accepts a YYYY-MM-DD
value. An RFC3339 datetime default like "2026-01-01T10:00:00Z" was
bound directly via defaultAsString and rendered blank, leading users
to believe the field had no default — while field.default remained
populated and was preserved on save through coerceDefault. Result:
hidden datetime defaults that silently survived unrelated edits.
Fix: derive a display-only dateDefaultDisplay string that truncates
anything after the YYYY-MM-DD prefix, and bind the date input to
that. field.default itself stays untouched until the user actually
picks a new date, at which point onDefaultDateInput writes the pure
YYYY-MM-DD value. This keeps API-loaded datetime defaults round-
tripping untouched (when the user doesn't edit them) while making
them visible for manual correction.
Pad
Project management for developers and AI agents.
One binary. Local-first. No accounts. 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 never leaves your laptop.
Quick Start
brew install xarmian/tap/pad
cd your-project
pad auth configure
pad workspace init
pad server open
For a local install, choose Local in pad auth configure. Pad will remember that this client manages a local server, auto-start it when needed, and open the web UI at localhost:7777.
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.
| Pad | Linear / Jira | Notion | |
|---|---|---|---|
| Setup | pad auth configure + pad workspace 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 it | 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 navigation —
j/kto move,Enterto open,Escto go back,Cmd+Kto 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
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, 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"
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:
pad workspace onboard # Analyzes project structure, saves workspace context, and suggests conventions
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 xarmian/tap/pad
Build from Source
git clone https://github.com/xarmian/pad
cd pad
make build
cp pad ~/.local/bin/ # or /usr/local/bin/
Requires Go 1.25+ and Node.js 22+.
The go install github.com/xarmian/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/xarmian/pad
This publishes Pad to localhost:7777 on the host machine, which is the recommended default for local use.
To expose Pad beyond localhost intentionally, publish the port more broadly:
docker run -p 7777:7777 -v pad-data:/data ghcr.io/xarmian/pad
Use broader publishing only when you intend to make Pad reachable from other machines or interfaces.
Binary Download
Pre-built binaries for macOS, Linux, and Windows are available on the releases page.
Getting Started
1. Configure this Pad client
pad auth configure
For most local installs, choose Local. If you're connecting to another Pad server, choose Remote or Docker and enter its base URL.
2. Initialize a workspace
cd ~/projects/myapp
pad workspace init "My App"
This creates a .pad.toml file linking your project directory to a Pad workspace with default collections. Choose a template to start with pre-configured collections:
pad workspace init "My App" --template scrum # Scrum-style with sprints
pad workspace init "My App" --template product # Product management focused
3. Install the AI skill
pad agent install # Auto-detect and install for all found tools
pad agent install claude # Or install for a specific tool
pad agent install cursor
pad agent install copilot
4. 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.
5. Teach your agents the rules
pad workspace onboard # Auto-analyze project, save workspace context, and suggest conventions
# Or browse the convention library
pad library list --type conventions # Pre-built conventions you can adopt
pad library list --type playbooks # Pre-built multi-step workflows
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
pad workspace onboard Analyze project, save workspace context, and suggest conventions
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 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.
Authentication
Pad runs without authentication by default for frictionless local use. On a fresh instance, run pad auth setup on the server host to create the first admin account:
pad auth setup # Initialize the first admin account
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.tomlfile
All data lives in ~/.pad/pad.db. Your data. Your machine. No telemetry, no cloud, no accounts.
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.