Create, update, query, and organize project work items (tasks, bugs, subtasks) as markdown files with YAML frontmatter. Use when the user asks to create a ticket or task, track work, check task status, assign work, manage a backlog, or organize project work. Also use when the user asks things like "what's left to do", "what am I working on", "mark this as done", "create a bug for this", "what tasks are in progress", "show me the backlog", or "what's the status of the project". Use for reviewing tickets, filtering tasks by status or priority, doing code reviews against task acceptance criteria, or querying the task board. Tasks are stored in the local project's .sandpiper/tasks directory.
Manage project work items as markdown files with YAML frontmatter, stored in the local project's .sandpiper/tasks directory.
For the full normative specification, see references/SPEC.md. This skill document provides operational guidance and quick reference.
Project routing triggers (whenToRead fields) are automatically injected into the system prompt by the sandpiper system extension at session start. You do not need to run project list to get routing context — it is already in your context as an <available_projects> block.
If you need the full project list with task counts (e.g., to assess project health or find the right key), run:
scripts/sandpiper-tasks project list
Projects with no PROJECT.md will appear with empty whenToRead. If you create a new project, you MUST provide , , and .
--name--description--when-to-readtask create -p SHR -t "Implement feature X" --priority HIGH --reporter USERtask pickup SHR-1task create -p SHR -t "Write parser" -k SUBTASK --parent SHR-1 --reporter AGENTRefs: SHR-1 in commit messagesBugs found while working on a task are their own top-level work items, not subtasks — because bugs have independent lifecycle and priority, and a subtask implies "part of the parent's scope." Link them instead:
task create -p SHR -t "Race condition in FIFO reader" -k BUG --priority HIGH --reporter AGENTtask update SHR-1 --blocked-by SHR-5 (if the bug blocks the original task)task complete SHR-1 (sets status to NEEDS REVIEW)task complete SHR-1 --final --resolution DONEtask complete SHR-1 --final --resolution WONTFIXtask summary -p SHR — status/priority breakdown at a glancetask list -s IN_PROGRESS — what's actively being worked ontask list -s NOT_STARTED --priority HIGH — high-priority backlog itemsThe sandpiper-tasks CLI is the primary interface for task operations. It is bundled at scripts/sandpiper-tasks relative to this skill's directory. Resolve this path against the skill directory to get the absolute path before running commands.
scripts/sandpiper-tasks <command>
# Or with explicit directory:
scripts/sandpiper-tasks --dir /path/to/project <command>
| Flag | Description |
|---|---|
-d, --dir <path> | Path to directory containing .sandpiper/tasks (defaults to cwd) |
-f, --format <fmt> | Output format: raw, json, toon |
--no-save | Skip disk writes and index update; output only (implies --format raw) |
scripts/sandpiper-tasks task create -p SHR -t "Implement feature X" --priority HIGH --reporter USER
scripts/sandpiper-tasks task create -p SHR -t "Fix the bug" -k BUG --priority HIGH --reporter AGENT
scripts/sandpiper-tasks task create -p SHR -t "Write tests" -k SUBTASK --parent SHR-1 --reporter AGENT
scripts/sandpiper-tasks task pickup SHR-1
scripts/sandpiper-tasks task pickup -p SHR --filter-status NOT_STARTED # bulk
scripts/sandpiper-tasks task complete SHR-1 # → NEEDS REVIEW
scripts/sandpiper-tasks task complete SHR-1 --final --resolution DONE # → COMPLETE
scripts/sandpiper-tasks task complete SHR-1 --final --resolution WONTFIX # → COMPLETE (won't fix)
scripts/sandpiper-tasks task update SHR-1 --status IN_PROGRESS --assignee AGENT
scripts/sandpiper-tasks task update SHR-1 --priority LOW
scripts/sandpiper-tasks task update SHR-1 -t "New title" # rename
scripts/sandpiper-tasks task update SHR-1 --desc "New description text" # set description
scripts/sandpiper-tasks task update SHR-1 --depends-on SHR-2,SHR-3 # set dependencies
scripts/sandpiper-tasks task update SHR-1 --related "" # clear relationships
scripts/sandpiper-tasks task update SHR-1 -i # open in $EDITOR
scripts/sandpiper-tasks task update SHR-1 -i --status IN_PROGRESS # pre-apply fields, then edit
scripts/sandpiper-tasks task update -p SHR --filter-status IN_PROGRESS --assignee USER # bulk
scripts/sandpiper-tasks task move SHR-1 -k BUG # convert TASK → BUG
scripts/sandpiper-tasks task move SHR-3 -k TASK # promote SUBTASK → TASK
scripts/sandpiper-tasks task move SHR-5 -k SUBTASK --parent SHR-1 # demote to subtask
scripts/sandpiper-tasks task move SHR-1 -p CLI # move to CLI project (re-keys)
scripts/sandpiper-tasks task move SHR-1 -p CLI -k BUG # move + convert in one step
Moving across projects re-keys the task and all its subtasks, creates .moved tombstone files, and updates all inbound references in other task files.
scripts/sandpiper-tasks task list # all tasks
scripts/sandpiper-tasks task list -p SHR --top-level # top-level SHR tasks
scripts/sandpiper-tasks task list -s NOT_STARTED --priority HIGH # high priority not started
scripts/sandpiper-tasks task list -q "FIFO" # full-text search
scripts/sandpiper-tasks task show SHR-1 # full detail + subtasks
scripts/sandpiper-tasks task show SHR-1 --metadata-only # frontmatter fields only, no body or subtasks
scripts/sandpiper-tasks --format toon task show SHR-1 --metadata-only # structured metadata (minimal context cost)
scripts/sandpiper-tasks task summary # status/priority breakdown
scripts/sandpiper-tasks task summary -p SHR # project-scoped summary
scripts/sandpiper-tasks project list # list all projects with task counts + metadata
scripts/sandpiper-tasks --format toon project list # structured output with whenToRead + task counts
scripts/sandpiper-tasks project create SHR \
--name "Shell Relay" \
--description "Zellij-based shared terminal" \
--when-to-read "Use for relay extension work" # create project (all three flags required)
scripts/sandpiper-tasks project show SHR # show PROJECT.md for a project
scripts/sandpiper-tasks project update SHR --when-to-read "New trigger" # update metadata field(s)
scripts/sandpiper-tasks project update SHR -i # open PROJECT.md in $EDITOR
scripts/sandpiper-tasks task archive # archive all completed tasks
scripts/sandpiper-tasks task archive -p SHR # archive only SHR completed tasks
scripts/sandpiper-tasks task archive --list # list already-archived tasks
scripts/sandpiper-tasks --no-save task archive # dry run (show what would be archived)
Archiving moves completed task files (and their subtask directories) to an archive/ subdirectory within each project. Archived tasks are excluded from normal queries but preserved in full. Use --list to see what's been archived.
scripts/sandpiper-tasks index update # rebuild index
scripts/sandpiper-tasks -f json task list -p SHR --top-level # JSON array
scripts/sandpiper-tasks -f toon task show SHR-1 # TOON format
scripts/sandpiper-tasks -f raw task show SHR-1 # raw markdown
scripts/sandpiper-tasks --no-save task create -p SHR -t "Preview" # dry run
Use the sandpiper-tasks CLI for all task operations rather than editing files directly. The CLI maintains the index (which powers fast queries and counter management), enforces validation rules (like requiring a resolution when completing), and keeps formatting consistent. Without the CLI, it's easy to create tasks with missing fields, duplicate numbers, or stale indexes.
Direct file editing is allowed — the index detects out-of-band changes — but save it for exceptional cases like bulk description edits or corruption recovery.
TASK and BUG are always top-level (directly under the project directory)SUBTASK is a child of a TASK or BUG (inside the parent's subtask directory)SUBTASK cannot have its own subtasks (max depth is one level)Tasks are never deleted — they're completed with a resolution. This preserves the audit trail so you can always understand what was planned, what was done, and what was abandoned.
DONE — work completed successfullyWONTFIX — task is no longer valid or will not be addressed--final flag)UNASSIGNED — no one is working on thisUSER — the user is working on thisAGENT — the agent is working on this--reporter USER--reporter AGENTtask pickup <key>task complete <key> (for review) or task complete <key> --final --resolution DONEEvery modification to a task (excluding creation) automatically appends an entry to the task's activity log — a structured footer at the end of the task file. Each entry records the timestamp and what changed:
---
# Activity Log
## 2026-03-21T09:00:00.000Z
- **status**: NOT STARTED → IN PROGRESS
- **assignee**: UNASSIGNED → AGENT
## 2026-03-21T10:00:00.000Z
- **description**: added (3 lines)
The activity log is maintained automatically by the CLI. No manual action is required.
Full content diffs for every task modification are stored in:
.sandpiper/tasks/history/<KEY>/<TIMESTAMP>.diff
These are standard unified diffs — readable by any diff tool and useful for auditing the full evolution of a task's content, including description changes that the activity log only summarizes.
# View a task's history
ls .sandpiper/tasks/history/SHR-1/
# Read a specific diff
cat .sandpiper/tasks/history/SHR-1/2026-03-21T09-00-00.000Z.diff
date -Iseconds for manual timestamp editsIf editing task files directly, use date -Iseconds for updated_at.
Reference task keys in commit messages: Refs: SHR-1, SHR-2
Pipe --format json to jq for scripting:
scripts/sandpiper-tasks -f json task list -s IN_PROGRESS | jq '.[].key'
task list uses -s/--status, while bulk mutating commands (like task complete / task update) use --filter-status.
# Querying
scripts/sandpiper-tasks task list -s NEEDS_REVIEW
# Bulk mutation
scripts/sandpiper-tasks task complete --final --resolution DONE --filter-status NEEDS_REVIEW
By default, task files are tracked inline on the current VCS branch alongside code.
Every task operation (create, update, pickup, complete) produces file changes that
appear in git status / jj st. For active projects this creates significant churn.
To keep task history separate from code history, configure a separate branch or external repo in a config file at the project root:
// .sandpiper-tasks.json
{ "version_control": { "mode": { "branch": "tasks" } } }
Key fields:
| Field | Default | What it does |
|---|---|---|
mode.branch | "@" | "@" = inline; any other value = branch name |
mode.repo | omit | External repo URL (cloned to .sandpiper/tasks/) |
auto_commit | false | Commit to task branch after every mutation |
auto_push | false | Push after every auto-commit |
Once configured, bootstrap with:
# Fresh project — initialise the separate workspace/worktree
scripts/sandpiper-tasks --dir /path/to/project storage init
# Existing project — migrate inline tasks onto the separate branch
scripts/sandpiper-tasks --dir /path/to/project storage migrate
# Sync with remote
scripts/sandpiper-tasks --dir /path/to/project storage sync # pull then push
scripts/sandpiper-tasks --dir /path/to/project storage push
scripts/sandpiper-tasks --dir /path/to/project storage pull
After storage init or storage migrate, commit both sides:
.gitignoreFor complete configuration options, backend selection rules (jj workspace vs git
worktree), repair guidance, and auto-commit semantics, read
references/storage.md.