task-pigeon
Version:
Small local task queue CLI backed by SQLite.
769 lines (559 loc) β’ 24.5 kB
Markdown
# Pigeon π¦
Pigeon is a small local task queue CLI for coordinating work across people and coding agents.
It stores tasks in a project-local SQLite database, so everyone running Pigeon from the same repo can share a queue without a server, account, daemon, or hosted project tracker.
```text
CLI -> small command functions -> Drizzle -> SQLite
```
Pigeon is intentionally boring: tasks, claims, notes, blockers, done.
## Why Pigeon? β¨
Agent coordination usually fails in small, annoying ways: two agents claim the same work, blocked tasks disappear from view, context lives in chat scrollback, or each checkout quietly creates a separate queue.
Pigeon keeps the coordination surface tiny:
- ποΈ project-local SQLite by default.
- π explicit task claiming with optional owner requirements.
- π¬ tiny message board for cross-agent coordination.
- π URLs and notes built into the happy path.
- π§ blocked work stays visible in the default queue.
- π§Ύ readable responsive output for terminals of different sizes.
- π οΈ config when you need policy, defaults when you do not.
## Install π
Pigeon requires Bun `>=1.3.14`.
Run it without installing:
```sh
bunx --bun task-pigeon@latest status
```
Install it globally for repeated use:
```sh
bun add --global task-pigeon
pigeon status
```
Update a global install:
```sh
bun update --global task-pigeon
```
Direct `bunx` installs from Git URLs are not supported. Use the npm package.
## Quick Start β‘
Run Pigeon from the project directory that should own the queue:
```sh
cd ~/Code/my-project
pigeon init
pigeon config --by andrew
pigeon doctor
pigeon whoami
pigeon status
```
Add work with enough context for the next person or agent:
```sh
pigeon add "Fix failing publish" \
--url https://github.com/andrew-bierman/pigeon/issues/3 \
--note "trusted publishing failure" \
--priority high \
--labels release,npm
```
List and claim work:
```sh
pigeon list
pigeon claim --by codex
pigeon claim 8 --by claude
```
Update task state:
```sh
pigeon update 8 --note "OIDC configured; waiting on npm" --priority urgent
pigeon block 8 --note "waiting on npm account setting" --blocked-by npm
pigeon release 8
pigeon done 8
```
Watch the queue:
```sh
pigeon watch
pigeon monitor --by codex
pigeon monitor --by codex --once # one-shot output for scripts
pigeon monitor --by codex --mentions-only
pigeon monitor --by codex --messages unread
pigeon monitor --by codex --messages all --all-messages
pigeon monitor --by codex --hide-status --hide-tasks
pigeon monitor --by codex --compact
pigeon monitor --by codex --output table
pigeon wait --by codex
pigeon wait --by codex --mentions-only
pigeon wait --by codex --timeout 300000
pigeon watch --all
pigeon watch --status pending --interval 500
pigeon list --status pending,blocked
pigeon list --status active
```
Monitor interactivity:
```text
Monitor runs as a tiny interactive dashboard in TTY terminals.
In non-interactive sessions it stays in plain text and auto-refreshes like before.
[β/k] move [tab] switch pane
[enter] detail [esc/backspace/left] back from detail
[c] claim [d] done [r] release
[a] ack message [z] resolve message
[e] reply message
[n] new message
[g] jump to top [h] toggle help overlay
[t] task section [m] message section [s] status section
[p] pause refresh [f] force refresh [q] quit
```
While the monitor is open, press `n` to write and send a coordination message inline. Messages accept normal `@mentions`, so you can keep the conversation on the same screen without dropping back to a separate command.
Press `e` on a selected message to reply directly to the original author.
The monitor keeps a visible control bar near the top so it is obvious whether you are browsing, in detail, or composing. Detail mode backs out with `Esc`, `Backspace`, or left arrow, and the expanded help overlay stays available with `h`.
By default, `list` and `watch` show active tasks: `pending`, `claimed`, and `blocked`. Use `--all` to include `done`, `--status done` to inspect completed work, or CSV filters like `--status pending,blocked`. `--status active` is shorthand for `pending,claimed,blocked`.
`monitor` hides resolved messages by default. Add `--all-messages` when you explicitly want resolved message history on the screen.
Use monitor visibility flags when you want a focused screen:
```sh
pigeon monitor --compact
pigeon monitor --hide-status
pigeon monitor --hide-tasks
pigeon monitor --hide-messages
```
Use `wait` when an agent is idle, blocked, or waiting for another counterpart. It exits when visible tasks or watched messages change, then the agent should re-run `status`, `list`, and unread messages.
```sh
pigeon wait --by codex
pigeon wait --by codex --messages unread --timeout 300000
```
Leave a message for other agents:
```sh
pigeon message add "@claude Codex is checking release automation" --by codex
pigeon message add "Please review the release notes" --by codex --to claude
pigeon message list --mentioned claude
pigeon message list --by claude --unread
pigeon message ack 1 --by claude
pigeon message resolve 1
```
## Shared Queues π€
By default, Pigeon stores data at `.pigeon/pigeon.db` under the shared project root for the current checkout. Nested worktrees join that same queue unless you explicitly opt into worktree-local mode.
Pigeon discovers the project root from Git metadata, so this works from any Git repository and from any nested directory. Linked worktrees resolve their shared Git root automatically; no `package.json` or Pigeon-specific project setup is required.
Always check the active queue before claiming:
```sh
pigeon status
```
To find stale queues in the current project or linked worktrees:
```sh
pigeon db list
```
This reports each discovered database with task and message counts. It does not scan unrelated directories or the whole disk.
To combine an old worktree queue into the active queue, preview first and then apply:
```sh
pigeon db merge --from .worktrees/pr-1116/.pigeon/pigeon.db
pigeon db merge --from .worktrees/pr-1116/.pigeon/pigeon.db --apply
```
The source database is never modified. New tasks and messages carry a short stable key, so merges match the same record even when mutable fields change. Older databases and snapshots fall back to exact-content matching, while conflicting local IDs are remapped and message read state is preserved.
Numeric IDs remain local SQLite keys for simple commands. Show the stable task keys when you need to compare or document records across databases:
```sh
pigeon list --columns id,uid,status,title
```
To share a queue across directories or machines with a synced path, set `PIGEON_DB`:
```sh
PIGEON_DB=~/tasks/my-project.pigeon.db pigeon status
PIGEON_DB=~/tasks/my-project.pigeon.db pigeon list
```
Agents should use the same project directory or the same `PIGEON_DB`; otherwise they will create separate queues.
## Snapshots π¦
Need a quick backup or a way to move a queue to another machine? Use a JSON snapshot of the current local database:
```sh
pigeon snapshot export --file pigeon.snapshot.json
pigeon snapshot import --file pigeon.snapshot.json
```
Snapshot commands act on the local SQLite queue. If you have remote mode configured in your shell, set `PIGEON_LOCAL_ONLY=1` first so the command stays on the local database.
### Why is remote mode unexpectedly enabled?
`pigeon` uses this precedence for remote settings:
- `PIGEON_URL` / `PIGEON_TOKEN` environment variables
- project config (`.pigeon/config.json` in the current directory)
- user config (`$XDG_CONFIG_HOME/pigeon/config.json`)
- package defaults
If a command unexpectedly calls a remote server, check the resolved value:
```sh
pigeon whoami
pigeon config --show
env | rg '^PIGEON_'
```
To clear a stale remote configuration quickly:
```sh
pigeon config --clear-remote-url --clear-remote-token
```
For a clean local session, isolate both DB and config:
```sh
TMP_CONFIG=$(mktemp -d)
PIGEON_DB=.tmp/pigeon.db XDG_CONFIG_HOME=$TMP_CONFIG pigeon status
```
## Private Remote π
When another computer needs the same queue, the boring option is to run one Pigeon server next to the SQLite file and connect over a private network such as Tailscale.
Server:
```sh
PIGEON_TOKEN=secret pigeon serve \
--host 100.x.y.z \
--port 8787 \
--db /data/pigeon/my-project.db
```
Client:
```sh
pigeon config --remote-url http://100.x.y.z:8787 --remote-token secret
pigeon status
pigeon list
pigeon claim --by andrew
```
Environment overrides work well for temporary sessions:
```sh
PIGEON_URL=http://100.x.y.z:8787 PIGEON_TOKEN=secret pigeon monitor --by codex
```
Remote mode is command-level HTTP, not SQL-over-HTTP. The client sends fixed Pigeon operations such as `listTasks` or `claimTask`; the server owns Drizzle, SQLite, migrations, and locking. The private API is implemented with Elysia and Eden Treaty for a typed client/server contract. That keeps the system as one SQLite writer without introducing sync conflict rules.
Use `pigeon remote` for examples. Use `pigeon whoami` to see whether the current shell is pointed at a remote server.
Clear remote config:
```sh
pigeon config --clear-remote-url --clear-remote-token
```
## Messages π¬
Messages are for coordination context that should not become a task: βI am editing the README,β βClaude is handling issue #12,β βdo not publish until CI is green.β
```sh
pigeon message add "@codex Claude is reviewing the table output" --by claude
pigeon message add "Please check the package metadata" --by claude --to codex
pigeon message reply 3 "I checked it" --by codex
pigeon message list
pigeon message list --mentioned codex
pigeon message list --by codex --unread
pigeon message ack 3 --by codex
pigeon message resolve 3
pigeon message list --all
```
Mentions can come from `@name` in the message body or from `--to name`. Use comma-separated names for multiple counterparts: `--to codex,claude`.
Read state is per agent name. If Codex acknowledges a message, it is still unread for Claude until Claude acknowledges it too.
The sender should usually clean up the message. If you post a message for Claude, check Claude's view with `pigeon message list --by claude`; once Claude has acknowledged or acted on it, resolve the message so it leaves everyone's monitor.
Resolved messages are hidden from normal message lists and monitors for everyone. Use `ack` for βI read thisβ; use `resolve` for βthis no longer belongs on the live board.β Use `pigeon message list --all` to inspect resolved message history.
`pigeon status --by codex` includes Codex's unread message count.
## Identity πͺͺ
Pigeon identity is casual local config, not real sign-in. It is just the default value for commands that accept `--by`, which keeps repeated agent commands shorter and makes monitors easier to read.
```sh
pigeon config --by andrew
pigeon config --user --by codex
pigeon whoami
pigeon status
pigeon claim
pigeon message add "@claude I am taking the npm publish task"
```
Clear it when you want commands to require explicit names again:
```sh
pigeon config --clear-by
```
Environment overrides are useful for one-off agent sessions:
```sh
PIGEON_BY=claude pigeon monitor --mentions-only
```
## Agent Workflow π€
Recommended loop:
```sh
pigeon status
pigeon list --output normal
pigeon claim --by codex
pigeon update 12 --note "Investigating failing publish workflow"
pigeon block 12 --note "waiting on npm trusted publishing"
pigeon wait --by codex
pigeon done 12
```
Use `release` when you claimed the wrong task or need to put work back in `pending`.
Input validation is deliberately strict where it prevents coordination mess:
- `add` requires a non-empty title.
- provided URLs must be valid URLs.
- priority must be `low`, `normal`, `high`, or `urgent`.
- labels are stored as trimmed, deduped CSV.
- `block` requires a non-empty note.
- blank optional `url`, `note`, or `labels` values clear that field.
## Output π
`list` and `watch` use responsive `auto` output by default. When URLs or notes are visible, `auto` uses a readable list so links stay intact while titles and notes wrap with the terminal.
Default columns:
```text
id,status,title,url,claimedBy,updatedAt
```
Use `uid` as an optional stable task key. It is intentionally not in the default columns so normal output stays compact.
Optional task metadata:
```sh
pigeon add "Polish release docs" --priority high --labels docs,release
pigeon update 12 --priority urgent --labels ci,npm
pigeon block 12 --note "waiting on package access" --blocked-by npm
```
For full readable links and notes, you can force normal list output:
```sh
pigeon list --output normal
pigeon watch --output normal
```
Choose columns for one run:
```sh
pigeon columns
pigeon list --columns id,status,priority,title,url,labels,blockedBy,note,claimedBy,updatedAt
pigeon watch --columns id,status,priority,title,url,labels,blockedBy,note,claimedBy,updatedAt
```
Force a compact grid:
```sh
pigeon list --output table
```
Valid columns:
```text
id,status,priority,title,url,claimedBy,blockedBy,labels,note,createdAt,updatedAt,claimedAt,blockedAt,doneAt
```
Valid output modes:
```text
auto,table,normal
```
## Configuration βοΈ
Configuration precedence:
```text
defaults < user config < project config < PIGEON_BY/PIGEON_URL/PIGEON_TOKEN/PIGEON_LOCAL_ONLY/PIGEON_WORKTREE_LOCAL/PIGEON_COLUMNS/PIGEON_OUTPUT/PIGEON_REQUIRE < CLI flags
```
`PIGEON_LOCAL_ONLY=1` disables remote mode for that shell and forces local DB/queue behavior, even if user/project config already has a remote URL.
`PIGEON_WORKTREE_LOCAL=1` opts into per-worktree queue isolation when you intentionally want a separate board in each checkout.
Project config lives next to the active database at `.pigeon/config.json`.
User config lives at `$XDG_CONFIG_HOME/pigeon/config.json`, or `~/.config/pigeon/config.json` when `XDG_CONFIG_HOME` is not set.
Config files are JSON with a top-level `"version": 1`. Older unversioned config files are treated as v1, and Pigeon writes the version on the next config save. Pigeon intentionally does not use executable TypeScript config.
Show effective config, paths, and sources:
```sh
pigeon config --show
```
Set project defaults:
```sh
pigeon config --by codex
pigeon config --columns id,status,priority,title,url,labels,note,claimedBy,updatedAt --output normal
```
Set user defaults:
```sh
pigeon config --user --by andrew
pigeon config --user --columns id,status,priority,title,url,labels,note,claimedBy,updatedAt --output normal
```
Use environment overrides:
```sh
PIGEON_BY=claude pigeon status
PIGEON_URL=http://100.x.y.z:8787 PIGEON_TOKEN=secret pigeon status
PIGEON_COLUMNS=id,status,title,url PIGEON_OUTPUT=normal pigeon list
```
### Required Fields β
Pigeon can require selected command fields from config. Defaults are intentionally loose; project teams can tighten only the fields they care about.
Show the available required-field keys:
```sh
pigeon requirements
```
Require owners on claims for the current project:
```sh
pigeon config --require claim.by
pigeon claim --by codex
```
Require richer task intake too:
```sh
pigeon config --require claim.by,add.url,add.note
```
Clear requirements:
```sh
pigeon config --clear-requirements
```
Use an environment override:
```sh
PIGEON_REQUIRE=claim.by pigeon claim --by codex
```
## Agent Skill π§
Pigeon ships a TanStack Intent skill in the npm package.
Agents that understand Intent can load package guidance for project-local queues, claiming, releasing, blocking, readable output, and config:
```sh
npx @tanstack/intent@latest load task-pigeon#pigeon
```
Intent discovers package skills from installed project dependencies. Install `task-pigeon` in the project first when using `intent load` from a project workspace.
## Command Tour π§
| Command | Purpose |
| --- | --- |
| `pigeon init` | Create or verify the active SQLite queue. |
| `pigeon status` | Show the active database path and status counts. |
| `pigeon add` | Add pending work with optional URL, note, priority, and labels. |
| `pigeon list` | Show active work by default. |
| `pigeon message` | Add, list, ack, and resolve coordination messages. |
| `pigeon claim` | Claim the oldest pending task, or a specific ID. |
| `pigeon release` | Move a claimed task back to pending. |
| `pigeon update` | Edit title, URL, note, priority, or labels. |
| `pigeon block` | Mark work blocked with a required note and optional dependency. |
| `pigeon done` | Mark work complete. |
| `pigeon watch` | Re-render the queue as it changes. |
| `pigeon monitor` | Watch status, tasks, and messages together. |
| `pigeon wait` | Block until visible tasks or messages change. |
| `pigeon serve` | Serve one SQLite queue over private HTTP. |
| `pigeon config` | Show or update project/user defaults. |
| `pigeon columns` | Show output column names and examples. |
| `pigeon requirements` | Show configurable required-field keys. |
| `pigeon identity` | Show local actor examples. |
| `pigeon remote` | Show private remote server examples. |
| `pigeon whoami` | Show the active actor, source, and queue path. |
Common commands:
```sh
pigeon init
pigeon status
pigeon config --show
pigeon whoami
pigeon identity
pigeon remote
pigeon columns
pigeon requirements
pigeon message add "@claude Heads up for other agents" --by codex
pigeon message list --mentioned claude
pigeon message list --by claude --unread
pigeon message ack 1 --by claude
pigeon message resolve 1
pigeon message list --all
pigeon add "Task title" --url https://example.com --note "context"
pigeon list
pigeon list --all
pigeon list --status claimed
pigeon list --status pending,blocked
pigeon list --status active
pigeon claim --by codex
pigeon claim 8 --by codex
pigeon release 8
pigeon update 8 --title "New title" --url https://example.com --note "context"
pigeon block 8 --note "why work is blocked"
pigeon done 8
pigeon watch
pigeon monitor --by codex
pigeon monitor --by codex --mentions-only
pigeon wait --by codex
pigeon wait --by codex --mentions-only
pigeon wait --by codex --timeout 300000
```
## Roadmap πΊοΈ
Pigeonβs goal is still: modern tooling, minimal architecture, useful coordination.
Everything should stay boring in practice: clear, local-first, and easy to reason about.
### What Works Today
- β
Project-local SQLite queues with no server required.
- β
Typed Drizzle schema and migrations.
- β
Strict validation for task titles, URLs, blockers, and configurable required fields.
- β
Atomic claiming so agents do not grab the same pending task.
- β
Active-by-default lists: `pending`, `claimed`, and `blocked`.
- β
Task metadata for priority, labels, and blocking dependency.
- β
CSV status filters and `--status active`.
- β
Responsive readable output that preserves links.
- β
User/project config for columns, output, and required fields.
- β
Casual local identity config for default `--by` values.
- β
Private HTTP remote mode for sharing one SQLite queue across machines.
- β
Cross-agent messages with per-agent read state and global resolve.
- β
Direct message replies that auto-target the original author.
- β
Message mentions plus mention-focused monitor/wait filters.
- β
Interactive monitor dashboard with detail panes, inline compose, and quick actions.
- β
Blocking wait command for agents that need to wake on board changes.
- β
TanStack Intent skill packaged with npm.
- β
Trusted npm publishing through GitHub Actions.
- β
Lightweight JSON snapshots for backup/recovery.
### Launch Gates (Before public)
- β
Finalize release/upgrade docs (bunx, global install, version bump flow).
- β
Publish a short βfresh machineβ validation flow for `whoami`, `config`, `status`, and `monitor`.
- β
Add a `CHANGELOG.md` and keep release notes current.
- β
Add explicit local-only remote fallback guidance in docs:
- `PIGEON_LOCAL_ONLY=1`
- `whoami` + `doctor` checks
- β
Make `.pigeon` + DB path behavior explicit for avoiding queue forks across machines.
### Near-Term (Post-Launch, if still boring)
- Better message ergonomics: `message list --resolved`, `message ack --all --by <name>`.
- Monitor polish for longer sessions (sectioned defaults and clearer visibility hints).
- Optional shell completion if it stays clean with CitTY and doesnβt add complexity.
- More flexible required fields only if usage proves the defaults are insufficient.
### Maybe Later
These are interesting, but only if they stay boring:
- Due dates if they are just metadata.
- Dependency polish if `blockedBy` needs more than plain text.
- `message reply` only if flat messages become hard to scan.
- A compact TUI only if the plain CLI remains first-class.
- Optional GitHub issue helpers for URLs, without becoming a GitHub client.
- Direct Cloudflare D1 support if the implementation can stay small and local SQLite remains the default.
- Hosted SQLite experiments, likely Turso/libSQL first, only if private remote is not enough.
## Production Checklist π¦
Before publish from a clean clone:
- `bun install`
- `bun run ci`
- `bun run smoke`
- `bun run build`
- `bun run build:binary`
- `npm pack --dry-run`
Fresh-machine verification:
```sh
bunx --bun task-pigeon@latest status
bunx --bun task-pigeon@latest whoami
bunx --bun task-pigeon@latest config --show
PIGEON_LOCAL_ONLY=1 bunx --bun task-pigeon@latest status
```
Release flow (when youβre ready):
```sh
bun run release:patch # or bun run release:minor
git push
git push --tags
```
Publishing is executed by GitHub Actions using npm trusted publishing.
Do not run `npm publish` locally for normal releases. If you need to trigger a publish manually, use the `Publish` GitHub Actions workflow dispatch or the GitHub UI.
### Non-Goals
These are how Pigeon avoids becoming the thing it was built to avoid:
- No hosted service.
- No daemon required.
- No plugin system.
- No workflow engine, event bus, or automation framework.
- No repository/service/adapter layers just for architecture theater.
- No multi-user permissions model beyond private network access plus optional bearer token.
- No attempt to replace Linear, GitHub Issues, Slack, or project management tools.
The bar for new features: can two agents use it immediately, does it make coordination clearer, and can the code still be explained as `CLI -> small command functions -> Drizzle -> SQLite`?
## Development π§ͺ
```sh
bun install
bun run ci
```
Useful scripts:
```sh
bun test
bun run test:coverage
bun run check
bun run typecheck
bun run smoke
bun run build
bun run build:binary
```
`bun run check` runs Biome, the project policy checker, and TanStack Intent validation.
The policy checker enforces the intentionally flat architecture:
- CitTY for CLI parsing.
- Drizzle-centered database access.
- centralized `PIGEON_DB` reads.
- single `options` object params for command functions.
For local CLI development:
```sh
bun install
bun run build
bun link
pigeon --help
```
For code work where you want to avoid touching your active queue/config (same machine, same user), use the dev runner:
```sh
bun run dev
bun run dev -- status
bun run dev -- add "Try command in isolation"
bun run dev -- monitor --once
```
`bun run dev` always uses:
- a temporary database path
- a temporary user config directory
- `PIGEON_LOCAL_ONLY=1` (no remote calls)
This lets you iterate from any project without changing what other agent sessions are using.
If you are shipping this for others, `pigeon doctor` is the quality default you can suggest to every support request:
```sh
pigeon doctor
```
It prints the resolved DB path, config sources, and remote state so people can self-diagnose in 10 seconds.
For the installed package (or `bunx`) while developing:
```sh
PIGEON_LOCAL_ONLY=1 \
PIGEON_DB="$(mktemp)/pigeon-dev.db" \
XDG_CONFIG_HOME="$(mktemp -d)/xconfig" \
bunx --bun task-pigeon@latest status
```
## Release π¦
See [CHANGELOG.md](./CHANGELOG.md) for release notes.
Patch release:
```sh
bun run release:patch
git push
git push --tags
```
Minor release:
```sh
bun run release:minor
git push
git push --tags
```
Publishing runs from GitHub Actions on `v*` tags using npm Trusted Publishing. Use the GitHub Actions publish workflow rather than local npm publish commands.