qqq
Local task queue for coding agents. Task metadata lives in each project's
.qqq/qqq.db; image bytes live in .qqq/images/. No server needed.
Install
Requires Rust 1.85+ and C compiler. Install latest published crate:
Command name: qqq; crate name: qqq-cli. Published releases can lag this
README's development features. Install current source from repo root for latest
behavior:
Quick start
Run init in project root. Task commands use nearest .qqq directory in
current directory or parents; missing qqq.db there causes error. Config
commands need no DB.
next claims highest-priority ready task, oldest ID on ties. complete frees
its dependent tasks. Use IDs returned by add and next; examples above assume
fresh DB. See qqq --help or qqq <command> --help for all flags.
List and show
list shows status, priority, and dependency tree; roots and siblings follow
creation ID order. In a terminal with known width, it shows full descriptions
with wrapped, aligned continuation lines; piped output keeps first-line previews.
show adds messages, images, ownership history and Herdr link. Negative indexes
work with show, edit, archive, unarchive, and reopen; -1 selects newest task.
--oneline limits terminal and watch views to each description's first line.
JSON and search still use full descriptions.
--max-completed N keeps N most recent completions plus unfinished tasks;
0 hides completed tasks. Hidden parents display children as roots. --all
bypasses configured limit; it conflicts with --max-completed.
--query TEXT matches any part of a full description, including later lines,
using Unicode lowercase comparison. Repeat --status to match any listed
status: new, in_progress, completed, or error. Query and statuses combine
with AND. Search runs after completed-display limit; use --all to search all
completed tasks. Visible ancestors of matches appear as [context] rows even
when they do not match filters. JSON stays flat and marks those ancestors with
context_only: true; direct matches keep existing task fields. Empty filtered
results return [] in JSON or No matching tasks. in human output.
--watch prints initial list, then refreshes after DB commits. Terminal output
redraws; pipes, TERM=dumb and JSON append snapshots. Ctrl-C stops watching.
Default list, watch, TUI, and claim queue hide archived tasks. Use
list --include-archived, list --watch --include-archived, or
tui --include-archived to browse them. Human lists label archived rows
[archived]; JSON includes archived: true without changing description.
--all changes completed-task limit only; hidden archived completions do not
consume that limit.
Global --json works before or after commands. JSON lists stay flat, preserve
full descriptions and parent_id; watch prints one array per line. Empty lists
return []; no ready task returns null. Errors use stderr (exit 1 for runtime,
2 for arguments). NO_COLOR=1 or TERM=dumb disables color.
Add and edit
Descriptions preserve whitespace and newlines; blank-only text fails.
Priority defaults to 0 and accepts integers from -100 through 100.
Higher priority claims first among ready new tasks; equal priority uses oldest
ID. list stays in ID/dependency order. Editing priority on active, completed,
or error tasks keeps status and ownership metadata; an already-owned task still
returns to its owner before new claims.
Archive and unarchive
Archive keeps task status, description, priority, messages, images, ownership
history, and dependencies. show, edit, messages, and image export still work
by ID. Archive/unarchive add history events; repeated commands change nothing.
Events record supplied --session or cli when absent. In-progress tasks
cannot be archived. An unfinished parent cannot be archived while a visible
unfinished child depends on it; adding or reparenting an unfinished child, or
unarchiving one, under an archived unfinished parent also fails. Archived
completed parents still release dependent tasks.
Reopen completed work
reopen returns a completed task to new and records a reopen event. It keeps
description, priority, parent, messages, images, creation time, and prior
history. A task in any other status fails without changes; repeating reopen
also fails. Archived completed tasks require unarchive first. A completed
child under an archived unfinished parent cannot reopen until parent is
unarchived or completed. Completed descendants stay completed; new direct children
wait for reopened parent to complete again. Reopened tasks return to default
lists, including --max-completed 0, and become claimable when dependencies
permit.
Built-in editor
On a terminal, add without text or edit without update flags opens built-in
editor. Ctrl-S saves, Esc exits blank draft or confirms discard of nonempty
draft, Ctrl-C exits, Ctrl-W deletes previous word on same line, Ctrl-V pastes
clipboard text or image. Option+Left/Right moves cursor by word. Clipboard paste
needs desktop clipboard support.
Built-in editors collapse pastes over 1,000 characters into one gold
[Pasted Content N chars] item. Saving stores full payload in Markdown:
```pasteboard
pasted text
```
Shift-Up/Down and qqq edit restore complete fences as one editable item;
Backspace or Delete removes entire item. Payload containing backticks gets a
longer fence. NO_COLOR=1 and TERM=dumb keep label plain. Existing task text
outside pasteboard fences remains unchanged on save.
In built-in qqq add, Ctrl-S creates or updates task, clears editor, then waits
for next task. Saves commit immediately; exit keeps prior saves. Shift+Up loads
newest task, then older tasks; Shift+Down moves toward newer tasks, then blank
draft. Header shows selected ID and status. JSON output returns saved tasks as
array on exit. Exit without any save returns error.
Switching from changed draft asks before discard; N, Enter, or Esc keeps it.
Navigation includes all unarchived tasks, regardless of status; deleted IDs are
skipped. --parent and --priority apply to each new task. --image files attach
only to first successful save, including when updating existing task. Inline add,
external editor and qqq edit save once.
Use --edit (-e) to force $EDITOR. Without terminal input and stderr,
interactive add/edit uses $EDITOR automatically. Editor command must wait
until editing finishes.
Built-in add/edit editor uses stderr; stdout holds result on exit. Cancelling draft keeps earlier saves. Direct edits preserve omitted fields, ownership and attachments.
Task TUI
Upper panel lists unarchived tasks by default; lower panel edits current task.
Shift-Up/Down selects tasks in displayed tree order, then blank draft. Ctrl-S
updates selected task or creates new task, keeping saved task open for further
edits or task actions. Shift-Down past last displayed task opens blank draft. Esc/Ctrl-C in
selected task returns to blank draft; Esc asks before discarding unsaved edits.
Press again in blank draft to exit. Esc/Ctrl-C in dirty new draft asks before
discard; y exits without saving, n keeps draft for editing or Ctrl-S save.
Repeated Ctrl-C keeps confirmation open. Ctrl-V or terminal paste inserts text; pasting
image file path attaches image. Saves commit immediately. TUI needs terminal
and writes no stdout, including with --json.
Task list refreshes after external DB commits without keyboard input. Refresh keeps current editor draft, filter query, and manual scroll positions. Selected task status updates in editor title; task text stays in editor until reloaded or saved.
Mouse wheel scrolls task list or editor under pointer. Both panes keep separate scroll positions; scrolling editor never edits or saves text. Shift-Up/Down returns list to selected task. Editor keys reveal caret after manual scroll. Left-click task row to load it in editor, including indented or wrapped rows. Left-click editor text to place caret; dirty drafts ask before switching tasks.
Filter bar stays visible above task list. Ctrl+/ focuses it; type to match any
part of full description, ignoring case. Matching tasks retain parent chain;
no matches shows No matching tasks.. Backspace edits query. Esc clears
nonempty query, then exits filter focus on next press. Tab or Enter returns to
editor. / or Alt+/ inserts literal / into editor. Shift-Up/Down navigates visible
tasks while filter is active. Query changes do not change selected task, unsaved
draft, or DB.
Ctrl-G opens actions for selected task. Press c to complete owned task,
r to retry error task, o to reopen completed task, a to archive or
unarchive, p to set priority (-100..100), or d to set parent by positive
task ID or none. Esc closes menu or prompt. State changes ask for y before
running; priority and parent changes ask when draft has unsaved edits. Successful
action refreshes task and list; rejected action keeps draft and shows DB error.
Error view wraps long messages; Up/Down scrolls, Esc closes it.
Dependencies and images
Each task has at most one parent. Child becomes ready when parent completes. Parent must exist; self-parenting and cycles fail. Parent changes affect future claims without removing active ownership.
Attach files through add or edit; repeat --image for multiple files:
Image IDs come from show. Export needs selected task's image ID and new output
path. PNG, JPEG, GIF and WebP signatures supported, up to 20 MiB each; signature
check does not fully validate file. Bytes copy into
.qqq/images/<task_id>/<image_id>.<ext>. Built-in editor also accepts pasted
image paths or clipboard images. Pasted images become Markdown links in the
description, such as . Reloading a task
shows each linked image as one editable item; deleting that item removes its
description link while keeping the attachment available through show.
Description, status, parent and image updates save atomically.
Agents and recovery
Use stable, unique session ID per worker:
--session overrides QQQ_SESSION. Each owner gets one active task; repeated
next returns it. Claims never expire. --wait blocks until work is ready;
without it, empty queue prints No ready tasks. and exits successfully. Error
tasks and blocked children stay out of queue.
Without --session or QQQ_SESSION, owner discovery uses:
- Exact Herdr pane from
HERDR_PANE_IDorHERDR_ENV=1(requires pane ID). - Native Codex
CODEX_THREAD_ID, thenCODEX_SESSION_IDfallback. - Unique Herdr agent at project root (
foreground_cwdbeforecwd).
Explicit session IDs and native Codex ownership work without Herdr when dispatch is
disabled or next --local is used. Exact Herdr context takes precedence.
next --local skips dispatch, still resolves owner.
Completion, release and error marking require owner. Use original session token
or uniquely matching displayed harness_session; add --harness-name when
public sessions overlap. JSON assignment fields: harness_name,
harness_session, orchestrator_name, orchestrator_session.
Return or retry work
Return active task owned by worker-1:
Alternatively, mark active task failed, then retry:
--set-pending equals --set-status new. Active tasks require owner; error
tasks can be retried without one. Error marking requires nonblank reason,
clears assignment, records reason as message. Status updates skip editor.
For abandoned active work, inspect task before forcing return:
--force skips ownership matching and Herdr discovery. It requires literal
--set-status new; cannot combine with --set-pending or --edit. Return/retry
preserves content, dependencies, messages, images and Herdr link.
Session IDs coordinate local agents; they do not authenticate users. Any local caller can edit task content or append messages regardless of ownership.
Config and aliases
Config: ~/.config/qqq/config.toml (under HOME).
Keys use TOML dotted syntax; quote literal dots: 'alias."with.dot"'.
Alias values are strings; herdr.next-to-new-agent is boolean;
display.max-completed is non-negative integer. Other values accept TOML
literals, falling back to strings.
Reads do not create files. --list shows stored values; --get can inspect
invalid settings. Writes validate known settings, preserve comments and unknown
keys. --json returns typed values; unset restores default.
Completion limit config affects human lists only; explicit --max-completed
affects text and JSON. Missing limit means unlimited.
Example config:
[]
= 10
[]
= "list"
= "next"
= "complete"
= "add"
qqq bug "Fix login" expands to qqq add "Fix login". Aliases support quoting
and chains; built-in commands take precedence. No shell expansion. Cycles,
invalid quoting and ! aliases fail.
Herdr
Optional Herdr integration links tasks to live agent panes:
Discovery selects exact caller pane or unique agent at project root. Ambiguous identity fails before claim. Without agent-session hooks, terminal ID plus agent kind supplies identity. Active claims retain identity as hooks change.
link stores association without changing owner; find searches live panes for
saved link identity. Both require Herdr CLI and server. Local explicit --session
claims do not auto-link; new-agent dispatch links spawned agent even when caller
uses explicit session.
To dispatch ready work into new Codex agent tab:
Dispatch needs HERDR_ENV=1 and HERDR_WORKSPACE_ID. It reuses caller's active
task; otherwise reserves ready task, opens sibling tab, starts agent and submits
prompt. Empty or blocked queues create no tab. next --local skips dispatch.
Default: disabled.
Startup failure releases dispatch reservation; created tabs stay open. Prompt errors keep claim and link because delivery may have happened. Inspect agent before retry or forced release.
Data
Keep .qqq/ out of Git. Create portable snapshot while other local qqq
writers run:
Backup contains consistent SQLite data plus every stored image. Tar archive
starts with manifest.json, then qqq.db, then images/<task-id>/<image-id>.<ext>.
Manifest records SHA-256 hashes and sizes; restore checks those, image rows,
SQLite integrity, and foreign keys before installing .qqq. Backup refuses to
overwrite destination. Restore works only from new project directory or one
with empty .qqq; existing data and nested projects are rejected. Invalid
archives leave project unchanged. Relative snapshot paths resolve from current
directory.
Preview permanent deletion before confirming it:
Only archived tasks without children can be deleted. Use positive task ID;
relative creation indexes can change before confirmation. Preview lists messages,
events, Herdr link, stored images, and paths without changing project data;
--json returns same counts with deleted:false. --yes removes task and
dependent data permanently, including stored image files, then returns
deleted:true. Release or complete an active task before archiving it; delete
or reparent every child before deleting parent. IDs remain reserved after
deletion. Preview refuses legacy DB versions and pending recovery without
changing files; run qqq list to migrate or recover before previewing again.
If deletion stops while images are staged, next DB-backed qqq command recovers
them according to committed DB state. Keep backup until recovered project
passes qqq doctor.
Check project health without changing DB or attachments:
Doctor checks SQLite integrity, foreign keys, schema version, image paths, byte counts,
signatures, and orphan files/directories. Healthy project exits 0. Issues print recovery
actions and exit 1; JSON includes ok, counts, and issues with code, path,
message, and action. Doctor never migrates DB. If SQLite journal/WAL sidecars
exist, stop writers and recover or checkpoint SQLite before rerunning doctor.
For damaged DB or images, restore verified snapshot into new directory first;
inspect recovered data before replacing damaged project files. Keep damaged
copy until recovery is verified. For DELETE_RECOVERY_PENDING, follow returned
action: fix unsafe staging paths or access errors first. Valid interrupted
deletions recover through normal DB-backed command, such as qqq list;
rerun doctor afterward.
To move old root-level qqq.db, stop DB writers. Before qqq init, check that
.qqq/qqq.db does not exist. From project root:
Check task data before removing old DB. New CLI does not discover root-level
qqq.db; qqq init without migration creates separate empty DB.
Compatible DBs at schema versions 1–8 migrate to version 9. Version 6 moves
existing image blobs to .qqq/images/ before SQLite drops its data column.
Migration tries VACUUM to reclaim old blob pages. If compaction warns, stop
writers and run sqlite3 .qqq/qqq.db 'VACUUM;' later. Upgrade other qqq workers
before migration; older binaries cannot open version 9. Legacy title or
pending schemas need manual conversion; newer unknown schemas fail. Back up
before conversion.
Development
Tests require Python 3 for real-terminal coverage on macOS and Linux.
CI checks Ubuntu 24.04 and macOS 14, builds release
binary, uploads qqq-<OS>-<ARCH> archives (14-day retention).
Publish to crates.io
Publish workflow checks v<version> tag against
Cargo.toml, runs checks and package dry run, then publishes qqq-cli. Setup:
- Sign in to crates.io, verify email, create token allowed to publish
qqq-cli. - Add GitHub Actions repo secret
CARGO_REGISTRY_TOKEN. - Choose unused version, update
Cargo.tomlandCargo.lock, commit changes. - Push commit and matching tag. Example for version
0.2.0:
For manual validation, run Publish to crates.io with existing tag and
default dry_run=true. Set dry_run=false to publish new version. Published
versions cannot be overwritten.
License: MIT.