xarmian 9e7daa779f feat: tie done-detection to the board group-by field (TASK-604) (#140)
* feat: tie done-detection to the board group-by field

Closes TASK-604. Make "is this item done?" follow the collection's
settings.board_group_by rather than the hardcoded `status` key. If a
collection's board is grouped by `resolution`, then resolution's
terminal options drive dashboard counts, progress bars, changelog,
and starred-items filtering. Collections without an explicit
board_group_by (every collection today) continue to behave exactly
as before because the fallback resolves to `"status"`.

Why this shape
- No ambiguity: one field per collection wins. No reconciling
  "status says in-progress, resolution says fixed."
- One JSON path to swap: every $.status query becomes
  $.<done_field>. No dynamic OR across schema-discovered fields.
- Matches the mental model: the field you organize the board by is
  the field that represents the item's current state. The old
  mismatch (board grouped by X, "done" count from status) is a
  latent bug this resolves.
- Non-breaking: board_group_by defaults to nil → DoneFieldKey
  returns "status" → behavior identical to pre-TASK-604.

Model layer (internal/models/terminal.go)
- DoneFieldKey(schema, settings) resolves the done-field key with a
  fallback chain: valid select on schema → that field, else "status".
- TerminalValuesForDoneField(schema, settings) returns (fieldKey,
  values) honoring the done field, falling back to
  DefaultTerminalStatuses when the resolved field has no
  terminal_options.
- TerminalPlaceholdersForDoneField(schema, settings) is the SQL
  convenience returning (fieldKey, placeholders, args).
- IsTerminalItem(fields, schema, settings) is the canonical
  Go-side membership check.
- Legacy API (TerminalStatusesFromSchema, IsTerminalStatus,
  TerminalStatusPlaceholders) kept as back-compat wrappers that
  delegate with empty settings — resolve to "status" for callers
  that don't have settings in scope yet.

SQL callers migrated to the new helpers
- internal/store/collections.go ListCollections active-count query
- internal/store/items.go GetItemProgress + GetAllItemProgress:
  - New collectionDoneFilter type + childrenDoneFiltersFor{Parent,
    Collection} + doneFiltersForWorkspace helpers load each
    candidate collection's (schema, settings) and resolve per-
    collection done keys + terminals.
  - buildChildrenDoneExpr(filters, alias) compiles filters into a
    single SQL boolean expression using per-collection OR clauses:
      ((alias.collection_id=? AND LOWER(...)
        IN (?,?)) OR (alias.collection_id=? AND LOWER(...)
        IN (?,?)) ...)
  - Each child item is evaluated against its own collection's
    done rules, so mixed-collection child progress is correct
    without a global union hack.
- internal/store/agent_roles.go GetRoleBreakdown + Go-side filter
- internal/store/item_stars.go starred-items filtering now uses a
  collectionDoneContext map (schema + settings) and IsTerminalItem.

Go-side callers migrated
- internal/server/handlers_dashboard.go: buildSchemaMap →
  buildDoneContextMap (carries settings), isItemTerminal →
  isItemDone (evaluates against the done field). 7 call sites
  updated.
- internal/server/handlers_items.go: plan-progress recompute and
  per-item /progress endpoint now use the done-context approach.

Left status-specific (per task scope)
- Link-payload $.status extracts in items.go getItemLink /
  GetItemLinks / GetParentForItem — these populate
  link.SourceStatus / link.TargetStatus, which are status-specific
  by design.
- cmd/pad reconcile paths — no schema in scope, default-list
  fallback is the right call.
- search.go facet "status breakdown" — a different UX concept
  (bucket search results by status values) than done-detection.

Web UI reactivity
- FieldEditor: new activeDoneField prop. Each modal derives it from
  boardGroupBy with the same fallback rule as the Go DoneFieldKey.
- Fields tab: the "Done?" column header on each select field renders
  an "Active" green pill when that field is the board group-by, or a
  muted "Saved" pill + inline hint otherwise ("Switch the board
  group-by to <key> to make them drive done-detection"). Reactive to
  boardGroupBy changes in the Display tab.
- DisplaySettingsEditor: "Board group by" label gets a helper line
  explaining the new responsibility.

Tests
- internal/models/terminal_test.go: 13 unit tests covering fallback
  resolution, placeholder args, membership (case-insensitive), and
  back-compat shim semantics.
- internal/store/done_field_test.go: 3 integration tests:
  1. Bugs collection grouped by resolution → items with terminal
     resolution values count as done; items with status=fixed but
     resolution=open do NOT count as done (proves status is no
     longer consulted when it isn't the done field).
  2. Collection without board_group_by still uses status terminals.
  3. Mixed-collection children: each child evaluated against its
     own done rules.
All pass alongside the full existing suite.

* fix: restrict done field to select (reject multi_select)

Two linked Codex P1 findings on PR #140, both rooted in the same
gap: multi_select fields store their values as JSON arrays, but both
the Go-side membership check (IsTerminalItem) and the SQL done
expression (buildChildrenDoneExpr) assume a scalar string. Naively
accepting multi_select as a done field would silently miss items
whose terminal value is one of several in the array — dashboards
and progress would report wrong counts.

Rather than implement array-containment semantics across both
paths (which would require deciding "any terminal value → done" vs
"all terminal values → done", SQL-dialect-aware JSON-contains, and
new tests for both shapes), close the gap with a constraint: only
select fields qualify as a done field. If array semantics become
a requirement later, that's a focused follow-up that can update
both paths together with a clear definition.

Changes
- DoneFieldKey and TerminalValuesForDoneField: loop bodies now
  match only `select`, not `select || multi_select`. A
  board_group_by pointing at a multi_select field falls back to
  'status' — matching the rule for non-existent or non-select
  fields.
- IsTerminalItem: docstring made the scalar contract explicit;
  non-string values (which would be the multi_select array shape)
  already returned false, which is now the deliberate behavior.
- buildChildrenDoneExpr: added a doc note that the scalar
  JSON_EXTRACT path is correct because the upstream resolution
  only hands us select fields.
- Web UI: EditCollectionModal + CreateCollectionModal derive
  activeDoneField matching the backend rule (select only), and
  FieldEditor.isActiveDoneField gates on field.type === 'select'.
  A multi_select field never lights up the green "Active" pill now,
  even if a user somehow pointed board_group_by at one.

Tests
- Replaced TestDoneFieldKey_AcceptsMultiSelect with
  TestDoneFieldKey_RejectsMultiSelect. Asserts that a multi_select
  board_group_by falls back to 'status' instead of being honored.
- Existing 12 unit tests + 3 integration tests all still pass.

* fix: include soft-deleted collections in done-filter loaders

Two related Codex P2s on PR #140. The done-filter loaders were
limiting their SELECT to collections with deleted_at IS NULL, but
the outer callers (GetItemProgress, GetAllItemProgress,
GetRoleBreakdown) count items regardless of their collection's
deleted_at. Net effect: after a collection was soft-deleted, its
items lost their per-collection clause in buildChildrenDoneExpr and
were always evaluated as non-terminal — undercounting done in plan
progress and inflating active counts in the role breakdown.

Fix
Drop the `c.deleted_at IS NULL` guard from all three filter
loaders:
- childrenDoneFiltersForParent
- childrenDoneFiltersForCollection
- doneFiltersForWorkspace

Soft-deleted collections still have valid schema + settings rows in
the DB, so the done rules remain applicable until a hard delete
cascades. This also matches what the outer queries count: if they
include items from a soft-deleted collection, the filter loaders
must too.

Regression test
TestGetItemProgress_HonorsSoftDeletedChildCollections:
  1. Create a parent + two children in a child collection where one
     child is done and one is open — assert done=1.
  2. DeleteCollection on the child collection (soft-delete).
  3. Re-run GetItemProgress — assert done is still 1, not 0.
Fails before the filter-loader fix, passes after.

* fix: avoid N+1 in plans progress + preserve done fallback on bad schemas

Two Codex P2s on PR #140.

P2: Avoid N+1 list-collection queries in plans progress
handlePlansProgress's restricted path was calling s.store.
ListCollections solely to build a ctxMap, but ListCollections runs a
separate active-item COUNT query per collection (collections.go),
burning O(number of collections) round-trips on every call. In
larger workspaces this materially inflates latency and can cause
timeouts. Add a lightweight Store.ListCollectionsMinimal that
returns only the ID / Schema / Settings needed for done-context
construction and skips the count queries entirely. Handler switches
to it.

P2: Preserve done fallback for unparseable collection schemas
scanCollectionDoneFilters was `continue`-ing past collections whose
schema failed to parse. Because buildChildrenDoneExpr composes a
per-collection OR clause and only applies the default-list fallback
when NO filters are constructed overall, a single malformed
collection could leave its items without a matching clause —
silently marking them as perpetually active in progress / role /
starred queries. Emit a fallback filter (status + DefaultTerminal-
Statuses) for that collection instead of skipping it, matching
pre-TASK-604 behavior for its items while still honoring the
configured rules for every other collection.

* fix: sanitize done-field keys + cover granted-item collections

Two more Codex findings on PR #140.

P1: Sanitize done-field keys before embedding SQL JSON paths
buildChildrenDoneExpr passes the resolved done-field key straight
into JSONExtractText, whose dialect implementations interpolate it
as a string literal inside `json_extract(..., '$.<key>')` /
`-->>'<key>'`. Schema / settings rows are persisted without backend-
side key validation, so a crafted board_group_by (e.g. a key with
quotes, semicolons, or SQL metacharacters) could break the
resulting query or inject. Since TASK-604 made done-field
resolution dynamic, this needs a chokepoint.

Fix: DoneFieldKey now refuses to resolve to any candidate that
doesn't match ^[a-zA-Z][a-zA-Z0-9_]*$ and falls back to the literal
"status" (which is always safe). The pattern matches the convention
already in use for search-field filtering in internal/server/
handlers_search.go.

Added TestDoneFieldKey_RejectsUnsafeKeys covering injection-shaped
strings, dots, dashes, leading digits, empty strings, and spaces.

P2: Include granted-item collections in dashboard done context
The dashboard was filtering `collections` by visibility BEFORE
building ctxMap, but allItems can still include items from
collections outside the visibility set via item-level grants
(dashItemIDs). Those items missed their own done-rules and
fell back to the status-default, misclassifying them for guests
with item-level grants in collections that use a non-status done
field.

Fix: build ctxMap from ListCollectionsMinimal(workspaceID) first —
always covering every collection in the workspace — then apply
visibility filtering to `collections` for the summary section only.
isItemDone now sees the real done rules for every item the
dashboard iterates, regardless of how visibility surfaced it.

* fix(web): mirror backend safe-key check in activeDoneField derivation

Codex P2 on PR #140. The previous commit added a safe-key regex on
the backend (DoneFieldKey rejects keys outside ^[a-zA-Z][a-zA-Z0-9_]*$
and falls back to "status"), but the Web activeDoneField derivation
in both modals only checked type === 'select'. For legacy / API-
created schemas carrying keys like `resolution-v2` or `foo.bar`, the
Fields tab would display an "Active" green pill on that field even
though the server silently ignores it and falls back to status. Users
could configure terminal options on the wrong field and never see
them take effect.

Fix: export isSafeDoneFieldKey from field-editor-types.ts (a tiny
helper wrapping the same regex the backend uses) and gate both
modals' activeDoneField derivations on it. Unsafe keys fall back to
'status' in the UI, matching the backend's behavior exactly —
Active/Saved pills are now truthful.
2026-04-17 21:45:55 -04:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad

Project management for developers and AI agents.

CI Release License


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 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

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.toml file

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.

License

Apache License 2.0

Languages
Go 64.8%
TypeScript 21.5%
Svelte 13.1%
Shell 0.3%
CSS 0.1%