From taskwarrior-plugin
Batch taskwarrior operations without the ID-renumbering and stdin foot-guns. Use when closing/triaging/modifying many tasks in one pass, bulk `task done` loops, or scripting task queries.
How this skill is triggered — by the user, by Claude, or both
Slash command
/taskwarrior-plugin:task-bulk-ops optional filter, e.g. project:foo +bulk or a status/tag filteroptional filter, e.g. project:foo +bulk or a status/tag filterThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Operate on **many** taskwarrior tasks in one pass — queue cleanup, a PR-merge
Operate on many taskwarrior tasks in one pass — queue cleanup, a PR-merge
sweep, a project wrap-up — without hitting the four foot-guns that make the
obvious for id in 1 2 3; do task $id done; done silently do the wrong thing.
Every foot-gun fails invisibly: the loop reports success, the count looks
right, and the wrong tasks (or no tasks) were touched.
| Use this skill when... | Use a sibling skill instead when... |
|---|---|
| Closing / annotating / modifying many tasks in one pass | Closing one task with a landing commit — use task-done |
Running a bulk task done loop over a queue | Retiring tasks whose GitHub issue/PR closed — use task-reconcile |
Scripting a query over the queue (export | jq, tag/project filters) | Reading a consolidated queue report — use task-status |
| Any state-changing op iterated over a set of tasks | Filing / claiming / releasing a single task — use task-add / task-claim / task-release |
task --versiontask status:pending countParse $ARGUMENTS as an optional taskwarrior filter that narrows the set to
operate on (e.g. project:foo, +bulk status:pending, +gh). When empty, ask
the user which set to target before mutating anything — never bulk-mutate an
unfiltered queue by default.
Execute bulk operations under these four rules. They are not optional style preferences — skipping any one silently corrupts the batch.
Taskwarrior numeric IDs are a display index over pending tasks: the moment
any task completes, every higher ID shifts down by one. A numeric ID is stale
across any gap between reading and acting — a loop iteration, a later turn, a
new session, or an unrelated task done in another session.
Resolve the immutable UUID at read time, then use only UUIDs for every later op:
# Loop over a set — capture UUIDs up front (they never shift)
UUIDS=$(task status:pending project:foo export | jq -r '.[] | .uuid')
for u in $UUIDS; do
task "$u" done </dev/null # (Rule 2: </dev/null)
done
# Acting later? Resolve the UUID the moment you know you'll act, not the number.
PROMOTE=$(task _get 169.uuid) # 169 is valid RIGHT NOW, at read time
# ...any number of turns / unrelated closes later...
task "$PROMOTE" done </dev/null # still the right task
If you only have a numeric ID and a gap has passed, re-derive it (re-run the
export/list, re-match on description) before acting — do not trust the
cached number. The same applies to annotate / modify / delete: any
state-changing op iterated over a set references UUIDs.
The +LATEST _get trap. _get is a DOM accessor taking an
<id>.<attribute> reference — given a filter (a tag like +LATEST) it
silently returns empty (exit 0) and captures nothing. To grab the UUID of the
task you just added, use the +LATEST-aware accessor or export | jq:
task +LATEST uuids # → UUID of the just-added task
task +LATEST export | jq -r '.[0].uuid' # always exit-0, parallel-safe
Do not reach for the
_getDOM accessor with a tag filter to grab that UUID —task +LATEST _get uuidsilently returns empty (exit 0)._getneeds an<id>.<attribute>reference:task _get <id>.uuidworks for a known numeric id, but the just-added task's UUID comes fromtask +LATEST uuids.
task done (and other mutating subcommands) read from stdin. Inside a shell
for loop the loop's input is stdin, so task done eats the remaining
iterations and the loop exits early after one or two — reporting "processed 15"
while closing 1. Two safe forms:
# Form A — redirect stdin per inner command
for u in $UUIDS; do
task "$u" annotate "swept in cleanup" </dev/null
task "$u" rc.confirmation=no done </dev/null
done
# Form B — xargs runs each command in its own subshell, no stdin link (preferred one-liner)
echo "$UUIDS" | xargs -I {} sh -c 'task rc.confirmation=no {} done'
Always pass rc.confirmation=no to batch task done — without it
taskwarrior may prompt "blocked by N other tasks, complete anyway?" and hang.
task config has the same confirmation hazard — and it's worse.
task config <name> <value> prompts to confirm by default; non-interactively
(a script, this Bash tool, a hook, chezmoi) the un-answered prompt makes it
exit 0 without writing the value. The failure is invisible. The sharpest
bite is declaring UDAs:
# WRONG — exits 0 but the UDA is NOT written; next `task add foo ghid:1417`
# appends the literal "ghid:1417" to the description instead of setting the UDA
task config uda.ghid.type numeric
# RIGHT — rc.confirmation=no makes the write happen; </dev/null guards stdin
task rc.confirmation=no config uda.ghid.type numeric </dev/null
task rc.confirmation=no config uda.ghid.label "GH Issue" </dev/null
Verify the write landed rather than trusting the exit code:
task _udas | grep -qx ghid && echo "declared" || echo "MISSING — config did not persist"
export | jq, and mind the filter caveatsPrefer task <filter> export | jq over task <filter> list: export emits
[] and exits 0 on an empty result (parallel-safe), while list exits 1 —
which silently cancels sibling calls in a parallel Bash batch.
Empty project: as the first filter errors. task project: status:pending list
fails with Unable to find report. Put the value-less filter later, or sidestep
the CLI with jq:
task status:pending export | jq -r '.[] | select(.project == null) | .uuid'
Escape + in test() when matching a tag-style marker that lives in the
description text (not a real tag):
task status:pending export | jq -r '.[] | select(.description | test("\\+upstream_issue")) | .uuid'
Real taskwarrior tags (set via +tag at creation, listed in .tags) filter
through the CLI: task +upstream_issue status:pending export. Use underscores or
camelCase in tag names, never hyphens — taskwarrior parses a hyphen mid-tag as an
exclude filter, so the tag silently never lands.
done, not afterOnce a task is completed its id becomes 0 and task <id> no longer addresses
it (only the UUID does). Add annotations before closing so the annotate/done
workflow stays uniform and the annotation lands on the still-pending task that
task <id> info shows.
for u in $UUIDS; do
task "$u" annotate "closed in PR-merge sweep" </dev/null # annotate FIRST
task "$u" rc.confirmation=no done </dev/null # then close
done
After the pass, re-query and report what actually changed — never trust the loop count. For a close sweep, confirm the pending count dropped by the expected amount and no stray tasks were touched:
task status:pending count
| Context | Command |
|---|---|
| Snapshot a set as UUIDs | task <filter> export | jq -r '.[] | .uuid' |
| Bulk close (xargs, no stdin trap) | echo "$UUIDS" | xargs -I {} sh -c 'task rc.confirmation=no {} done' |
| Bulk close (loop) | for u in $UUIDS; do task "$u" rc.confirmation=no done </dev/null; done |
| UUID of just-added task | task +LATEST uuids |
| UUID of a known numeric id | task _get <id>.uuid |
| Null-project tasks | task status:pending export | jq -r '.[] | select(.project == null) | .uuid' |
Description-text marker (escape +) | task status:pending export | jq -r '.[] | select(.description | test("\\+marker")) | .uuid' |
| Declare a UDA non-interactively | task rc.confirmation=no config uda.<name>.type <type> </dev/null |
| Verify a UDA landed | task _udas | grep -qx <name> |
| Foot-gun | Symptom | Fix |
|---|---|---|
Numeric IDs renumber after each done | Closes the wrong tasks after the first | Address by UUID; resolve at read time |
+LATEST _get uuid returns empty | Captures nothing (exit 0); next op no-ops/misfires | task +LATEST uuids |
task done consumes loop stdin | "Processed 15", only 1 closed | </dev/null per command, or xargs -I {} |
Missing rc.confirmation=no | Loop hangs on blocked-task prompts | Pass rc.confirmation=no to batch done |
task config prompts, exits 0 without writing | UDA silently not declared; value lands in description text | rc.confirmation=no ... </dev/null; verify with task _udas |
Empty project: first filter | Unable to find report | Reorder, or export | jq 'select(.project==null)' |
Annotating after done | id is 0; task <id> can't address it | Annotate before done |
/taskwarrior:task-done — close one task with a landing commit (single-task path)/taskwarrior:task-reconcile — close tasks whose linked GitHub issue/PR closed (bulk, but GitHub-driven)/taskwarrior:task-status — read-only consolidated queue report/taskwarrior:task-add — file a single linked task (the +LATEST uuids capture pattern originates here).claude/rules/parallel-safe-queries.md — the export | jq (exit-0-on-empty) idiomnpx claudepluginhub laurigates/claude-plugins --plugin taskwarrior-pluginClose taskwarrior tasks whose linked GitHub issue/PR closed or merged. Use when stale trackers pile up, after a PR-merge sweep, or auditing queue drift.
Provides CRUD operations for Todoist tasks, projects, sections, labels, and comments via the td CLI. Requires confirmation before destructive actions. Activate when user wants to read, create, update, or delete Todoist data.
Executes eligible tasks from session task list, syncs against codebase/PR state to surface stales, and generates handovers. Use /task-run [--all] [--sync [--dry-run]] [--handover [query]].