o
+--\----+
| \ |
| punch |
| list |
+-------+
A lightweight, local, markdown-native task and ticket system for humans and AI agents — no database, no app, no account required.
Most task tools are apps: databases behind a UI, locked to a service, requiring context-switches away from your work. GitHub Issues are powerful but heavy — and don't travel with your local project. Plain text files are too loose to query or automate.
Punchlist sits in the middle: every task is a plain markdown file with YAML frontmatter, committed alongside your code, editable in any text editor, queryable with --json, and legible to both humans and AI agents without translation.
- No database. Tasks are
.mdfiles in atasks/folder. - No app. Open any task in nvim, VS Code, Obsidian, or anything else.
- No lock-in. Plain markdown and YAML. If you stop using
pin, your files are still there. - Travels with your project. Commit your tasks alongside your code.
- Works with your AI assistant. JSON output, structured metadata, dependency tracking, and acceptance criteria make punchlist a first-class context layer for AI agents and coding tools.
pin init # initialize any folder as a punchlist project
pin "write release plan" pri:1 # create a task (defaults to TODO)
pin ls # list all tasks
pin browse # interactive TUI browserThe pin command grammar is natural and tolerant. State is optional; if omitted, the task defaults to TODO.
pin "write outline"
pin todo "draft messaging brief" pri:1
pin todo "send pr draft" by:2026-01-15
pin todo "ship notes" by:tomorrow
pin todo "review plan" by:friday
pin todo "write release plan" pri:1 by:2026-01-09 tags:{launch,pr}
pin todo "Deploy to prod" depends:3,4 pri:1
pin done "shipped quick fix for onboarding typo"
pin begun "triage the backlog"
pin block "waiting on vendor response"
pin todo ../work "queue follow-up" # create in another project folderEach task gets a numbered markdown file (tasks/001-write-outline.md) with YAML frontmatter and a body you can freely edit.
Scope: Each initialized folder has its own independent task list. Use an explicit path to target a different project:
pin ls ../work # list tasks in another project
pin todo ../work "queue follow-up" # create a task in a specific projectpin walks up from the current directory to find the nearest .punchlist/ folder, so it's usually correct — but worth checking when working across sibling projects.
pin ls # all tasks, grouped by state
pin ls todo # filter by state
pin ls todo --tag launch # filter by tag
pin ls --order id # sort by id instead of state
pin ls todo --chunk 10 # show first 10 matches
pin ls todo --chunk 10 --page 2
pin ls todo --by-priority
pin ls todo --by-date-reverse
pin search "release" # full-text search across title, body, frontmatter
pin show 12 # full task detailAll read commands support --json for use in scripts and AI pipelines:
pin ls --json
pin ls todo --json
pin show --json 12
pin search --json "keyword"
pin ls --ready --json | jq '.[0].id' # next actionable taskpin ls --json returns a task array without body. pin show --json includes the full body, metadata, and acceptance criteria.
States are fully configurable. Default states from pin init:
| State | Aliases | Browse hotkey |
|---|---|---|
TODO |
todo | t |
BEGUN |
begun, started, inprogress | b |
FOLLOWUP |
followup, confirm | f |
DEFER |
defer | l |
NOTDO |
notdo | x |
DONE |
done, complete, completed | d |
Change state with any state token:
pin start 12
pin done 12
pin block 12
pin todo 12
pin REDLIGHT 12 # any custom state token works
pin begun "triage the backlog" # state + new task title creates a task in that stateAdd or rename states by editing states: in .punchlist/config.yaml. All commands respect the configured ordering.
pin browse
pin browse todopin browse opens a keyboard-driven TUI viewer. Move between tasks with ←/→, K/J, or space; scroll long task bodies with ↑/↓, pgup/pgdn, home, and end; update state with hotkeys, add notes with n, open in your editor with e, set priority with 1–9/0, quit with q.
When a state hotkey is used in browse, the state is applied and the cursor advances to the next task.
pin note 12 "call vendor and confirm timeline" # append a note
pin tag 12 15 "today, blocked" # add tags to one or more tasks
pin due 12 "next tuesday" # set or change due date
pin due 12 2026-01-15
pin edit 12 # open in $EDITORpin done 2 3 6-9
pin del "[2-3, 7]" # quote brackets in zsh to avoid glob expansionPunchlist is designed as a first-class AI agent substrate: structured enough to be queried and automated, simple enough to edit by hand.
Capture where a task came from, who assigned it, and to whom:
pin meta 1 source=standup-2026-02-27 from=alice to=bob
pin meta 1 # display all metadata
pin meta 1 from= # delete a key (empty value)
pin show --json 1 | jq .meta # read in JSONMetadata lives in meta: YAML frontmatter — invisible to human users who don't need it, fully accessible to agents that do.
Structured checkboxes from a ## Acceptance section in the task body:
pin acceptance 1 # list criteria with indices (alias: pin checks)
pin check 1 2 # toggle item 2 checked/unchecked
pin show --json 1 | jq .acceptance # structured array: text, checked, indexpin "Deploy" depends:1,2 pri:1 # create task with dependencies
pin deps 5 # forward deps + reverse lookup
pin ls --ready # only tasks whose deps are all DONE
pin ls --ready --json | jq '.[0].id' # pick next actionable task---
state: todo
pri: 2
by: 2026-02-01
tags: [ai, assistant]
depends_on: [3, 4]
meta:
source: standup-2026-02-27
from: alice
to: bob
---
# <short, testable outcome>
## Goal
<one sentence>
## Acceptance
- [ ] <verifiable result>
- [ ] <test or command to run>
## Context
- Files: <paths>
- Links: <issues/docs>
## Notes for assistant
- Constraints: <style/tech>
- Assumptions: <if any>- Keep tasks small and specific — one outcome per ticket.
- Include concrete acceptance checks; use
pin checkto toggle them as you go. - Use
pin metato capture provenance: source meeting, who assigned it, and to whom. - Use
depends:andpin ls --readyso agents always pick the right next task. - Treat
pin noteas a running log — decisions, commands run, blockers hit. - See
AGENTS.mdfor machine-facing guidance anddocs/assistant-brief.mdfor an extended agent workflow guide.
The punchlist MCP server gives AI assistants (Claude Desktop, Claude Code, Cursor, or any MCP-compatible client) direct, structured access to your tasks. Instead of grepping markdown files and burning tokens on raw text, the AI navigates typed data: domains, configs, filtered queries, metadata-only listings.
The MCP server is a standalone Python process that reads and writes the same files pin does. They coexist — use pin from the terminal, use MCP from your AI tools, edit tasks in Obsidian or nvim. The filesystem is the shared contract.
| Without MCP | With MCP |
|---|---|
| AI greps 600 task files to find blocked items | punchlist_list(state="BLOCK") — metadata only, ~500 tokens |
| AI guesses at file naming and config format | punchlist_create(title="...") — correct by construction |
| AI reads entire task bodies to answer summary questions | punchlist_summary — counts by state, priority, tags |
| Context window fills with raw markdown | Metadata-first: bodies only on explicit punchlist_get |
cd mcp/
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Add to your project's .mcp.json or ~/.claude.json:
{
"mcpServers": {
"punchlist": {
"command": "/path/to/punchlist/mcp/.venv/bin/python3",
"args": ["/path/to/punchlist/mcp/server.py", "--root", "/path/to/workspace"]
}
}
}Same JSON format, placed in:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
| Tool | Purpose | Token cost |
|---|---|---|
punchlist_discover |
List all domains, states, task counts | Very low |
punchlist_list |
Filtered metadata (no body) — state, tag, priority, search, date | Low |
punchlist_get |
Full task with body, notes, log | Medium |
punchlist_search |
Full-text across titles and bodies | Medium |
punchlist_create |
Create task — auto-ID, correct filename, config update | Low |
punchlist_update |
Change state/priority/tags, add notes — auto-logged | Low |
punchlist_summary |
Dashboard: counts by state, priority, top tags | Medium |
punchlist_cross_domain |
Query across all domains at once | Medium–High |
Point --root at a parent directory and the server discovers all nested punchlist domains automatically:
workspace/
.punchlist/config.yaml ← root config (shared defaults)
home/
.punchlist/config.yaml ← domain: "home"
tasks/
work/
.punchlist/config.yaml ← domain: "work"
quantum/
.punchlist/config.yaml ← domain: "work/quantum" (nested)
tasks/
Each domain has its own states, ID counter, and task set. The root config provides fallback defaults.
Task files are plain markdown with YAML frontmatter — Obsidian reads them natively. Point an Obsidian vault at your workspace, edit tasks in Obsidian's editor, and the MCP server sees changes immediately (no caching, no sync). The MCP server adds structured query and mutation that Obsidian doesn't have; Obsidian adds rich editing and linking that the MCP doesn't need.
1. punchlist_discover → orient: what domains, what states
2. punchlist_summary → lay of the land: counts, recent activity
3. punchlist_list(state=...) → filtered metadata
4. punchlist_get(id=...) → drill into one task
5. punchlist_update(id=...) → change state, add notes
For implementation details, see mcp/PLAN.md (architecture) and mcp/CLAUDE.md (setup guide).
.punchlist/config.yaml supports:
states: list of state definitions withname,aliases, andtui_hotkeyls_state_order: custom state ordering forpin lsnext_id: next task id (auto-managed)id_width: zero padding width for filenames (default 3)title_max_len: max stored title length before truncation (default 80)ls_title_max_len: max title length shown inpin ls(default 80)edit_start_insert: add+startinsertfor vim/nvim (default true)edit_goyo: add+Goyofor vim/nvim (default false)browse_margin: columns of left/right margin inpin browse(default 12)
pin config # open config in $EDITOR
pin config migrate # backfill new config fields without overwriting yoursState config rules: name and aliases must be single-word tokens; tui_hotkey must be a single character and cannot conflict with reserved keys (n, q, j, k, J, K, space, e, 0–9).
your-project/
.punchlist/
config.yaml # project config
tasks/
001-write-outline.md
002-review-plan.md
...
.trash/ # deleted tasks land here (not permanently removed)
The punchlist source tree includes:
punchlist/
cmd/ # Go CLI commands
config/ # config loading and state catalog
task/ # task parsing and serialization
mcp/ # MCP server (Python, optional)
server.py # MCP server implementation
pyproject.toml # Python package config
.venv/ # Python virtual environment (after setup)
docs/ # extended documentation
AGENTS.md # machine-facing agent instructions
Tasks are plain markdown files with YAML frontmatter. Config is plain YAML. Nothing is hidden, encoded, or proprietary.
Get notified when you cd into a punchlist-enabled folder:
# find nearest parent with .punchlist
_punchlist_root() {
local d="$PWD"
while [[ "$d" != "/" ]]; do
[[ -d "$d/.punchlist" ]] && { print -r -- "$d"; return 0 }
d="${d:h}"
done
return 1
}
_punchlist_task_count() {
local root tasks_dir
root="$(_punchlist_root)" || return 1
if [[ -d "$root/tasks" ]]; then
tasks_dir="$root/tasks"
elif [[ -d "$root/.punchlist/tasks" ]]; then
tasks_dir="$root/.punchlist/tasks"
else
return 1
fi
local -a files
files=("$tasks_dir"/*.md(N))
print -r -- "${#files[@]}"
}
typeset -g _PUNCHLIST_LAST_COUNT=""
_punchlist_maybe_notice() {
[[ -o interactive ]] || return 0
local count
count="$(_punchlist_task_count)" || { _PUNCHLIST_LAST_COUNT=""; return 0 }
if [[ "$count" != "$_PUNCHLIST_LAST_COUNT" ]]; then
local plural=""
(( count != 1 )) && plural="s"
print -r -- "${count} task${plural}. Use \`pin ls\` to review."
_PUNCHLIST_LAST_COUNT="$count"
fi
}make check # go test ./... && go vet ./...
make build # builds ./pin with version from VERSION file
make install # go install with version ldflagsFor command grammar details, see docs/grammar.md.
Punchlist is open source software.
- Author: Skip Levens
- Organization: Giant Ravens
- License: MIT
- Project home: https://github.com/GiantRavens/punchlist