What tasqx is
tasqx is the organiser for your AI: a backlog, a memory and a brief for your coding agent, and a fast, terminal-first task manager for you. It is a headless Rust core engine that exposes one stable, versioned JSON API — and every surface you touch (the CLI, the MCP server, the HTML report, this guide) is a client of that one contract.
The five ideas that shape everything else
| Principle | What it means for you |
|---|---|
| Fast | One-shot commands open SQLite, do the work, and exit. No daemon required, no warmup. |
| Local-first | Your data is one SQLite file on your disk. No account, no cloud, works offline forever. |
| One API | The CLI does nothing you cannot do over JSON. Every verb below names the method it calls. |
| AI-native | The same typed API drives a bundled MCP server, so an agent is a first-class user. |
| Honest | Every command speaks human text and --json. Exit codes are contract, not decoration. |
How the pieces fit
The core is a plain Rust library. The CLI links it and calls functions in-process — no IPC on the hot path. The JSON API is a thin envelope over that same dispatch layer, so "call a function" and "send a JSON command" run identical code. There is exactly one dispatch table.
Every mutation is logged, in the same transaction
Adding, modifying, completing — each writes its row to an append-only events
table inside the same SQLite transaction as the change itself. The log cannot
drift from the data, because there is no window in which one exists without the
other. That is what makes charts, reminder dedupe,
and sync possible without a migration.
Where your data lives
| What | Where |
|---|---|
| Store | $TASQX_DB if set, else the platform data dir: %APPDATA%\tasqx\tasqx\data\tasks.db on Windows (the doubled segment is what the directories crate produces from organization + application), ~/.local/share/tasqx/tasks.db on Linux, ~/Library/Application Support/dev.tasqx.tasqx/tasks.db on macOS |
| Config | $TASQX_CONFIG_DIR/config.toml, else the platform config dir |
| Themes | $TASQX_CONFIG_DIR/themes/*.toml |
| Socket | $TASQX_SOCK, else a platform default (see Daemon) |
Point $TASQX_DB at a scratch file to try anything in this guide
without touching your real store. Every example below was run exactly that
way.
Settings
tasqx config list shows every setting with the layer that supplied it.
Resolution order is the CLI flag, then the TASQX_* environment variable, then
config.toml, then the built-in default.
| Setting | Home | Default | What it does |
|---|---|---|---|
theme.name | config.toml | nord | Terminal theme: a built-in (nord, gruvbox, dracula, solarized, mono) or a user file. |
dashboard.enabled | config.toml | true | Open the dashboard when tasqx is run bare on a terminal (D58). |
dashboard.panels | config.toml | tasks,projects,burndown,pulse,effort,tokens | Panels the dashboard draws; membership is visibility, position is focus and tab order (D58). |
dashboard.refresh | config.toml | auto | Whether the dashboard re-reads on a timer, or only on r (D58). |
dashboard.window | config.toml | week | The burndown window the dashboard opens on; w cycles it live (D58). |
notify.enabled | config.toml | false | Allow the daemon to raise native OS notifications for reminders. |
tokens.enabled | config.toml | false | Allow the daemon to attribute AI token usage by parsing local tool transcripts (#17). |
otlp.enabled | config.toml | false | Run a local OTLP/HTTP receiver in the daemon to ingest token telemetry from AI tools (#18). |
otlp.port | config.toml | 4318 | TCP port the OTLP receiver binds on 127.0.0.1 (OTLP/HTTP convention: 4318). |
board.port | config.toml | 0 | TCP port tasqx board binds on 127.0.0.1; unset (0) takes a free port each run. A fixed port also keeps the board's token between runs, so a bookmark survives. |
daemon.idle_timeout | config.toml | 0 | Minutes with no clients and no work after which tasqx daemon exits by itself; 0 stays up until Ctrl-C (D5). |
default_project | the store | — | Project a bare tasqx add files into. Lives in the store; set it with tasqx use. |
mcp.add_status | config.toml | pending | Status a task added over MCP takes when the call names none: triage holds agent proposals out of @working until tasqx accept (D238). |
detail.time_format | config.toml | both | How the task-detail view (show, MCP) writes timestamps and durations. |
tasqx config edit opens the same settings on a full-screen editor: up and down
move, enter toggles a switch or opens a theme picker, escape leaves. Moving
through the theme list repaints the screen in that theme before anything is
written, which is the one thing editing config.toml by hand cannot do.
default_project is shown there but not editable — it lives in the store and is
set with tasqx use. Piped or redirected, config edit refuses and exits 2
instead of writing escape codes into your pipe; scripts should use config set.
A Sync section sits below the settings: c connects a tasqx-remote-*
connector found on PATH through a form (the connector's own fields, then the
sync passphrase, twice), s runs tasqx sync, and d disconnects after
confirming — the remote itself is untouched either way.
Install and quickstart
tasqx is a single static binary with no runtime and no dynamic linking. Install it with a package manager, an installer script, or build it from source.
With a package manager
Updates then come from brew upgrade tasqx / scoop update tasqx, and brew
switches Tab completion on by itself. macOS and x86-64 Linux, with Homebrew
(there is no prebuilt ARM Linux binary yet, so ARM Linux builds from source):
brew install dimitritholen/tasqx/tasqxWindows, with Scoop:
scoop bucket add tasqx https://github.com/dimitritholen/scoop-tasqx
scoop install tasqxWith the installer script
macOS and x86-64 Linux:
curl -fsSL https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.sh | shWindows (the first statement makes older PowerShell able to download at all):
[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; irm https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.ps1 | iexPrebuilt archives for Linux, macOS and Windows are also on the project's GitHub Releases page.
Or build from source
git clone https://github.com/dimitritholen/tasqx.git
cd tasqx
cargo install --path crates/tasqx-cli --forceRequires Rust 1.95 or newer. On Windows the MSVC toolchain is discovered automatically — you do not need it on your PATH.
That installs tasqx alone. To sync between machines,
also build the connector you use:
# a synced folder
cargo install --path crates/tasqx-remote-dir --force
# or an R2 bucket
cargo install --path crates/tasqx-remote-r2 --forceSixty seconds with tasqx
Create a project, capture some work, and look at it.
tasqx init work.tasqx --desc "The tasqx project itself"work.tasqx
created now your default projectNow capture a task. Everything after add is the title — except the bits tasqx
recognises as structure. That is the inline sugar: +tag, project:,
!priority, due:, est:, repeat:, remind:. Whatever is left over is the
title.
tasqx add "Ship the v1 JSON API freeze +api +release project:work.tasqx !high due:friday est:4h"▌ #1 Ship the v1 JSON API freeze
▌ added H ▄▄▄▄ 15.7 work.tasqx due Fri +api +release est 4hThe sugar was consumed; only the real title remains. Tags and a due date, no
project — init made work.tasqx the default, so it is filled in for you:
tasqx add "Write the user guide +docs due:friday"▌ #2 Write the user guide
▌ added - ▄▄▄▁ 9.7 work.tasqx due Fri +docsEvery field is also available as a flag, which is what you want when a value contains spaces or comes from a variable:
tasqx add "Renew the TLS cert" project:work.tasqx +ops --due -1d▌ #3 Renew the TLS cert
▌ added - ▄▄▄▄ 12.0 work.tasqx due yesterday +opsFlags go outside the quoted title. tasqx add "Renew the cert --due -1d" puts the literal text --due -1d in your title — the shell
handed tasqx one argument, and tasqx believed it. Quote the title, leave the
flags bare.
Look at the working set
tasqx list shows the @working filter: everything pending or active that is
not blocked, hottest first. A bare tasqx prints the same table wherever it is
not talking to a person — piped, redirected, or under --json — and opens the
dashboard when it is.
tasqx list@working 3 tasks · 1 overdue
ID URG TASK PROJECT DUE TAGS
1 H ▄▄▄▄ 15.7 Ship the v1 JSON API freeze work.tasqx Fri +api +release
3 - ▄▄▄▄ 12.0 Renew the TLS cert work.tasqx yesterday +ops
2 - ▄▄▄▁ 9.7 Write the user guide work.tasqx Fri +docsThe line above the table says which filter answered and what the answer holds
beyond its own size — how much of it is late, due before the day is out,
running, or blocked. In the DUE cells a deadline inside the coming week is named by its weekday,
one further out by its date, and today and tomorrow carry a clock when the
store holds one.
That URG column is urgency — a computed score, not something you set. The
letter in front of it is the priority you did set. tasqx next is the "what
now" button: the single hottest unblocked task.
tasqx nextnext #1 Ship the v1 JSON API freeze
H ▄▄▄▄ 15.7 work.tasqx due Fri +api +release
tasqx start 1 · tasqx why 1And tasqx why shows its arithmetic, so the ranking is never a mystery:
tasqx why 1#1 Ship the v1 JSON API freeze
priority H 6.0
deadline due Fri 9.7
age created today 0.0
urgency 15.7Work it, finish it
tasqx start 1
tasqx stop 1
tasqx done 1▌ #1 Ship the v1 JSON API freeze
▶ started H ▄▄▄▄ 15.7 work.tasqx due Fri +api +release est 4h
▌ #1 Ship the v1 JSON API freeze
▌ stopped H ▄▄▄▄ 15.7 work.tasqx due Fri +api +release est 4h
▌ #1 Ship the v1 JSON API freeze
▌ done today work.tasqx due Fri +api +release est 4hWhere to go next
| If you want to… | Read |
|---|---|
| Know every verb and flag | Commands |
| Ask precise questions of your store | Filter grammar |
| Say "friday" or "every 3 days" | Dates & recurrence |
| Be told about a task before it is late | Reminders |
| Give an AI agent access | MCP |
| Script tasqx from another language | JSON API |
The filter grammar
One small grammar, used everywhere a query is taken: list, report, export,
watch, task.list, and the MCP tools. Learn it once.
The grammar
filter := or_expr
or_expr := and_expr ( "or" and_expr )*
and_expr := term ( "and"? term )* # juxtaposition = implicit AND
term := "(" or_expr ")" | predicate
predicate := "+" VALUE # require tag; VALUE not empty, lowercased (D172)
| "-" VALUE # exclude tag; VALUE not empty, not starting with a dash, lowercased
| "@working" # status in {pending,active} AND not blocked; a chooser (next, pick, a list with an actor) also skips other holders' live claims (D236)
| "@blocked" | "+blocked" | "status:blocked" # the blocked flag
| "@claimed" # under a live claim, anyone's (D236)
| "project:" VALUE
| "proj:" VALUE # alias of project: (#229 item 5)
| "status:" VALUE # `any` or `all` = every status, done and cancelled too (D210)
| "priority:" VALUE
| "title:" VALUE # substring of the title, case-insensitive (D210)
| WORD | QUOTED # a bare word or quoted phrase is "title:" of itself
| "due.before:" DATE
| "due.after:" DATE
| "completed.before:" DATE # when the task was finished
| "completed.after:" DATE
VALUE := CHUNK* # chunks abut; nothing may come between them
CHUNK := WORD | QUOTED
WORD := a run of characters, none of them whitespace, a quote or a paren
QUOTED := a run between double quotes; backslash escapes a quote or a backslash
DATE := any date `due:` accepts # tomorrow, friday, 2026-07-25,
# "in 3 days", eom, 2026-07-20T17:00Predicates
| Predicate | Matches |
|---|---|
+api | Tasks tagged api. |
-api | Tasks not tagged api. |
project:work.tasqx | Exact project match. |
proj:work | The same as project:. |
priority:high | Exact priority: H, M, L, or high, medium, low. |
status:pending | Exact status: triage, backlog, pending, active, done, cancelled. backlog is the date-held backlog; a proposal in triage is never in it. |
status:any | Every status, done and cancelled included; also spelled status:all. Overrides a default that hides closed tasks. |
weekly review | Free text: a bare word is a case-insensitive substring of the title, and several words must all appear. Quote a phrase to match it as one — "memory explorer". title:review and title:"weekly planning" are the explicit spelling; % and _ are ordinary characters. |
@working | Status pending or active, and not blocked. The default filter. |
@blocked | Open, with at least one dependency that is not yet done or cancelled. Also spelled +blocked or status:blocked. |
due.before:<date> | Due strictly before that instant. Takes any date due: takes — tomorrow, friday, 2026-07-25, eom, "in 3 days", or a full RFC3339 instant. |
due.after:<date> | Due strictly after that instant. Same date grammar. |
completed.before:<date> | Finished strictly before that instant. Same date grammar; pair with status:done. |
completed.after:<date> | Finished strictly after that instant. Same date grammar. |
@claimed | Under a live claim, whoever holds it. |
Values with spaces
A space separates predicates, so a project whose name contains one must be
double-quoted: project:"Home Renovation". A tag never contains one — tags are
stored lowercased and a spaced tag is refused, so +API matches api.
- Inside quotes, spaces and parentheses are ordinary characters and
and/orare ordinary words, so a project nameda (b)does not break the grouping. - Write
\"for a literal quote and\\for a literal backslash. - Quoting changes where a predicate ends, not what it means:
"project:x"is still a project match. - The quotes must reach tasqx, so protect them from your shell:
'project:"Home Renovation"'.
tasqx add and tasqx modify split their inline sugar with the same scanner,
but the two sides differ on purpose. The write side also honours the argument
boundary your shell drew, so tasqx add "paint" project:Home Renovation files
the task. The read side refuses the same words: project:Home Renovation could
equally be a project match plus a stray token, and guessing would answer with
the wrong rows at exit 0. A refused read costs a retype; a wrong one cannot be
noticed. The quoted spelling works on both sides, so learn that one. A name
containing a quote needs the escaped form on both sides:
project:"My \"Big\" Project".
tasqx list project:"Home Renovation" +paintCombining
A space is an implicit and. or and parentheses are explicit, and both
keywords are case-insensitive.
tasqx list "(+api or +ops) and status:pending"(+api or +ops) and status:pending 2 tasks · 1 overdue
ID URG TASK PROJECT DUE TAGS
1 H ▄▄▄▄ 15.7 Ship the v1 JSON API freeze work.tasqx Fri +api +release
3 - ▄▄▄▄ 12.0 Renew the TLS cert work.tasqx yesterday +opsTwo behaviours worth knowing
Dates compare as instants, not strings. due.before: and due.after: parse
both sides to timestamps and compare those. 2026-07-17T00:00:00Z and
2026-07-17T02:00:00+02:00 are the same instant, and the filter knows it — a
lexicographic comparison would not. A relative bound resolves once, when the
filter is parsed, so every row in one query is compared against the same
tomorrow. A date the parser cannot read is refused by name rather than quietly
matching nothing.
Unknown tokens are rejected. A token the grammar does not
recognise is an error naming the token, not a term that matches everything.
So tasqx list "colour:red" fails — colour: is not a predicate —
instead of silently listing every task. The same goes for a dangling or or
an unclosed (. Values follow the same rule when the set of them is
closed: status: and a date bound have a fixed list of accepted values,
so status:pendign is an error naming the value and the six statuses, not
an empty table. A project: or a tag is different on purpose — those names
are made at runtime, so an unknown one simply matches no row.
Where the grammar stops
On purpose, and permanently: no arithmetic, no computed expressions, no
subqueries. A filter language that grows those becomes a query language nobody
can predict. For anything beyond this, export to JSON and use a real
tool — jq, a script, whatever you like. The store is yours.
Getting Started
tasqx is the organiser for your AI: a backlog, a memory and a brief for your coding agent, and a task manager that lives in your terminal. Your tasks are stored in a single SQLite file on your own disk — no account, no cloud, works offline.
Install
With a package manager — updates then come from brew upgrade tasqx /
scoop update tasqx, and brew switches Tab completion on by itself.
macOS and Linux, with Homebrew:
brew install dimitritholen/tasqx/tasqxWindows, with Scoop:
scoop bucket add tasqx https://github.com/dimitritholen/scoop-tasqx
scoop install tasqxOr without one — Linux and macOS:
curl -fsSL https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.sh | shWindows (the first statement makes older PowerShell able to download at all):
[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; irm https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.ps1 | iexPrebuilt binaries for Linux, macOS and Windows are also on the
Releases page (https://github.com/dimitritholen/tasqx/releases), and you can
build from source with cargo install --path crates/tasqx-cli.
Each release archive, the Homebrew formula and the Scoop manifest also carry
tasqx-remote-dir and tasqx-remote-r2, the connectors Syncing Between
Machines uses. brew install and scoop install
put both on PATH for you; the curl/irm installers above only place tasqx
itself, so a connector from the archive needs copying onto PATH by hand, or
building it from source:
cargo install --path crates/tasqx-remote-dir and
cargo install --path crates/tasqx-remote-r2.
Install fine print
Both installer scripts pick the newest release, resolve your target triple,
verify the archive against its published checksum, and put files only in the
install directory: ~/.local/bin, or %LOCALAPPDATA%\Programs\tasqx\bin on
Windows. The Windows installer also adds that one directory to your user PATH,
a setting outside it, and -Uninstall takes it back out. Neither touches a
shell startup file unless you ask.
A pipe passes no arguments, so flags need the longer form:
curl -fsSL https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.sh | sh -s -- --dry-run&([scriptblock]::Create((irm https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.ps1))) -DryRunThe rest are --uninstall/-Uninstall (which also takes out the completion
block an older tasqx added to your shell's startup file) and --help/-Help; every switch also has an environment variable
(TASQX_UNINSTALL, TASQX_DRY_RUN, …), TASQX_VERSION pins a tag, and
TASQX_INSTALL moves the destination.
Honesty about what that buys you:
- The checksum is integrity, not provenance. It catches a truncated transfer or a corrupt CDN object; it is served from the same host as the archive, so it proves nothing about who built it. Nothing here is signed.
- The binaries are unsigned. On macOS the curl route goes around Gatekeeper rather than passing it — that is why the install just works.
- The Linux build links your system glibc (SQLite is bundled; there is no
other runtime). The floor is whatever GitHub's current
ubuntu-latestprovides. There is no musl build — an older distro builds from source.
All of it applies to the package-manager routes too: the Homebrew formula and the Scoop manifest are generated per release from the same published checksums and point at the same unsigned archives.
The whole loop is four commands
tasqx init work: create a project — just a name, no foldertasqx add Buy milk: capture a task (lands in the default project)tasqx next: the one thing to do nowtasqx done 1: complete it
tasqx init work
tasqx add Buy milk
tasqx next
tasqx done 1That's a working task manager. Everything else on this wiki is optional depth.
A few things worth knowing on day one:
- A project is just a name.
initcreates no folder and touches nothing on disk except the task store. The first project you create becomes the default, so baretasqx addknows where to file things. - Every task gets a short id (
#1,#42). That id is how you refer to it in every other command:tasqx done 42,tasqx show 42. - Capture takes shortcuts inline.
tasqx add Ship it due:friday +api !highsets a due date, a tag and a priority in one line. See Adding and Editing Tasks. - A bare
tasqxin a terminal opens the dashboard — a full-screen overview. In a script or pipe it prints the task table instead. See Dashboard and Live View.
Getting help
tasqx ships its own documentation — no internet needed.
tasqx manual
Alias: man
The complete guide, in your terminal. tasqx manual shows the table of
contents; tasqx manual <command> or tasqx manual <topic> opens one section.
projects and daemon are both a command and a topic, and open both pages.
| Command | What it does |
|---|---|
tasqx manual | Table of contents |
tasqx manual add | Everything about add |
tasqx manual filters | The filter language |
tasqx docs
The same guide as one self-contained HTML page, opened in your browser. No external requests, nothing tracked — it's generated from the binary itself.
| Command | What it does |
|---|---|
tasqx docs | Open in the browser |
tasqx docs --out guide.html | Write the file instead |
tasqx about
Who made tasqx, where to find it, and what build you are on. Five labelled rows: the author, his LinkedIn, the project on GitHub, the build this binary was made from, and the store it would open. It opens no store and no network — the store line is the path a command would use, resolved without creating anything.
tasqx aboutThe build line is the same string tasqx --version prints: the crate version
plus the commit it was built from, or unknown for a build from a source
tarball, which has no git to ask. Quote it when you report a bug.
tasqx about --notices prints the third-party notices: the attribution and
licence texts of the embedding model built into the binary for memory search.
Per-command help
Every command answers -h with usage, flags and copy-pasteable examples:
tasqx add -hWhere your data lives
The store is a SQLite file at $TASQX_DB if you set it, otherwise in your
platform's data directory. tasqx config store tells you exactly which file a
command would write to. Back it up any time with
tasqx export.
Projects
A project in tasqx is just a name that groups tasks — work, home,
kitchen-remodel. No folder is created, nothing appears on your disk. A task
belongs to at most one project: tasqx add with no default project files it
under none, and tasqx modify --clear project takes it out of one.
tasqx init
Create a project.
tasqx init work
tasqx init home --desc "Everything around the house"- If the store has no default project yet, the new one claims it. So on a fresh
install,
tasqx init workis all the setup you need. - Creating a second project does not move the default. Use
tasqx usefor that.
tasqx use
Choose the default project — the one a bare tasqx add files tasks into.
After this, tasqx add ... lands in home:
tasqx use home- The project must already exist and not be archived.
tasqx projectsmarks the current default with a*.
tasqx projects
List your projects.
| Command | What it does |
|---|---|
tasqx projects | The live ones, with the default marked * |
tasqx projects --all | Include archived ones |
--all is the only way to see archived projects — without it the table shows
only the projects that add and use will accept.
tasqx archive
Retire a project. Think shelf, not shredder: the tasks keep their history and stay in the store, the project just leaves the rotation.
tasqx archive kitchen-remodelThings to know before you archive:
- Once archived, no command will accept the project's name anymore — you can't
useit, and you can'taddormodifya task into it. - If you archive the project that is your default, the default is cleared:
a bare
tasqx addthen has no home until you runtasqx useagain. The command tells you when this happened. - Tasks in an archived project are left out of
tasqx nextand of the@workingset — the dashboard's working list included — because those answer "what should I do now". Name the project and they come back:tasqx next project:kitchen-remodel.list,reportandagendastill show them. The rule applies to any filter that mentions@working, so@working or +xalso hides a pending+xtask in an archived project; leave@workingout of the filter to see it. tasqx unarchiveis the way back.tasqx undodoes not reverse an archive, because archiving the default project also cleared the default, which the log does not restore.
tasqx unarchive
Put an archived project back into rotation.
tasqx unarchive kitchen-remodel- The project's tasks never left, so nothing else is restored; the line says
how many open tasks are back in
next. - A project that is not archived exits 5, an unknown name exits 4.
- It does not point the default project back at it.
tasqx usedoes that.
Adding and Editing Tasks
Capture fast, refine later. add and modify share the same tricks: inline
shortcuts in the title, natural-language dates, and flags for everything if you
prefer being explicit.
tasqx add
Aliases: a, new
Create a task. The simplest form is just a title:
tasqx add Buy milkThe title can carry inline shortcuts ("sugar"), so one line captures everything:
tasqx add Ship the release due:friday +api !high est:4h| Shortcut | Meaning |
|---|---|
+api | add the tag api |
project:work (or proj:work) | file it under work |
!high / !med / !low | priority |
due:friday | due date — natural language is fine |
scheduled:friday (or sched:) | when you can start — parks the task in backlog until then |
wait:monday | hide it until then — also parks it in backlog |
est:4h (or estimate:4h) | effort estimate (90m, 1h30m, 2d also work) |
repeat:"every monday" (or every: / recur:) | recurrence rule |
remind:-1h | remind me one hour before it's due, or at a time you name |
Every shortcut also exists as a flag (--due friday, --tag api,
--priority H, …) — same result, pick whichever reads better in your scripts.
More on dates, recurrence and reminders: Dates, Reminders and Recurrence.
A task without a project lands in your default project
(tasqx use changes which one that is).
When an open task in the same project already reads like the new one, add
still adds it and prints a note: line naming that task, up to three of
them. --json carries them as similar, an empty list when there are none.
tasqx modify
Aliases: mod, m, edit
Change an existing task. Takes the same inline sugar and dates as add:
| Command | What it does |
|---|---|
tasqx modify 42 due:friday !high | Set fields |
tasqx modify 42 Fix the login bug | Bare words replace the title |
Setting and clearing are different moves. Setting is due:friday;
removing is --clear due. There is no magic empty value:
tasqx modify 42 --clear due --clear remind--clear works for: project, priority, due, scheduled, wait,
remind, recurrence, estimate, tracked, budget_tokens. Tags are the
exception — a tag comes off by name, with tasqx untag.
A task's status is not a field you set: it moves through start, stop,
done, cancel and reopen — see Working on Tasks.
What a write prints
Every write answers with the same card: the task it named, then the outcome
and what changed, then the rest of that row. Bold is the write's own mark — it
says THIS is what changed, and nothing else wears it, because bold is the one
emphasis that survives NO_COLOR.
tasqx start 49▌ #49 Fix token refresh race on Android ▶ started H ▄▄▄▄ 17.0 mobile due tomorrow 17:00 UTC +bug est 4h #48 stopped · Ship the v2 pricing page
The rail at the left carries the state: ▶ while the task runs, ⊘ while it
is blocked, ▌ otherwise. Another task the write moved gets a line of its own
under the card:
tasqx done 50▌ #50 Rate-limit the /search endpoint ▌ done today api due Sat +perf est 3h #51 unblocked · Write the migration guide for SDK 3.0 note: no token counts were self-reported — tasqx done --help names the flags
Through a pipe the same words print, unfitted: the rail spells itself * for
running and B for blocked, and the gauge and the ▌ go. --json is
unchanged.
tasqx check
Acceptance criteria: the things that have to be true for a task to count as done.
tasqx check add 42 the notes name every breaking change
tasqx check set 42 <id> passed --evidence "cargo test: 0 failed"
tasqx check rm 42 <id>tasqx show numbers each check (2 [ ] the notes name every breaking change). <id> is that position, counting from 1 — tasqx check set 42 2 failed marks the second one — or the check's full id. A mistyped id is
refused with the task's real checks listed.
check add, check set and check rm answer with the one check they
touched and how many have passed, not the whole task; tasqx show has the
rest. A criterion may begin with -- (tasqx check add 42 --since 2026-01 is honoured): after the task, a dash-led word that is not one of check add's
own flags (--json, --no-daemon, …) is part of the criterion.
tasqx never runs a check. The criterion is a claim and the evidence is a
citation — both are stored exactly as you typed them and neither is
interpreted. If you want something run, a hook you installed runs it and calls
tasqx check set with the result, like any other client would.
Criteria in an annotation look the same and are not: an annotation is prose, so nothing can ask at completion time whether it was met. A check has a state.
Completing a task with a criterion still open or marked failed is not
refused — tasqx is a file on your disk, not a supervisor, and a refusal is
something a script routes around. tasqx done says so in one line, naming how
many failed and how many are still open, and it is counted: a completion is
proven only when every check passed, and tasqx report --outcomes has an
UNPROVEN column for the rest, adding N failed when any had a failed check.
That column is where the pattern shows up.
failed is a normal outcome and worth recording. Remove a check only when the
criterion was the wrong thing to ask — deleting one the work failed hides the
finding.
Token budgets
--budget-tokens sets a size gauge on a task:
tasqx add Port the payment adapter --budget-tokens 200000It counts fresh tokens — input, output and cache creation — and ignores
cache reads, because a budget dominated by re-reads measures how often an agent
re-read its own context rather than how big the work was. tasqx show prints
the pair once a budget is set.
It stops nothing. tasqx is a file on your disk that an agent talks to
between turns; it never sees the turn and could not interrupt one. An overrun
is a signal, and usually one signal in particular: the task was too big to hand
over whole. tasqx report --outcomes counts overruns so you can see which kinds
of work keep being cut too large.
For scripts that must not clobber a concurrent edit: --expected-rev makes
the modify fail (exit 5) if the task changed since you last read it.
tasqx tag
Attach one or more tags.
| Command | What it does |
|---|---|
tasqx tag 42 api release | Two tags, one command |
tasqx tag 42 +api | The leading + is optional |
Re-adding a tag the task already has is fine — the answer is simply the resulting tag set.
Tags are stored lowercase, so +API and +api are the same tag, and a tag
cannot contain a space — tasqx tag 42 "needs paint" is refused; write
needs-paint. A filter's +API matches it too. A store written by an older
tasqx is folded into this form the first time it is opened (spaces become
hyphens, duplicates merge), and each task it touched gets a tag.normalize
event naming every from → to it applied — event.list with that task's
ref shows them (see tasqx api).
tasqx untag
Remove one or more tags.
tasqx untag 42 apiAll or nothing: if any named tag isn't on the task, none are removed and the command tells you which tags the task does have. A typo never quietly succeeds.
tasqx annotate
Attach a timestamped note to a task. The text is stored exactly as you typed it — multi-line, markdown, links, all preserved.
tasqx annotate 42 Called the plumber, waiting on a quoteAnnotations show up in tasqx show, and they're searchable: the
memory system indexes them alongside your knowledge documents, so
"what did we decide about the plumber" is one tasqx memory search away.
For a long or multi-line note, give the text on stdin. - reads all of stdin,
as does a bare annotate when stdin is a pipe:
tasqx annotate 42 - < decision.mdOn a terminal, a bare tasqx annotate 42 opens $VISUAL or $EDITOR (else
vi) on a temporary file. Saving an empty or unchanged file stores nothing, and
so does empty stdin. A note that is only - can't be written as an argument.
tasqx show numbers the notes, oldest first, with a dim [n] in front of
each. That number, a unique prefix of at least eight characters of the note's
id, or the full id names a note anywhere a note is named. Notes written within
about a minute share their first eight characters, so a prefix is often
ambiguous (exit 5, listing the candidates); the number is the short way.
To correct a note instead of adding another, name it with --edit:
tasqx annotate 42 --edit 2 Plumber quoted 400, booked for FridayThe text is replaced in place: the note keeps its id, its timestamp and its
place in the history, so the first note is still the box card's Description,
and search finds the new text rather than the old. tasqx undo puts the old
text back. --edit takes the same stdin and editor forms; the editor opens on
the note's current text.
Put a note on the wrong task? Move it instead of retyping it:
tasqx annotate 42 --move 2 --to 57The note keeps its id, text and timestamp and lands on #57 in its place by
age; search finds it there. tasqx undo puts it back on #42.
tasqx unannotate
Permanently scrub one annotation's text:
tasqx unannotate 42 2This is a hard delete, not a hide: the text is overwritten in the store,
not merely removed from what tasqx shows you — use it to take back a secret, a
customer name, or a wrong root cause pasted into a note by mistake. What stays
behind is a tombstone (the id and when it was removed), for audit, with no
text in it. The tombstone is not hidden away: tasqx show and task.get both
list it, and the box card's Notes row counts it.
Name the note by the [n] position tasqx show prints, a unique id prefix of
eight or more characters, or its full id (annotations[].id in --json).
tasqx undo does not cover this: by the time the
removal is recorded, there is nothing left in the log to restore. An unknown
id, or one already removed, exits 4 rather than silently doing nothing.
Finding Tasks
Five commands answer "what's on my plate?", each from a different angle — plus a small filter language they all share.
tasqx list
Aliases: ls, l
The task table. With no filter it shows your working set: open tasks you can actually act on right now (blocked and hidden-until-later tasks are left out).
| Command | What it does |
|---|---|
tasqx list | The working set |
tasqx list project:work +api | Narrowed |
tasqx list due.before:friday | Deadline pressure only |
tasqx list --sort due --limit 20 | Order by a key (-due for descending, repeat or comma-separate several); the default is -urgency |
tasqx list --json --fields short_id,title,due | Return only those fields; use it with --json, the human table is not projected |
tasqx list --holder me | Choose work as holder me: with @working, tasks someone else has claimed are left out |
A list shows at most 100 rows. When more match, the head says so (100 of 192 tasks) and a last line gives the command for the rest: --offset 100 for the
next page, --limit 192 for all of them (--limit tops out at 10,000).
tasqx agenda reads every match, so it is never cut.
A bare tasqx in a pipe or script does the same thing; on an interactive
terminal it opens the dashboard instead.
tasqx next
The "what now" button. Prints the single most urgent task that isn't blocked.
tasqx next--card prints that task as the same 72-column box-drawn card tasqx show --card draws, meant for pasting into a document rather than reading
at a terminal; --ascii draws its borders with + - |.
tasqx agenda
Aliases: ag, cal
What's coming up, ordered by time and grouped by day — the calendar view of your tasks.
| Command | What it does |
|---|---|
tasqx agenda | The next 14 days |
tasqx agenda --days 3 | Just the next few |
- Each task appears on the earlier of its due date and its scheduled date — the first day it asks something of you — and the WHEN column says which of the two that was.
- Overdue tasks always show, no matter the window.
- Tasks it can't place (no date, or past the horizon) are counted under the
table rather than silently dropped, with the exact command that would reach
them: the
--daysthat reaches the furthest one, ortasqx listwhen it is further out than--daysgoes. - Days are UTC days: a bare date is midnight UTC, so a day groups the same way the store holds it.
tasqx show
Alias: get
One task, in full: description, tags, annotations, dependencies, whether it's blocked, and its revision number.
tasqx show 42--card prints the task as a fixed 72-column box-drawn card instead of the
usual screen — meant for pasting into a document, a chat reply or a pull
request rather than reading at a terminal. --ascii draws its borders with
+ - | for destinations that mangle box-drawing characters.
tasqx show 42 --card
tasqx show 42 --card --asciiWhen an annotation was removed with
tasqx unannotate, show
prints one line per removed annotation under the history — a tombstone naming
its id and when it was removed — and the box card's Notes row appends
", N removed".
tasqx why
Every open task gets an urgency score, and the ordering of every list comes
from it. why shows the arithmetic instead of asking you to trust it:
tasqx why 51#51 Write the migration guide for SDK 3.0 priority M 3.9 deadline due Mon 8.0 age 30 days 0.3 urgency 12.2 blocked by #50 Rate-limit the /search endpoint — `next` skips this task
Each row names the input in the words the rest of the terminal uses — the priority letter, how the deadline reads on a list, how old the task is — and the last row is the score every list sorts by.
Urgency is recomputed on every read, so the deadline row — and the total it feeds — climb as the due date approaches. The scores in any capture, including this one, are an illustration; the rows are the contract.
If tasqx ranks something surprisingly high or low, this is where the answer is.
--card prints the box card first, with this same arithmetic underneath
it instead of the plain text header above; --ascii draws the card's borders
with + - |.
The filter language
Everything that lists tasks (list, agenda, pick, watch, report,
export) takes the same filter expressions:
| Filter | Matches |
|---|---|
project:work | In a project |
proj:work | The same as project: |
status:pending | By status: triage, backlog, pending, active, done, cancelled |
priority:high | By priority: H, M, L, or high, medium, low |
+api | Has a tag |
-api | Does NOT have a tag |
due.before:friday | Due before a date |
due.after:monday | Due after a date |
completed.before:friday | Finished before a date |
completed.after:monday | Finished after a date (pair it with status:done) |
weekly review | Title contains every word, any case |
'"memory explorer"' | Title contains the phrase |
title:review | The same, spelled as a key |
status:any | Every status, done and cancelled too (status:all is the same) |
@working | Pending or active and not blocked — what a bare list shows. next, pick and list --holder with @working also leave out a task someone else has claimed |
@blocked | Open, with a dependency that is not yet done or cancelled |
@claimed | Under a live claim, whoever holds it (claims) |
A bare word is a title search, so tasqx list weekly review finds "Weekly
planning review". A word that looks like a token but is not one — remind:x,
due.before: with no value — is still refused. % and _ in a word are plain
characters.
Combine with and, or and parentheses:
tasqx list "project:work and (+api or +ui)"Values with spaces need quotes that actually reach tasqx — so wrap the
whole thing in single quotes (or backslash-escape it) to protect it from your
shell. Inside the quotes, parentheses and the and/or keywords are ordinary
characters too, as in a shell:
tasqx list 'project:"Home Renovation"'tasqx never guesses where a quoted value was supposed to end. If the shell
eats your quotes, project:Home Renovation is the project Home plus the title
word Renovation; a project that does not exist is refused by name, which is
the signal to quote. +needs paint is the tag needs plus the word paint;
when that matches nothing, list prints a hint: line naming +"needs paint".
The same rule is what lets you pass a whole expression as one argument:
tasqx list "+api or +web" is the expression.
report takes one axis, as its first word, and refuses report project priority;
write title:priority to match that word in titles.
add and modify do not refuse an unquoted multi-word value: it is either
not_found (no project named by the leading word) or, when a project is named
exactly that leading word, the task is filed there and the remainder is welded
onto the title. tasqx add "paint" project:Home Renovation becomes project
Home, title "paint Renovation". Quote it on both sides and this cannot
happen: tasqx add "paint" project:"Home Renovation".
Write \" for a literal quote and \\ for a literal backslash — a name
holding a quote needs that form on both sides:
tasqx add "paint" project:"My \"Big\" Project".
due is compared as an instant, not a calendar day.
Working on Tasks
The lifecycle: start, stop, done — with cancel, reopen, pick and undo around it. Nothing here destroys data; every change is recorded and reversible.
tasqx brief
Everything you need before starting a task, in one read.
tasqx brief 42
tasqx brief 42 --memory-limit 3You get the task's own card, then two things tasqx show never carried:
- Depends on — each prerequisite with what it concluded: its newest
annotation, printed under it. That is usually the single most useful thing to
read before picking work up, and it used to cost one
tasqx showper prerequisite. - From memory — documents and past annotations relevant to this task, found under a query tasqx builds from the task's own title, tags and project. You do not supply search terms, which matters because a guessed term that finds nothing looks exactly like a store with nothing in it. Half the slots are held for docs, so an imported ruling still reaches the page when a project's own task notes share its words; annotations take the rest, and docs take the whole page when more of them matched than that.
Memory is scoped to the task's project and stays scoped when that finds
nothing — it will not quietly search everything instead. When you want wider,
tasqx memory search is the read for it.
tasqx show is still the right command when you only want the task.
--card works here too, and prints the same box-drawn card tasqx show --card prints, followed by the brief's own Depends on / Blocks /
From memory sections.
tasqx start
Alias: s
Mark a task active and start its timer.
tasqx start 42 --keep: keep other active tasks running too
tasqx start 42
tasqx start 42 --keepBy default starting one task stops any other active one ("single-active") —
--keep opts out.
That stays true for you at a shell. It stops being true across sessions: if
an AI agent holds a running timer, starting a task from another session is
refused rather than silently stopping their clock, because stopping it would
leave the rest of their work untracked and tell only you about it. --keep
runs both deliberately.
The extra flags (--client, --session-id, --transcript-path) are for AI
agents reporting who is doing the work; see
AI Agents and Automation.
tasqx claim
Claim a task so other agents skip it. A claim is a lease: 30 minutes unless
--lease names another length, at most 24 hours.
tasqx claim 42 --holder builder-2: hold #42 for builder-2tasqx claim 42 --holder builder-2 --lease 2h: renew it for two hours
tasqx claim 42 --holder builder-2While the lease is live, anyone else's claim, start, stop, done,
cancel or modify to cancelled on the task exits 5 naming the holder and when
the lease runs out (--force-claim goes ahead and the log names whose claim it
overrode; done --force is only for open dependencies), and starting another task
leaves it running instead of stopping it. tasqx next skips it for anyone but
the holder, and so does tasqx pick. tasqx list still shows it, with its
holder and expiry: a list is a view, and only list --holder <id> @working —
an agent choosing its own work — leaves other holders' claims out. tasqx list @claimed lists every live
claim, and tasqx show names the holder and the expiry.
Without --holder the shell holds the claim as user, and tasqx next from
the same shell still shows it. next, list and pick take --holder to
choose work as a named holder, and start, done and cancel take it to act
on a task claimed under one (the timer itself stays the shell's):
tasqx start 42 --holder builder-2An AI agent on the MCP server holds it as its connection unless it names a
stable actor. Nothing sweeps an expired lease: once it runs out, anyone may
claim the task, and the log records that as a takeover.
Claim, renew, release and takeover each land in the event log with the holder.
tasqx release
Give a claim up. Only its holder can: a task with no claim, or someone else's,
exits 5. done and cancel end a claim without it. --force-claim releases a claim
someone else holds — one left by an agent that crashed or reconnected under a
new id — and the log records it as forced. A claim whose lease has run out can
be released by anyone, no --force-claim needed.
tasqx release 42 --holder builder-2tasqx stop
Alias: st
Pause an active task. The tracked time is kept.
tasqx stop 42While a timer runs, tracked already counts it: list, show, report and
the card all include the open interval up to the moment you ask.
tasqx adjust
Correct a task's tracked time when the clock was wrong — a timer left running over lunch, or work that never reached it.
tasqx adjust 42 -2h25m --reason "idle gap"
tasqx adjust 42 +30m --reason "forgot to start"- The reason is required and stays in the task's history with the correction.
- It works on done tasks too, so
report --outcomescalibration can be fixed after the fact. A correction that would take the total below zero is refused. showprints the net of every correction beside the total:tracked 2h30m (adjusted -2h25m).tasqx undotakes the newest correction back.
tasqx done
Aliases: d, x, complete
Complete a task.
tasqx done 42- If the task recurs, completing it spawns the next occurrence — the answer shows it.
- If other tasks were waiting on this one, they unblock now, and the answer names them. Finishing work tells you what it made possible.
- A task with dependencies still open is refused, naming them.
tasqx done 42 --forcecompletes it anyway; the override is recorded on the completion andtasqx report --outcomescounts it under FORCED. Cancelling a blocked task never needs the flag. - Report what the work cost with
--tool,--modeland--input-tokens,--output-tokens,--cache-read-tokens,--cache-creation-tokens(or one unsplit--total-tokens). Any count records a measurement;--tooland--modelalone still land on the completion.
tasqx done 42 --tool claude-code --model claude-opus-5 --input-tokens 4200 --output-tokens 900tasqx cancel
Aliases: delete, del, rm
Cancel a task. This is as close to "delete" as tasqx gets, on purpose: the
task keeps its history, stays in the event log, and comes back with reopen.
tasqx cancel 42If other tasks were waiting on the cancelled one, they're released — a dead prerequisite shouldn't block live work.
tasqx reopen
Bring a done or cancelled task back to pending.
tasqx reopen 42If open tasks depended on it, they become blocked again — the mirror image of
what done unblocked. The answer names them.
tasqx git
Record what happened to a task's pull request, and let the task follow it.
tasqx git event opened --branch task/42-pricing --pr https://github.com/acme/web/pull/7
tasqx git event merged --branch task/42-pricing --pr https://github.com/acme/web/pull/7 --sha 4f9c2e1
tasqx git event closed --branch fix-typo --pr https://github.com/acme/web/pull/8 --task 43A branch named task/<id>-<slug> names its task, whatever follows the id
(task/42-pricing-rebased too); --task names it when the branch does not.
- opened starts a pending task. It stops no other timer.
- merged completes the task, with
Merged <pr> (<sha>)as its delivery note, but only when every check passed (or it has none) and nothing blocks it. Otherwise the task stays open and a note names the checks that did not pass. - closed adds a note and leaves the status alone.
tasqx show lists the branch, the PR and the merge commit under git.
Reporting merged again after the checks are fixed completes the task. A PR
starts its task once: reporting opened again after you stopped it does
nothing, and a task a merge completed that you reopened stays open. An event
whose outcome already holds changes nothing, and so does one a recorded merge
settled (closed after merged); both say why. When someone holds a claim on
the task, the event acts as that holder.
tasqx never asks GitHub, or any network. Something that watched the PR tells
it: a script reading gh pr view --json, a CI step, a git hook.
tasqx triage
Work you or an agent finds mid-task can be filed as a proposal instead of
going straight into the backlog: tasqx add … --triage (an agent passes
status: "triage", or every MCP add lands there once mcp.add_status is
triage). A proposal stays out of @working, next and the urgency ranking,
and cannot be started or completed until it is accepted.
--discovered-from <id> links it to the task it was found on.
tasqx triagelists the inbox oldest first, with the tool that filed each proposal and the
task it was found on. tasqx list status:triage is the same set as a table.
Settle each one with a single command, below.
tasqx accept
tasqx accept 12Makes the proposal pending — or backlog, while a future wait or scheduled
still holds it. tasqx undo puts it back in triage.
tasqx decline
tasqx decline 12 --reason "out of scope for this release"Cancels the proposal and keeps the reason as a note. tasqx undo puts it back
in triage; the note stays.
tasqx duplicate
tasqx duplicate 12 --of 7Cancels the proposal, moves its notes to the task it repeats, makes the tasks
that waited on it wait on that one instead (unless that would close a loop) and
links the two with duplicates. Undo does not take this one back; tasqx reopen brings the proposal back to triage, and the notes and dependents stay
where they went.
The dashboard's header, the HTML report and the desktop app count what is waiting in triage.
tasqx pick
Aliases: p, fzf
A full-screen browser over your tasks, each row the row tasqx list prints.
| Command | What it does |
|---|---|
tasqx pick | Browse the working set |
tasqx pick project:work | Narrow the candidates first |
| Key | What it does |
|---|---|
j/k | move, as do the arrows; g/G jump to the ends |
/ | search as you type, fuzzy-search style: wac finds "Write API conformance tests"; Enter or Esc keeps the filter |
enter | open that task's card, the one tasqx show prints (Esc goes back) |
s | start the task under the cursor, from the list or from its card |
q | leave; esc clears a search first, and only then leaves |
The bar along the bottom names the keys that can do something where you are, so it is shorter on an empty list, on a card, and on a narrow terminal — the way out is named at every width.
s is the only key that writes. A filter can list work that is done or
cancelled, and s on such a row is refused on the key bar's own row with the
screen still open.
Captured on a terminal 120 columns wide, since this screen cannot be piped:
pick @working 14 tasks · 1 overdue · 1 due today · ▶ #48 running ID URG TASK PROJECT DUE TAGS ▸ 47 H ▄▄▄▄ 18.1 Renew the TLS certificate for api.example.dev infra 2d ago +ops ▶ 48 H ▄▄▄▄ 17.8 Ship the v2 pricing page website today 17:00 +launch 49 H ▄▄▄▄ 17.0 Fix token refresh race on Android mobile tomorrow 17:00 +bug 50 H ▄▄▄▄ 15.9 Rate-limit the /search endpoint api Sat +perf 58 L ▄▄▄▁ 9.4 Book the dentist home Tue 52 M ▄▄▃▁ 8.6 Dark mode for the settings screen mobile 25 Sep +ui 56 M ▄▄▁▁ 6.0 Accessibility pass on the signup flow website 28 Sep +a11y 54 M ▄▂▁▁ 4.3 Cut build time under five minutes infra +ci 53 M ▄▂▁▁ 4.2 Move nightly backups to object storage infra +ops 55 M ▄▂▁▁ 4.0 Add OpenAPI examples for every endpoint api +docs 57 L ▃▁▁▁ 2.0 Replace the carousel on the home page website +ui 59 L ▃▁▁▁ 2.0 Archive last year's analytics dashboards website 61 - ▁▁▁▁ 0.4 Sketch ideas for the Q4 offsite home 60 - ▁▁▁▁ 0.1 Investigate flaky checkout test mobile +bug +ci j/k move / search enter open s start g/G ends q leave
The key bar is the screen's bottom row, below the rest of the list:
pick @working 14 tasks · 1 overdue · 1 due today · ▶ #48 running ID URG TASK PROJECT DUE TAGS ▸ 47 H ▄▄▄▄ 18.1 Renew the TLS certificate for api.example.dev infra 2d ago +ops ▶ 48 H ▄▄▄▄ 17.8 Ship the v2 pricing page website today 17:00 +launch 49 H ▄▄▄▄ 17.0 Fix token refresh race on Android mobile tomorrow 17:00 +bug 50 H ▄▄▄▄ 15.9 Rate-limit the /search endpoint api Sat +perf 58 L ▄▄▄▁ 9.4 Book the dentist home Tue 52 M ▄▄▃▁ 8.6 Dark mode for the settings screen mobile 25 Sep +ui 56 M ▄▄▁▁ 6.0 Accessibility pass on the signup flow website 28 Sep +a11y 54 M ▄▂▁▁ 4.3 Cut build time under five minutes infra +ci 53 M ▄▂▁▁ 4.2 Move nightly backups to object storage infra +ops 55 M ▄▂▁▁ 4.0 Add OpenAPI examples for every endpoint api +docs 57 L ▃▁▁▁ 2.0 Replace the carousel on the home page website +ui 59 L ▃▁▁▁ 2.0 Archive last year's analytics dashboards website 61 - ▁▁▁▁ 0.4 Sketch ideas for the Q4 offsite home 60 - ▁▁▁▁ 0.1 Investigate flaky checkout test mobile +bug +ci j/k move / search enter open s start g/G ends q leave
pick needs a real terminal (it draws a screen), so in scripts use
tasqx next to ask the same question and tasqx start <ref> to act on it.
Leaving without starting a task exits 0: reading your tasks and starting none
of them is an ordinary way to use it, so tasqx pick && … and a prompt
indicator survive q. A filter that matches no task still exits 4, and so
does an empty working set: that is a question tasqx could not answer, not a
session you ended.
tasqx undo
Alias: u
Take back the newest recorded change.
tasqx undoundo is deliberately narrow, and honest about it:
- Twelve operations are undoable:
done,cancel,modify,tag,stop,untag,undep,annotate,annotate --edit,annotate --move,adjustandaccept. Everything else is refused by name, with the command that does take it back — undoing astartistasqx stop, undoing anarchiveistasqx unarchive. - Undoing a
doneputs the task back in the status it left. If it was running, the interval reopens and its time comes back off the total; criteria the completion proved (an agent'schecks_passed) go back to open. If completing it spawned the next instance of a recurring task, that instance is removed too — but only while nothing has touched it. Touch it (annotate it, change it, start it) and undo refuses, naming it, rather than throw your work away. A completion that also recorded a token measurement is refused too: a measurement is the record of what a turn cost, andundodoes not retract one. - Undoing a
cancelputs back the status it left. Undoing amodifyputs back every field it changed, and the answer lists them, so a clearedprojectis back anddueis cleared again. Undoing atagtakes off only the tags that call attached, not one the task already had. - A
modifyortagwritten by an older tasqx did not record what it replaced, so undo refuses those and says so; from this version on every one does. - It reverses the newest recorded event, which is not always the last command you typed: a command that changed nothing recorded nothing, so undo reaches past it. That's why the answer names exactly what it undid — read it.
- Undo appends its inverse to the event log rather than erasing anything, so your history reads "X happened, then it was undone".
- There is no redo.
Dependencies
"This can't start until that is finished." Dependencies turn a pile of tasks into an ordered plan: blocked tasks drop out of your working set, and finishing a task tells you what it just unblocked.
tasqx dep
Make one task wait on another. The order is: dependent first, prerequisite second.
Make task 2 wait on task 1:
tasqx dep 2 1Task 2 is now blocked: it disappears from tasqx list's default working
set and from tasqx next, because you can't act on it yet. It reappears the
moment task 1 is done or cancelled — and tasqx done 1 names it in the
answer.
Blocked work isn't invisible, though: the dashboard keeps it on screen — its TASKS panel holds blocked rows alongside the rest and its header counts them — precisely because the default filter hides these tasks everywhere else.
tasqx undep
Remove a dependency edge, same argument order as dep:
Task 2 no longer waits on task 1:
tasqx undep 2 1If that was the task's last unfinished prerequisite, it unblocks immediately.
Building a chain
Dependencies pay off when you break a feature into steps:
tasqx dep 2 1: migration waits on schematasqx dep 3 2: shipping waits on migration
tasqx add Design the schema
tasqx add Write the migration
tasqx add Ship it
tasqx dep 2 1
tasqx dep 3 2Now tasqx next walks you through the chain in order, one actionable task at
a time — and an AI agent working through
MCP does exactly the same.
Dates, Reminders and Recurrence
tasqx reads dates the way you'd say them, and four different date fields let you say four different things about when.
Every date field — due, scheduled, wait — takes the same natural-language
grammar, through the flag or through the sugar. It resolves to RFC3339 at the
moment you type it.
Writing a date
Anywhere a date is expected — due:, --scheduled, wait:, remind: — you
can write:
tomorrow friday friday 17:00
in 3 days eom at 6pm
-1d 2026-09-15 2026-09-15T17:00
eom is end of month. -1d is yesterday. A bare weekday is the next one
after today, so on a Wednesday friday is in two days. next friday is that
weekday in the following Monday-to-Sunday week (nine days out), and
next week is that week's Monday; this friday and last friday are
refused rather than guessed. A bare time that has already passed
today rolls to tomorrow. Full RFC3339 works when you want to be exact.
Everything is UTC. A bare date is midnight UTC, and a clock time is a UTC
clock — due:17:00 is 17:00 UTC wherever you type it, whatever zone the
machine is set to, and every screen prints it back as 17:00. An offset you
write yourself (+02:00, or a trailing Z) is honoured as written:
2026-07-20T17:00:00+02:00 means another zone's clock, and is converted to UTC
on the way in.
What you can write
| Form | What you can write |
|---|---|
| Absolute | 2026-07-20, 2026-07-20T17:00, "2026-07-20 17:00", any RFC3339 |
| Relative days | today, tomorrow, yesterday, now |
| Boundaries | eom / "end of month", eow / "end of week" (ISO week ends Sunday) |
| Weekday names | monday..sunday or mon..sun — the next one after today. "next friday" is that day in the following Monday-to-Sunday week, "next week" that week's Monday; this/last are refused rather than guessed |
| Counted spans | in 1 day, "in 3 days", "in 2 weeks", "in 1 month", in 3 months — days, weeks and months only; in 2 hours is rejected |
| Signed offsets | -1d, +3d, 3d (no sign means the future); the units are d, w, mo, y, so 3d, 2w, 1mo and 1y all work |
| Times | 17:00, 5pm, attached to a day with a space — "tomorrow 17:00", "friday 9am", "tomorrow 9am", "monday 5pm" — or a full instant — "2026-09-09 17:00", 2026-09-09T17:00:00+02:00 (an explicit offset is converted to UTC on the way in) |
| Leading filler | "at 6pm", "on friday", "by monday 5pm" — at/on/by/@ are ignored |
The rules that resolve ambiguity
| Situation | Resolution |
|---|---|
| A date with no time | 00:00:00 UTC — the start of that day, whatever your zone. |
A bare time (9am) | 09:00 UTC today, or tomorrow if that UTC time already passed. |
| A weekday that is today | The next one — seven days out, not zero. |
| A naive date with a time | A UTC clock, whatever your zone. An explicit offset (+02:00) is honoured and converted to UTC instead. |
The literal now | This exact instant — not midnight, unlike every other keyword here. It is what due.before:now means, so it can find a task due earlier today. |
Short offsets carry day-and-larger units only: d, w,
mo, y. -2h is not a date — it is a reminder offset, and a different
grammar. Feeding it to --due is a clean error, not a guess:
tasqx add "Overdue ping" --due -2herror [bad_request]: could not parse date: "-2h" (try e.g. tomorrow, friday, 2026-07-20, "in 3 days" (day/week/month/year offsets only, no hours/minutes), eom, or 2026-07-20T17:00)A leading hyphen needs no escaping
--due -1d works. It looks like it should trip the argument parser into reading
-1d as a flag — every date-taking flag opts out of that explicitly, so a
signed offset is always a value:
tasqx add "Renew the TLS cert" project:work.tasqx +ops --due -1d▌ #3 Renew the TLS cert
▌ added - ▄▄▄▄ 12.0 work.tasqx due yesterday +opsThe four date fields
| Field | What it says | Effect |
|---|---|---|
due | When it must be finished | The deadline. Drives urgency up as it approaches, and is the anchor for relative reminders; overdue tasks stay loudly visible. A deadline with a time is overdue once it passes; a date without a time is due by the end of that day (UTC) |
scheduled | When you plan to start | A future value parks the task in the backlog, out of the @working set, until it passes |
wait | Hide it until this moment | Same parking, different intent: "not my problem yet" |
remind | When to nudge you | Fires a reminder; see below |
scheduled and wait both keep a task out of your working set until their
moment arrives — the difference is what you mean, and
tasqx agenda places tasks on scheduled
(or due), never on wait.
Estimates
Human durations, parsed at the edge and stored as ISO-8601 so report can total
them. 4h, 90m, 1h30m, 2d, 1w, or ISO PT4H directly.
tasqx add "Nail down the schema" est:soonerror [bad_request]: could not parse duration: "soon" (try e.g. 4h, 90m, 1h30m, 2d, 1w, or ISO PT4H)Reminders
remind: takes an offset from the due date, or an absolute time:
| Command | What it does |
|---|---|
tasqx add Call the bank due:"friday 9am" remind:-30m | 30 minutes before |
tasqx add Water plants remind:"friday 8am" | At an exact time |
The offset stays symbolic: move the due date and the reminder moves with it.
Reminders fire only while tasqx daemon
is running. A task notifies you only if you gave it a remind; nothing else
ever puts a task on the reminder heap.
The two forms
| Form | Example | Behaviour |
|---|---|---|
Offset from due | -1h, -30m, -2d, +15m | Stays symbolic in the store. Move due and the reminder moves with it. |
| Absolute instant | "friday 9am", 2026-07-20T17:00 | Resolved once, at set time, through the same date grammar. |
The sign is what disambiguates them: a leading - or + means offset,
anything else goes to the date parser. Without that rule 3d would be ambiguous
— "3 days before due" or "in 3 days"?
Offsets take s, m, h, d, w. Negative is before due, which is what you
almost always want.
tasqx modify 1 --remind -1h
tasqx show 1▌ #1 Ship the v1 JSON API freeze
▌ modified remind -1h H ▄▄▄▄ 15.7 work.tasqx due Fri +api +release est 4h rev 2
▌ #1 Ship the v1 JSON API freeze
▌
▌ status pending urgency H ▄▄▄▄ 15.7
▌ project work.tasqx due Fri (in 3 days)
▌ remind -1h estimate 4h
▌ tags +api +release created today 07:51 (just now)
▌ modified today 07:51 (just now)
▌ rev 2Notice remind shows as -1h, not as a resolved timestamp. That
is the point: it is still an offset. Push due back a week and the reminder
follows, with no second edit.
Who delivers them
The daemon. It keeps an
in-memory min-heap of upcoming reminder instants, rebuilt from the store on
start and whenever a task changes. A one-shot tasqx command never fires
anything — there would be nobody to fire it.
tasqx add "Deploy the release" --due "2026-07-16T09:00" --remind -1h
tasqx daemon --socket tasqx-remdemo▌ #1 Deploy the release
▌ added no project · set a default with tasqx use <project> - ▄▄▄▂ 10.3 due Thu
tasqx daemon: listening on tasqx-remdemo (Ctrl-C to stop)
tasqx daemon: store ~/.local/share/tasqx/tasks.db
tasqx reminder: [#1] Deploy the release (due 2026-07-16T09:00:00Z)That reminder had already ripened (due 09:00, minus 1h, and it was past 08:00) — so it fired on the next daemon start. A reminder that ripened while the daemon was down still fires, once, on the next start. Sleeping your laptop does not lose it.
It fires exactly once
Firing writes a reminded event, and that event row is the dedupe record.
Restart the daemon and the same reminder does not come back:
tasqx daemon --socket tasqx-remdemo # second start, same storetasqx daemon: listening on tasqx-remdemo (Ctrl-C to stop)
tasqx daemon: store ~/.local/share/tasqx/tasks.dbSilence — correct. The key is the (task, instant) pair, not just the task:
moving due moves a relative reminder to a genuinely new instant, which
should fire again.
Delivery never fails
The always-compiled backend writes one line to stderr and returns. That is the headless/CI-safe path: with no notification transport anywhere, delivery degrades to a logged line and exit 0 — never an error.
Native OS toasts (Windows, macOS, Linux/D-Bus) live behind the off-by-default
notify-os build feature and need an explicit opt-in:
# config.toml
[notify]
enabled = true
Even then the stderr line still comes first, so the verifiable surface never depends on which backend is live.
Firing one by hand
reminder.fire takes a ref and an at instant, and it is idempotent — the
second call reports fired: false rather than notifying twice:
echo '{"tasqx":"1","id":"r1","method":"reminder.fire","params":{"ref":"1","at":"2026-07-16T00:00:00Z"}}' | tasqx api
echo '{"tasqx":"1","id":"r2","method":"reminder.fire","params":{"ref":"1","at":"2026-07-16T00:00:00Z"}}' | tasqx api{"id":"r1","ok":true,"result":{"at":"2026-07-16T00:00:00Z","fired":true,"short_id":1},"tasqx":"1"}
{"id":"r2","ok":true,"result":{"at":"2026-07-16T00:00:00Z","fired":false,"short_id":1},"tasqx":"1"}And the event is in the log, where the dedupe check reads it:
{
"actor": "user",
"entity": "task",
"op": "reminded",
"payload": {
"at": "2026-07-16T00:00:00Z",
"due": "2026-07-17T00:00:00Z",
"remind": "-1h",
"short_id": 1,
"title": "Deploy the release"
},
"ts": "2026-07-16T08:52:35.4429051Z"
}Recurrence
repeat: makes a task come back. A recurring task is a template:
completing an instance spawns the next one with its date advanced by the rule.
Set it with repeat: / every: / --repeat, and stop it with
tasqx modify <ref> --clear recurrence.
| Rule | Example |
|---|---|
every N days|weeks|months | "every 3 days", "every week" |
weekly on <days> | "weekly on mon,wed,fri" |
monthly on day <D> | "monthly on day 15" |
monthly on the <Nth> <weekday> | "monthly on the 2nd tuesday", "monthly on the last friday" |
This is a deliberate subset — not full RRULE. Anything outside it is a clean error.
tasqx add Water plants repeat:"every 3 days"
tasqx add Standup repeat:"weekly on mon,wed,fri"
tasqx add Pay rent due:"2026-09-01" repeat:"monthly on day 1"
tasqx add Team retro repeat:"monthly on the 2nd tuesday"- Completing a recurring task spawns the next occurrence; the answer shows it.
The occurrence keeps the task's first note (its description) and its checks,
reset to open, and its Repeats row names the one it came from
(
every week from #604).tasqx briefon it adds a "Last time" section quoting what the previous occurrence delivered. - Missed occurrences don't pile up — they collapse into a single next one.
every N monthscan drift across short months; anchor withmonthly on day 15when the day of month matters.- Stop a recurrence with
tasqx modify <ref> --clear recurrence.
tasqx add "Water the plants project:home repeat:\"every 3 days\" due:today"
tasqx done 4
tasqx show 5▌ #4 Water the plants
▌ added - ▄▄▄▃ 11.4 home due today 23:59 ↻ every 3 days
▌ #4 Water the plants
▌ done today home due today 23:59
#5 next, due Fri
▌ #5 Water the plants
▌
▌ status pending urgency - ▄▄▃▁ 8.9
▌ project home due Fri (in 4 days)
▌ repeats every 3 days created today 07:51 (just now)
▌ modified today 07:51 (just now)
▌ rev 1Missed occurrences collapse
If the computer was off for a week, a daily task does not hand you seven instances. The rule advances at least once and then skips every slot at or before now, so you get exactly one future instance. A backfill storm is never useful.
Month-end, precisely
The two monthly forms differ on purpose, and the difference matters at month boundaries:
| Rule | From Jan 31 | Why |
|---|---|---|
monthly on day 31 | Jan 31 → Feb 28 → Mar 31 | Re-clamps against the stored target day every step, so month-end recovers. |
every 1 month | Jan 31 → Feb 28 → Mar 28 | Advances from the previous (already clamped) date, so it drifts and stays there. |
Pick monthly on day 31 when you mean "the last-ish day of every
month". Pick every 1 month when you mean "same slot, one month on".
Dashboard and Live View
Two ways to watch your work instead of querying it, and the daemon that makes the live one possible.
Screens and pipes
Six commands open a screen instead of printing a table, and each answers a
pipe in its own way. tasqx pick, tasqx dashboard and tasqx config edit
refuse one outright rather than write escape codes into it.
tasqx memory list and tasqx notes print their one-line-per-row tables
instead, and
tasqx watch prints each update as it arrives rather than repainting a screen
— it needs a running daemon either way. tasqx board is the same live view in
a browser.
| Command | What it opens |
|---|---|
tasqx pick | browse tasks, search them, read one, start one |
tasqx dashboard | the overview, and what a bare tasqx opens |
tasqx config edit | settings, previewing a theme as you move over it |
tasqx memory list | your docs, with the one under the cursor beside them |
tasqx notes | your notes: read, archive, promote or remove one |
tasqx watch | a table repainted on every change (needs a daemon) |
tasqx list never opens a screen. It is the verb that always prints the
table, on a terminal and through a pipe alike.
tasqx dashboard
Alias: dash
A full-screen overview of everything: your tasks, your projects, a burndown,
recent activity, effort and token spend — six panels (tasks, projects,
burndown, pulse, effort, tokens) over one snapshot, with a header that
counts what matters
(17 open · 1 active · 2 overdue · 3 blocked · 8 done/week).
| Command | What it does |
|---|---|
tasqx | On a terminal, a bare tasqx opens the dashboard |
tasqx dashboard | The same screen, spelled out |
tasqx --json dashboard | The panel data as one JSON document, no screen |
- It's read-only, with two exceptions:
popens the task browser,pick. Enter there reads a task, andsstarts it and brings you back.Nopens the notes browser, where a note can be archived, promoted or removed; closing it brings you back.q, Esc and Ctrl-C all close the dashboard. - Blocked work stays visible here, which is the point: the default task list
hides it. There is no BLOCKED panel: the NOW, NEXT UP, DUE, BLOCKED and RECENT
panels were folded into the one TASKS panel, so a blocked task keeps its row
there (and its
"blocked": trueunder--json), and the header counts them (3 blocked). --jsonis not the screen in text. The document carries a payload for four of the six panels —tasks,projects,burndownandtokens— beside thestatusheader it always writes and apanelsarray naming what you asked for.pulseandeffortare drawn from the screen's own model, sotasqx --json dashboard --panels pulse,effortis a valid request that answers with no panel payload at all.- Layout adapts to your window; below 56×14 it won't open.
- Configure it under
[dashboard]in the config: which panels, in what order, refresh mode and time window (tasqx config listshows the options).
For scripts and agents: the dashboard only opens when both stdin and
stdout are an interactive terminal. Piped, redirected, or with --json, a
bare tasqx prints the plain task table instead. But an agent or harness that
allocates a pty looks interactive, and a bare tasqx there opens a
full-screen program that waits for a keypress. So in anything automated, spell
the verb: tasqx list always means the table.
tasqx watch
A task list that redraws itself the moment anything changes — from another terminal, from an AI agent, from anywhere.
| Command | What it does |
|---|---|
tasqx daemon | In one terminal: the server |
tasqx watch project:work | In another: the live view |
watch subscribes to a daemon and
re-renders on every task.changed push; the daemon pushes a notification on
every write. It takes a filter,
defaulting to the working set, and any filter narrows what it follows. Ctrl-C
stops it. It needs a running daemon and will never auto-spawn one — it hints
instead:
tasqx watch --socket nopetasqx watch: no daemon reachable at nope
hint: start one with `tasqx daemon` (add `--socket nope` to match)On a terminal it clears the screen and repaints the table. Through a pipe it streams one line per event instead — so it composes with everything else in your shell:
tasqx watch --socket tasqx-docsdemo | cat
# ... meanwhile, in another shell:
# tasqx --socket tasqx-docsdemo add "Wire up the docs page ..."
# tasqx --socket tasqx-docsdemo done 34 tasks · 1 overdue
ID URG TASK PROJECT DUE TAGS
1 H ▄▄▄▄ 15.7 Ship the v1 JSON API freeze work.tasqx Fri +api +release
3 - ▄▄▄▄ 12.0 Renew the TLS cert work.tasqx yesterday +ops
5 - ▄▄▃▁ 8.9 Water the plants home Fri
2 - ▄▄▂▁ 7.1 Write the user guide work.tasqx Mon +docs
task.changed op=add short_id=6
task.changed op=done short_id=3Leave it open on a second monitor while an agent works through your backlog — you see every task start, complete and unblock as it happens.
tasqx board
The same live view in a browser: a kanban with a column per state, updating as tasks change, where you move a task by dragging its card.
| Command | What it does |
|---|---|
tasqx board | Print the board's URL and open it in your browser |
tasqx board --no-open | Print the URL only |
tasqx board --port 8123 | Serve on a fixed port (or set board.port) |
tasqx board --scope read | Serve the same board with every change refused |
Like watch, board needs a running
daemon. Ctrl-C stops it.
The columns are the states tasqx already derives, each headed by the filter
that fills it: Triage status:triage (proposals), Backlog
status:backlog, Blocked @blocked (which never holds a proposal), Ready
@working, Active status:active (one at a time) and Done
completed.after:-7d. Inside a column cards are ordered by urgency, on the
same scale and in the same three bands as tasqx list; a ▶ marks the running
task and ⊘ a blocked one. A card opens (click, or Enter) into its checks,
blocked-by list and opening note. The search box filters by title, tag, project
and id, and the lanes menu groups a column by project or by priority. The page
follows your terminal theme, light or dark by your system setting, and shows
daemon · offline if the connection drops.
Keys: j/k move between cards, h/l between columns, Enter opens, /
searches, Esc closes, s starts the focused card and d completes it.
Moving a task. Drag a card to another column and the board sends the same command you would type:
| Drop on | From | What runs |
|---|---|---|
| Active | any | tasqx start (the running task stops, as it always does) |
| Done | any | tasqx done |
| Ready | Active | tasqx stop |
| Ready | Done | tasqx reopen |
| Ready | Backlog | clears its wait and scheduled dates |
| Backlog | Ready | waits a week (change the date with tasqx modify) |
| Blocked | — | refused: a task is blocked by its dependencies, not by a drag |
| any | Triage | refused: settle a proposal with tasqx accept, decline or duplicate; the board never starts one |
While you drag, a tray appears under the card: drop on H, M or L to set the priority, clear to remove it, or cancel to cancel the task. Within a column cards stay ordered by urgency; there is no manual order to drag into. Every drag has a button in the card's panel (click it, or Enter), which is also how you move a task on a touch screen.
A message at the bottom says what happened in the command's own words — which
task a start stopped and after how long, which tasks a completion unblocked —
with Undo for a stop, completion, cancel or priority/date change. Undo is
tasqx undo with one extra condition: it only takes back your drag if nothing
has been written since, so it can never undo someone else's change. A start or
reopen is undone by dragging the card back.
If something else changed the task after the board drew it — you, in a
terminal, or an agent — the drop is refused, nothing is written, and the board
says "another session changed it" and shows the task as it now is. Every change
the board makes is recorded with board as its actor, so tasqx api event.list tells a drag apart from a command or an agent.
Editing a card. The panel is a form too. Tick or untick a check, add a note,
change the due date, estimate, wait or scheduled date, set the priority, and add
or remove a dependency; each change is one command with the card's revision, so
a task changed elsewhere since is refused rather than overwritten, and the
footer always shows the revision as it now is. A note goes into the same search
as every annotation (tasqx memory search), so name files and symbols in it.
Adding or removing a dependency moves the card between Blocked and its
other column without a drag. Titles and project moves stay with the CLI. Undo
covers a note, a removed dependency and a field change; a tick or an added
dependency is changed back by hand.
--scope read serves the board for a wall screen or a standup: no drag, no
buttons, and the server refuses any change that is sent anyway.
Who can reach it. The board binds 127.0.0.1 and nothing else, so it is
never reachable from another machine. On a shared computer another user could
still connect to a loopback port, so the page is also behind a random secret
token: the URL tasqx board prints carries it once, the browser trades it for
a cookie, and every other request without that cookie is refused. Requests
from another website, or under a hostname other than 127.0.0.1 or
localhost, are refused too, which is what stops a web page you are browsing
from reading or changing your backlog through your own browser. The server
forwards only the handful of commands a drag needs, each carrying the revision
the card was drawn at. The board is single-user
and local. Put it behind a reverse proxy and authentication is the proxy's job.
Without a fixed port, each run takes a free port and a fresh token. With one
(--port, or board.port in the config) the token is kept in a file next to
config.toml, readable only by you, so a bookmark of the printed URL keeps
working after a restart.
The page is one self-contained file: nothing is fetched from the internet.
The daemon
The daemon is optional. It holds one database connection, serves the JSON API
over a local socket to many concurrent clients, pushes change notifications, and
runs the reminder scheduler. The CLI never requires it. The agents page covers
the rest of it, including the OTLP receiver:
tasqx daemon.
One-shot or daemon?
| Mode | When | Why |
|---|---|---|
| One-shot | The default. Scripts, cron, the HTML report. | No process to manage. Open the DB, run one command, exit. |
| Daemon | Long-lived clients: a TUI, a GUI, watch, reminders. | One writer, warm caches, live push, and something to fire reminders. |
If a daemon is reachable, one-shot commands route through it automatically —
single writer, live-update semantics for free. If not, they open the store
in-process. Same command surface either way, and a missing or stale socket
falls back immediately rather than hanging. --no-daemon forces the in-process
path.
$TASQX_DB always wins over a daemon on another store. Naming the file the
daemon serves, commands route through it as usual. Naming a different file, a
daemon found on the default socket is passed over and the command runs
in-process against $TASQX_DB; a daemon named by --socket or $TASQX_SOCK is
refused with exit 2, naming both files.
Four surfaces never route through a daemon: api, mcp serve, chart and
report --html open the store in-process, always. They refuse an explicit
--socket rather than ignore it, because a flag that names a daemon and
is silently discarded aims your write at a different store than the one you
asked for.
Socket addresses
Resolution order: --socket, then $TASQX_SOCK, then the platform default.
| Platform | Default |
|---|---|
| Windows | The named pipe tasqx-default |
| Linux | $XDG_RUNTIME_DIR/tasqx/tasqx.sock (falls back to the data dir) |
| macOS | <data dir>/tasqx.sock — macOS has no runtime dir |
Running it
Diagnostics go to stderr; the socket carries the newline-delimited JSON API.
Ctrl-C stops it cleanly, unwinding the accept loop and removing the socket file
(a no-op for Windows named pipes). --db points it at a specific store.
tasqx daemon --socket tasqx-docsdemotasqx daemon: listening on tasqx-docsdemo (Ctrl-C to stop)
tasqx daemon: store ~/.local/share/tasqx/tasks.dbIt stays until you stop it. Set [daemon] idle_timeout to a number of minutes
and it will instead exit by itself once that long has passed with no client
connected, no subscriber attached, no reminder about to ripen and no telemetry
posted to its OTLP receiver — and it says so on stderr on the way out. 0, the
default, means it never does. Turning the receiver on does not hold it open:
only exports that actually arrive count as work. An idle exit also leaves a note
beside config.toml: the first command that then fails to reach the daemon
reports the retirement and which store is no longer being served, because a
daemon leaving changes where the same command line writes.
Now route a command through it — note this is the ordinary add, unchanged:
tasqx --socket tasqx-docsdemo add "Wire up the docs page +docs project:work.tasqx due:tomorrow"▌ #6 Wire up the docs page
▌ added - ▄▄▄▃ 11.4 work.tasqx due tomorrow +docsReports and Charts
What happened, what's left, and where the time and tokens went — in the terminal, or as an HTML page you can send to someone.
tasqx report
Summary counts, optionally grouped.
| Command | What it does |
|---|---|
tasqx report | Totals |
tasqx report project | Grouped by project |
tasqx report status | Grouped by status (or priority) |
tasqx report +urgent | Any filter narrows the scope |
The same numbers as one self-contained HTML file:
tasqx report --html --out review.htmlThe HTML report is a single file with inline styling and no external requests — it works from a mail attachment, a chat upload, or a USB stick. A filter scopes both modes identically, so the page and the terminal table always answer the same question.
The page is a review of a period, the 7 days ending today unless you pick another:
tasqx report --html --since 2026-09-01 --until 2026-10-01 --out september.html| Section | What it answers |
|---|---|
| The band | Done, added, net backlog change, blocked and overdue in the period, each against the period before it, with a sparkline per 7 days and a one-line summary of the biggest change |
| Standup | Done yesterday (tracked against estimate, unproven completions flagged), in progress and for how long, blocked and by which task, the next five and why each is next |
| Projects | Open, done in the period and its change, median tracked ÷ estimate, tokens per completion and overdue, per project. Click a row and every section shows that project alone |
| Net flow | Added and done per 7 days since the store started, the net change, and the open backlog under it |
| Outcomes | How tracked time compared with the estimate, and what was reopened, cancelled or completed unproven — the same counts report --outcomes prints |
| Find a task | A search over every task in scope; a task opens in an overlay with its dependencies by title |
Annotation bodies are not put in the file, because a report is meant to be
shared and that is where client detail lives. --with-notes embeds the
newest three per task behind a toggle. A filter that names one project
(tasqx report project:acme --html) makes a page about that project alone,
and a task it depends on elsewhere shows as "a task outside this report".
--all adds cancelled tasks to the search. --metrics picks terminal
columns and is refused with --html, and the page is always grouped by
project, so tasqx report status --html is refused too.
The TOKENS column names a group's largest bucket with that bucket's own count
(cacheR 1.2M), or - when nothing was spent. The four buckets — in, out,
cacheR (cache read) and cacheW (cache creation) — are never blended into one
figure; --json and report --html carry the full split. They are filled by
self-reported counts on task.done (the primary source, via token.add — see
tasqx tokens), falling back to
parsing local AI-tool transcripts when tokens.enabled = true, or by the
daemon's OTLP receiver when otlp.enabled = true
(tasqx daemon).
Grouping and what counts
tasqx report [group_by] [filter] [--all] — group by project (default),
status, or priority. Estimates total as ISO-8601 durations, which is why
est: is parsed at the edge rather
than stored as opaque text.
What counts. A report is an aggregation, so it leaves cancelled tasks out
— tasqx has no hard delete, and without this every task you ever threw away
would inflate your totals forever. Done tasks still count: completed work is
real work, and it carries nearly all your tracked time. Two ways to override
that: pass --all to count everything including cancelled, or name a status in
the filter — tasqx report status:cancelled means what it says and is taken
literally.
tasqx reportPROJECT COUNT EST OVERDUE TRACKED TOKENS
home 1 PT0S 1 PT0S -
work.tasqx 3 PT5H30M 1 PT0S cacheR 1.2Mtasqx report statusSTATUS COUNT EST OVERDUE TRACKED TOKENS
pending 4 PT5H30M 2 PT0S cacheR 1.2MThe HTML report
tasqx report --html emits a weekly review as one self-contained file — inline
CSS, inline SVG charts, no external requests — themed from the same palette as
your terminal. Exactly like the tasqx docs guide.
tasqx report --html --out review.htmlWrote self-contained HTML report → review.htmlWithout --out it writes to stdout. One small inline script does the
clicking; the page reads fully without it.
Both HTML surfaces — this report and the tasqx docs guide — hold
the same line: no CDN, no web fonts, no
remote images, no scripts fetched from anywhere. Mail the file, commit it,
open it on a plane — it renders the same.
tasqx report --outcomes
tasqx report says what the work cost. --outcomes says how it went.
tasqx report --outcomesPROJECT CLOSED DONE REWORK FORCED SILENT CALIB DROPPED OVER UNPROVEN UNBRIEFED TOKENS api 7 6 0/6 0/6 0/6 ×1.17 n3 1/7 1/2 - 6/6 cacheR 7.0M ~medium infra 9 8 0/8 0/8 1/8 ×1.00 n4 1/9 1/1 1/2 8/8 cacheR 2.8M ~medium mobile 15 13 2/13 0/13 0/13 ×1.35 n2 2/15 1/3 1/3 13/13 cacheR 7.7M ~medium website 15 13 0/13 1/13 2/13 ×1.58 n9 2/15 0/2 0/5 13/13 cacheR 2.4M ~medium REWORK / FORCED / SILENT / DROPPED / OVER / UNPROVEN / UNBRIEFED read count over the completions or closings they were counted against — a rate with no denominator beside it is not a rate. OVER counts only the completions that had a budget and UNPROVEN only those that had acceptance criteria. UNBRIEFED counts completions of a task no brief or memory-carrying start was ever read for. CALIB is the median tracked-over-estimate ratio and the number of completions carrying both figures.
| Column | What it counts |
|---|---|
| REWORK | Completions that were later reopened |
| FORCED | Completions that overrode open blockers |
| SILENT | Completions carrying no annotation |
| UNBRIEFED | Completions of a task with no tasqx brief, and no tasqx_start_timer that carried memory, on record |
| CALIB | Median tracked-over-estimate, and how many completions had both |
| DROPPED | Work that was started and then cancelled |
| OVER | Completions that went past their token budget, of those that had one |
| UNPROVEN | Completions with a check still open or failed, of those that had checks |
| TOKENS | The largest token bucket, as everywhere else |
Every rate reads count/n rather than a percentage, deliberately: "3/12" and
"3/3" are the same percentage and very different news, and a rate over three
completions is not evidence of anything. Check the denominator before you act
on the number.
The scope is tasks that closed, by the date they closed — so a completion
that was later reopened still counts, which is the whole point of the REWORK
column. Open work is invisible here; plain tasqx report is the view for work
in flight. Group and filter as usual (tasqx report --outcomes project,
tasqx report --outcomes +api), and window with --since/--until.
tasqx chart
Charts drawn right in the terminal, from the event log. A bare tasqx chart
draws throughput.
| Command | What it does |
|---|---|
tasqx chart throughput | Tasks added vs done, per week |
tasqx chart heatmap | GitHub-style activity calendar |
tasqx chart burndown | Open tasks over the last N days |
- throughput answers "am I finishing as fast as I'm adding?"
- heatmap answers "when do I actually get things done?"
- burndown answers "is the pile shrinking?" (
--days 30widens the window)
Each takes the same filter DSL list/report/agenda do, so a chart can be
scoped to one slice of the store:
tasqx chart burndown project:workonly counts work's own tasks and events — the whole store otherwise.
tasqx chart burndown --project work is shorthand for the same thing;
project:work on the filter is the more general form, since it combines with
a second predicate (tasqx chart burndown project:work +urgent).
The charts degrade cleanly from truecolor terminals down to no color at all.
Chart samples
All three read the append-only event log, so they are history, not a snapshot — and they are pure reads that never touch your tasks.
tasqx chart throughput --weeks 12Weekly throughput added [#] done [#]
W27 added 0 done 0 net 0
W28 added 0 done 0 net 0
W29 added ########## 5 done ## 1 net +4
> 4-wk velocity 0.2 done/wk - WIP trending uptasqx chart heatmap --weeks 4Completions - last 4 weeks . 0 : 1-2 + 3-4 # 5+
Mon . . . .
. . . .
Wed . . . .
. . . :
Fri . . . .
. . . .
Sun . . . .
> 1 done - current streak 1 days - best 1tasqx chart burndown --days 7Remaining open - all tasks
4 ______#
0 2026-07-10 -> 2026-07-16
> 4 left - up 4 over 7 days| Chart | Flags |
|---|---|
throughput | --weeks <n> (default 12) |
heatmap | --weeks <n> (default 12), --year (52 weeks) |
burndown | --days <n> (default 30), --project <p> |
tasqx why
Not a report, but the same spirit: tasqx why <ref> breaks a task's urgency
score into its components, so the ordering of every list stays explainable.
See Finding Tasks.
Memory
tasqx can remember more than tasks. The memory system stores knowledge — runbooks, decisions, conventions, research — and makes it searchable together with every task annotation you've ever written. For AI agents this is the difference between starting cold every session and starting informed.
tasqx memory add
Store one knowledge document. The body is kept exactly as given.
tasqx memory add "Deploy runbook" "Deploys go through the blue-green pipeline"--standingmarks the doc as standing: a ruling that belongs in every session of its project (or every project, when unscoped) until it is cleared, so a correction given once is not forgotten by recency. MCP clients receive standing docs when a session starts. Past 15 standing docs in one scope,addanswers with a hint to merge or retract.--sourcenames where the doc came from, and names one doc: a source another document already holds is refused withconflict(exit 5), naming that document, soimportalways knows which one to replace. The same goes forupdate --source.
When a doc of the same project, or an unscoped one, already reads like the
new doc, add still stores it and prints a note: line naming that doc, up
to three of them, so you can update or remove one instead of keeping both.
--json carries them as similar, an empty list when there are none.
tasqx memory list
Browse your documents without a query — the enumeration search cannot do,
since ranking needs words to rank.
tasqx memory list- On a terminal this opens a full-screen browser:
j/kmove,/searches as you type, Enter reads the document beside the list,qleaves. - Piped, under
--json, or with--limit/--offset, it prints one line per document instead — a screen would be the wrong answer to a pipe. - Newest-modified first, paged the same way as
tasqx list. --standinglists only the standing docs (and prints the table).
tasqx memory search
Search over your documents and your task annotations, by meaning as well as
by words: a note that says "login errors" is found by sign-in failures. An
embedding model built into the binary ranks entries by meaning beside full-text
search (bm25), the two lists are fused, and each hit says which found it.
tasqx memory search blue-green- Plain words are matched as phrases, so hyphens and dots are safe to type.
- An entry with every word ranks above one with only some of them (filler words
like "the" aside); a hit with only some is marked
some words, so a partial match is never read as a full one. - A hit found by meaning alone is marked
≈with its similarity on the line under it; its passage is the one closest to your words. - An identifier such as
D41or#607has no word to match by meaning, so it is looked up by its characters alone.--mode lexicaldoes that for any query: exact words only, the way search worked before meaning was added.--mode semanticis meaning only. --min-similarity(0 to 1, default 0.30) is how close in meaning a hit must be. Lower finds more distant paraphrases, and more noise.- The model reads English; text in another language is found mostly where the query shares its words.
--scope docsor--scope annotationssearches only one half (defaultall).--source-prefix myrepo/docs/adr/keeps only the docs whose source starts with that text (literal, case-sensitive) and leaves annotations out. A doc imported from a git checkout is sourced under the repository's directory name.- Power users can pass
--rawfor FTS5 operator syntax (words only). - The answer includes the query that actually ran, so "no hits" is distinguishable from "nothing stored about this".
- A doc hit whose origin file has changed since it was imported (or last
refreshed) carries the word
staleon its line, inmemory searchand in the FROM MEMORY section ofbrief, so you can tell a ruling that is behind its file from one that still matches it.--jsonhas it as"stale": true.memory listandmemory showdo not check the file.
tasqx memory show
Read one document whole, by the id a search hit gave you.
tasqx memory show 019f8422-7b3e-7c41-a2d9-6f1b0e5c8a12tasqx memory update
Correct a document in place, keeping its id.
tasqx memory update 019f8422-7b3e-7c41-a2d9-6f1b0e5c8a12 --body "corrected text"--title,--body,--source,--projectand--standing true|falseeach replace that field; omit one and it stays as it was.- Guarded by the same optimistic-concurrency rev
tasqx modifyuses:--expected-revfails withconflict(exit 5) if the document moved under you since you read it. - This is the correction path, and the reason
rmis rarely the right verb: adding a fixed copy leaves the wrong one searchable, and removing loses the id every search hit and annotation pointed at.
tasqx memory import
Turn a folder of markdown into searchable memory — one document per file, the
title taken from the first # heading.
tasqx memory import docs/adr- One transaction: if any file fails, nothing is imported.
- Re-importing the same directory replaces those documents instead of
duplicating them, so it's safe to re-run whenever the sources change.
Every document a re-import replaced is named under the summary line, with
the title it had (
replaced_docsunder--json). A document whose title and text are exactly what is stored is left alone — same revision, no event, not counted as replaced — and the summary reports it asunchanged(2 replaced, 3 unchanged;unchangedunder--json). Over the JSON API, a batch that names one source twice is refused whole. - A document whose recorded file still exists and is not the file being
imported under its source — two clones under the same directory name — is
never replaced: the whole import is refused with
conflict(exit 5), naming the document and both files. A worktree and its main checkout are one repository, not two clones, so importing from either replaces the same documents. A recorded file that cannot be checked at all (a permission error) refuses the import too, with exit 2, rather than being taken for gone. - A replace bumps the document's revision, so an
update --expected-revtaken before the re-import is refused withconflictinstead of silently overwriting the freshly imported text. --projectscopes every document in the batch, so an agent's own repo docs land in that project's memory instead of unscoped. Omitted, a new document stays global and an existing one keeps whatever scope it already had (like--standingon a re-import); naming a project moves an existing document's scope there, so re-pointing an import at a different project rescopes it.- The stored
sourceis the git toplevel's directory name followed by the file's path inside the repo —clouter/README.md— or, outside a git work tree, the current directory's name followed by the path under it. A worktree uses its main checkout's name, so it replaces the same documents. The same folder imported asdocs/,./docs/or its absolute path, from any directory, is one document per file, not several, and two repos'README.mdare two documents, not one. Across machines that holds for checkouts with the same directory name: independent clones under one name share keys and conflict, while differently named checkouts get distinct keys. Documents imported before this keep their old, unprefixed source; the next import of the same file lands beside them. An import that finds a doc already holding an older spelling of the same file never removes it, but prints anote:line naming it so it can be retired by hand — as the same file when that doc recorded which file it came from, and only as a possibility when it did not. A backslash folds to/in that comparison only on Windows; on unixdocs/a\b.mdis its own file name, never a spelling ofdocs/a/b.md. A symlink pointing at another file in the same import collapses onto it too — samesourceonce resolved — so only the real file becomes a document, and the alias is named in anote:line instead of refusing the batch. tasqx memory import --refreshtakes no path. It walks the documents already imported, compares each against the file it was read from, and re-reads the ones whose file has changed — keeping the document's id, its project and its standing flag, and bumping its revision. A file that was only touched, without its text changing, counts as unchanged: the stored size and timestamp are a fast filter, and the bytes are what decides. A document whose file can no longer be read is named and kept, never deleted — the file may be on another machine or a volume that is not mounted — and the command still exits 0, so a refresh in a script does not fail over a file somebody moved. An MCP session runs this same sweep on its owninitializewhen its scope allows writes, so an agent's session opens on the working tree without a watcher or a daemon job.- Each imported document also records where it came from:
origin_path(the file's absolute path on this machine),origin_mtime(its modification time in unix seconds) andorigin_size(its size in bytes), all shown bymemory show --jsonand carried byexport. They say what the file looked like when it was read, so a later check can tell whether it has changed since; identity is stillsource, andmemory addnever sets them.
Put together, this is one model, not three separate features: memory
remembers which file a document came from, and checks that file only when
you ask — a search, a brief, --refresh, or an agent session starting — never
by watching the filesystem in the background. Nothing is ever deleted because
a file moved or disappeared; it's named so you can decide what to do about it.
tasqx memory verify
Check the code that documents talk about, not the file they came from.
tasqx memory verify
tasqx memory verify 019f8422-7b3e-7c41-a2d9-6f1b0e5c8a12A document cites code through its source (a <repo>/<path> key) and every
backticked path, path:line or path::symbol in its body. Run from inside
a git checkout, verify checks those against the checkout's working tree:
each path must exist, and each symbol must appear as a whole word in the file
it names. A path in the body is checked only when its first folder or file
exists at the top of the checkout, so a branch name or a path written
relative to a subfolder is skipped rather than reported missing. Each
document gets one status:
ok: every cited path and symbol is there.missing_path: a cited file or directory is gone, for example renamed.missing_symbol: the files are there, but a cited function or type is not.unverifiable: nothing could be checked. The document cites no code, its source or its project names a different repository, it is a global document whose source names no repository, or you are not in a git checkout. This is never reported asok.
The text output lists the stale documents with the references that are gone,
then a count per status; --json lists every document. --project checks
one project's documents. The command exits 0 even when documents are stale,
because it is a report. Nothing is indexed: it is a file check and a text
search, run when you ask for it.
A symbol is looked for only in a regular file of at most 4 MB; anything else
counts as present. A search hit and memory show --json carry the same check
as stale_refs, run in the directory you call them from,
and an agent's session start marks a standing ruling whose cited code is gone.
Run verify after a rename or a large refactor, and correct what it names
with tasqx memory update.
tasqx memory rm
Remove one document, permanently, by id.
tasqx memory rm 019f8422-7b3e-7c41-a2d9-6f1b0e5c8a12This is a permanent delete, like tasqx unannotate and removing a note, and
it exists on purpose: something stored wrong needs a way to be retracted. It is
outside undo.
Reach for it only when the document should not exist at all. To fix what one
says, tasqx memory update rewrites it in place and
keeps the id — no permanent loss, and nothing stale left in the index.
Why this matters for AI agents
An agent connected over MCP reaches this same
store — searching works even on a read-only connection. Feed it your ADRs and
runbooks with memory import, and past decisions surface while the agent
works. And because annotations are indexed too, an agent that documents its
work as it completes tasks is building the knowledge base as a side effect.
Notes
A note is for the thing you want to write down before it gets away: an idea,
a link to read later, a question for tomorrow. It is not a task. It has no
due date and no urgency, it stays out of tasqx list, next, the dashboard's
panels and the reports, and memory search never sees it. Write it in a second and
forget it safely.
When a note turns out to matter, promote it: it becomes a task, or a memory doc, and the note is archived with a pointer to what it became.
tasqx note
Jot one down. Every word after note is the note, kept exactly as you typed
it.
tasqx note try the new tracing exporter
tasqx note https://www.rfc-editor.org/rfc/rfc9457 --project api--projectfiles it under a project. Without it the note has none. There is noproject:shortcut asaddhas: a note's words are its own.- Quote the text when it holds characters your shell reads (
#,*,?,&). - Notes have their own numbers. Note 3 and task 3 are different things.
- Put
--before text that is a number or starts with-, so it is not read as a note's number or a flag:tasqx note -- -5 degrees tonight.
Read one whole, with its project and the day you added it:
tasqx note 3Change one by naming it first and saying what to do after:
tasqx note 3 edit try the OTLP exporter instead
tasqx note 3 archive
tasqx note 3 unarchive
tasqx note 3 rmeditreplaces the text.archiveputs a note out of sight; it stays undertasqx notes --archived.unarchivebrings it back.rmdeletes it for good.tasqx undodoes not cover notes, so archive when you are not sure.
The number always comes first. Any other first word is part of the note, so
tasqx note edit the readme writes down "edit the readme". A subverb on its
own, or followed by a number (tasqx note archive 7), reads as a command typed
the wrong way round: it is refused (exit 2) with the right spelling, and
tasqx note -- archive 7 stores it as text.
note used to be another name for annotate. A number followed by text is
now refused (exit 2) and names the command you probably meant:
tasqx annotate 42 "called the plumber, waiting on a quote"tasqx notes
List your notes, newest first.
tasqx notes2 notes ID NOTE PROJECT ADDED 2 idea: a dark-mode toggle in the docs header yesterday 1 read https://www.rfc-editor.org/rfc/rfc9457 before the error-format chan… ↗ api 2d ago
Each line has the note's number, its first line, ↗ (L without Unicode)
when it holds a link, its project and the day you added it. A note that is
only a link is never cut short, so you can still copy it; its line runs past
the edge of the terminal instead.
--linksshows only notes holding anhttp://orhttps://link.--archivedshows the archived ones, promoted notes among them.--allshows open and archived together.--search <text>keeps notes containing that text, ignoring case. It is a plain match, not the ranked searchmemory searchdoes.--project <name>keeps the notes filed under that project.
tasqx notes --links
tasqx notes --search exporter --allThe notes browser
On a terminal, tasqx notes opens a full-screen browser over the same
notes, and the flags above decide where it opens: --search becomes the
search you can edit, --archived and --all choose which notes it holds.
Piped, or with --json, it prints the table.
| Key | What it does |
|---|---|
j / k | move down / up (the arrows too) |
g / G | first / last note |
/ | search the notes' text, every word you type |
| Enter | read the note whole; a link is shown in full and never opened |
| Esc | clear the search; with none, leave (in a note, back to the list) |
q | leave (in a note, back to the list) |
a | archive the note, or bring an archived one back |
t | promote it to a task |
m | promote it to a memory doc |
x | remove it for good, after you answer y |
l | show only the notes holding a link, or all of them again |
Each change says what it did on the line above the keys, and a refusal is
shown there too: a note someone changed after the browser read it is
refused rather than overwritten, and the list is read again. A promoted note
does not offer t and m. From the dashboard,
N opens the same browser, and closing it takes you back.
Promote a note
When a note is worth doing, make it a task:
tasqx note 3 promote task
tasqx note 3 promote task --title "Evaluate the OTLP exporter"The task lands in the note's project, or your default project when it has
none. Its title is --title, or the note's first line cut at 80 characters;
when that title does not say everything, the whole note becomes the task's
first annotation. The echo is the same card tasqx add prints.
When it is something to remember rather than something to do, make it a memory doc:
tasqx note 3 promote memory --title "Tracing exporters we tried"Either way the note is archived and remembers what it became. That happens in one step: if the task or doc cannot be made, the note is left as it was. Promotion is final, so a promoted note cannot be promoted again or unarchived. Cancelling the task later does not bring the note back; to capture the idea again, write a new note.
Notes, tasks, memory and triage
| Question | Note | Task | Triage proposal | Memory doc |
|---|---|---|---|---|
| For | a thought you might act on | work you will do | work someone suggests | knowledge to keep |
Urgency, @working, reports | no | yes | not until accepted | no |
Found by memory search | no | its annotations | its annotations | yes |
| Becomes something else | promote | — | accept | — |
Use a note when you are not yet sure. A task asks to be done; a proposal in triage is a task waiting for your yes; a memory doc is something an agent should read before it works. A note asks for nothing.
Every command here takes --json and prints the API's answer. Agents reach
notes through the MCP tools tasqx_add_note, tasqx_list_notes,
tasqx_get_note (one note; a body over max_body_bytes is cut and marked body_truncated, so raise it to read all), tasqx_update_note, tasqx_remove_note and tasqx_promote_note.
AI Agents and Automation
tasqx treats an AI agent as a normal user. There's one JSON API underneath everything; the CLI is one client of it, and the built-in MCP server is another. An agent gets the same data, the same rules and the same safety properties you do.
tasqx setup
Connects tasqx to Claude Code, Codex CLI and Gemini CLI in one step. For each
tool it finds, it registers the MCP server (user level, write access) and
installs the tasqx-workflow and retro skills. A tool appears
only when claude, codex or gemini is on your PATH; with none of them, setup
says how to install one.
tasqx setupIt's a checklist screen — Space toggles an item, Enter installs what's ticked, q quits. Run it again later and it shows what's already in place. Without a terminal it prints that list and exits.
tasqx setup --list: what is installed, per itemtasqx setup --yes: install everything not yet present, no screentasqx setup --only retro: limit to one item (mcp,tasqx-workflow,retro); repeatable. Codex and Gemini items carry the tool's name:codex:mcp,gemini:retrotasqx setup --yes --force: also overwrite a skill file that differs from the one tasqx carries
Each tool is registered through its own command (claude mcp add,
codex mcp add, gemini mcp add); tasqx never edits their config files. The
skills land in ~/.claude/skills, ~/.codex/skills and ~/.gemini/skills.
The skill text is compiled into tasqx, so an upgrade brings new skill text
with it. A skill file you edited is kept unless you pass --force or tick it
on the screen.
tasqx mcp
The built-in MCP (https://modelcontextprotocol.io) server — the standard way
AI tools like Claude Code connect to external systems.
tasqx mcp serve: read-only by defaulttasqx mcp serve --scope write: explicit write access
tasqx mcp serve
tasqx mcp serve --scope writeTo wire it into Claude Code by hand, without the skills that
tasqx setup adds:
claude mcp add --scope user tasqx -- tasqx mcp serve --scope writeBy hand for the other two, the commands tasqx setup runs:
codex mcp add tasqx -- tasqx mcp serve --scope write
gemini mcp add --scope user tasqx tasqx -- mcp serve --scope writeAny other MCP client takes the same shape in its config:
{
"mcpServers": {
"tasqx": { "command": "tasqx", "args": ["mcp", "serve", "--scope", "write"] }
}
}
What the agent can do: list and read tasks, add and modify them, complete them (and learn what that unblocked), start and stop timers, tag, annotate, wire up dependencies, create projects, and search and store memory.
mcp serve works on the store itself, not through a running daemon, so it
ignores $TASQX_SOCK (and says so on stderr). That does not hide an agent's
writes from tasqx watch or the desktop app: a running daemon notices changes
made by other processes on its store, so they show up live, within about half
a second.
Forty-three tools, one verb each. Twelve reads: list_tasks, get_task,
brief_task, next, summary, outcomes, list_projects, search_memory,
get_memory, list_memory, list_notes, get_note.
Thirty-one writes: add_task, modify_task, complete_task, reopen_task,
cancel_task, triage_task, start_timer, stop_timer, claim_task, release_task, adjust_tracked, tag_task, untag_task,
annotate_task, update_annotation, move_annotation, remove_annotation, add_tokens, add_check, set_check,
remove_check, add_dependency, remove_dependency, add_memory,
update_memory, remove_memory, add_note, update_note, remove_note,
promote_note, create_project (all prefixed tasqx_).
What makes this more than remote CRUD:
- One call before starting, instead of five.
brief_taskreturns the task, each prerequisite with what that task concluded, what this one blocks, and relevant memory — under a query tasqx derives from the task's own title, tags and project. The agent supplies no search terms, which matters because a guessed term that finds nothing looks exactly like a store with nothing in it. - "What now" and "where was I" in one call.
nextistasqx nextover MCP: the most urgent unblocked task, scoped byprojectandfilter, with the first paragraph of its opening note. Withresume: trueit also lists the active tasks (and who holds each clock) and the five open tasks annotated most recently, each with the start of its newest note and when it was written. Likeget_task, it answers the rendered view, andinclude_json: trueadds the machine block. - A task with its criteria and prerequisites, in one call.
add_tasktakeschecks(strings) anddepends_on(short ids or UUIDs) and creates them with the task, all or nothing: an unknown prerequisite or a blank check refuses the whole add and creates nothing. - Completing a task returns what it unblocked, so an agent can decompose a
feature into a dependency chain with
add_dependencyand then walk it, picking up each task the moment its prerequisites clear. - "Done" can mean something. Acceptance criteria are rows with a state and a
citation (
add_check,set_check), not prose in an annotation that nothing can ask about. tasqx never runs a check — the criterion is a claim and the evidence is text it stores verbatim. Completing with one still open is not refused; it is counted. - The agent can read its own record.
outcomesis on the read scope, so even a read-only session can ask what its rework rate on this project is before deciding how carefully to work. - Agents get long-term memory.
search_memorygives even a read-only agent retrieval by meaning and by words over your imported docs and every task annotation — feed it your ADRs withtasqx memory import docs/, and past decisions surface while it works. Annotations feed the same index, so an agent that documents its work is building the knowledge base as a side effect. - Token spend lands on the task.
complete_tasktakes token counts plus who spent them — ortotal_tokens, one unsplit number, when that is all the harness reports — andadd_tokensrecords a count that arrives after completion; a log-parse fallback fills gaps and refuses contested samples rather than guess. The table, the dashboard and the HTML report all show what each task cost.
Safety properties worth knowing:
- Read-only sessions can't see the write tools at all — an agent can't call what it isn't offered. (Scope configures the local process; it's not authentication.)
- There's no bulk-delete tool, on purpose. Cancelling goes through the same reversible, logged path as everything else, so an agent can't quietly destroy a week of work.
- The permanent deletes (
remove_memory,remove_annotation,remove_note) each say so in their description, so the host's confirmation gate applies. initializeanswers withinstructions, a scope-aware workflow the host may inject into the agent's system prompt; under read-only scope it names no write tool. It also carries the session's standing memory docs, for the project named by the directorymcp serveruns in (or one of its parents), else the default project, within a 3 KB budget.- A write-scoped
initializealso refreshes imported docs from disk before it answers, so a session opens on the working tree with no watcher and no daemon job needed; a read-only session skips the sweep, andinstructionsnames how many docs were refreshed or went missing only when that count is not zero.
tasqx_get_task and tasqx_brief_task take a view argument, "markdown"
by default. Pass view: "card" when a person has to decide on the task — one
the agent proposes adding, or one they asked about: the rendered block becomes
a fixed-width, box-drawn card meant to be pasted whole into a chat reply or a
document, instead of the usual prose. The same card is available from a
shell: tasqx show 42 --card prints the identical bytes.
Routine events do not need a card. Starting or completing a task is one line
the agent writes from what it already read — the brief and the completion's
unblocked list:
▶ #711 Add unc output style · M · 20m · 0/2 checks
✔ #711 done · 2/2 checks · unblocked #712
A subagent handed a task runs tasqx show <id> --card itself instead of
receiving the card pasted into its brief.
Both tools answer the rendered view and nothing else. The machine-readable
JSON block is the same result a second time, so it is sent only when you ask
for it with include_json: true — worth doing when a script parses the
answer, and a waste of the response budget when an agent is going to read it.
A few more places the response is shaped for the reader that is actually paying for it:
tasqx_list_tasksanswers a compact row by default —short_id,title,status,priority,urgency,blocked,due,project,tags— with any null key omitted; an explicitfieldslist,[]included, returns the engine's own full row.tasqx_complete_taskrefuses a task with open blockers and names them;force: truecompletes it anyway, and the override is recorded and counted intasqx_outcomes. Passview: "card"when a person has to decide on the outcome: the answer then leads with the box card of the task as completed, so notasqx_get_taskre-read is needed. A routine completion leaves it out and prints the one✔line above.- A long annotation body is cut in a
tasqx_get_task/tasqx_brief_taskresponse with a marker naming its real size;max_body_bytesraises the cap, and a removed annotation is listed as a tombstone rather than disappearing from the count. tasqx_brief_taskholds half its memory page for knowledge docs (frommemory add/memory import), so a ruling is not buried under a project's own, more numerous, task annotations.tasqx_brief_taskpages the task's annotations the waytasqx_get_taskdoes: the newest 20 by default,annotations_limitandannotations_offsetto choose another page. A brief over the response budget drops memory hits first, then older annotations, and the view names theannotations_offsetthat reads the rest.
Why the tool descriptions are short
Every tool description states its contract — what the call does, what it
returns, and the one rule you need to call it right — and cites a D-number
when a design decision is behind that rule. The reasoning is in DESIGN.md
§12 under that number, and tasqx_search_memory finds it once the repository's
docs have been imported with tasqx memory import. The descriptions are short
because most clients inject the whole roster into every request, a few
thousand tokens on every prompt.
A few facts the short descriptions no longer spell out, that you may still want to act on:
annotations_limitandinclude_jsonspend the same budget. Atasqx_get_taskresponse is fitted to a byte budget, and the machine-readable JSON block is charged first. A big annotation page together withinclude_json: truetherefore buys less history than the page you asked for.budget_tokensstops nothing. tasqx is called between turns and cannot interrupt one. It counts fresh tokens only — input, output and cache creation, never cache reads — so a task that blows its budget is a signal it was too large to hand over whole, not an error.tasqx_start_timertakesactorandkeep. Each MCP connection gets its own actor id, and starting a task while a different actor holds a running clock is refused rather than silently stopping their timer. Passkeep: trueto run both deliberately — which is what several agents working in parallel need.tasqx_claim_taskis how parallel agents stay off each other's work. A claim is a lease, 30 minutes unlessleasenames another length and at most 24 hours, held by the connection (or theactoryou name). While it is live the task leaves every other connection's@workingandtasqx_next, theirtasqx_stop_timer,tasqx_complete_taskandtasqx_cancel_taskon it needforce_claim, and theirtasqx_start_timeron it is refused naming the holder and the expiry. Claim again to renew;tasqx_release_task, completing or cancelling gives it up; an expired lease is anyone's. Claim before you start when more than one agent works the same store. The connection id changes when the server restarts, so an agent that may reconnect should pass its own stableactor(its session id) to claim, start, complete, cancel and release; a claim stranded by a dead session is freed byforce_claim: trueontasqx_release_taskor by its lease running out.tasqx_remove_annotationredacts the event too. It overwrites the body in the annotation row and in the original write's audit event, in one transaction, so a secret pasted into a note is also gone from an export.tasqx undocannot bring it back.tasqx_update_annotationcorrects a note in place. The id, timestamp and position stay, so a corrected first note is still the card's Description, and memory search stops finding the old sentence.tasqx undoputs the previous text back.tasqx_move_annotationmoves a note to another task. For a note written on the wrong task: the id, text and timestamp are kept, search follows, andtasqx undoputs it back.tasqx_update_annotationandtasqx_remove_annotationtake the note's full id, a unique prefix of eight or more characters, or its 1-based position on the task.- Proposals can wait in triage.
tasqx_add_taskwithstatus: "triage"files a task outside@working,nextand the urgency ranking, so work an agent discovers mid-task does not compete with the planned backlog;discovered_fromlinks it to the task it was found on. Settasqx config set mcp.add_status triageand every MCP add that names no status lands there.tasqx_triage_task(ortasqx accept,decline,duplicateat a shell) settles each one;tasqx triagelists the inbox with the tool that filed each proposal.
The server's instructions block is the short version of how to work, so a
fresh install already nudges the agent to search before deciding and to write
as it works. The skill in
.claude/skills/tasqx-workflow/
— tasqx setup installs it for you, or Claude Code picks it up automatically
inside the tasqx repository — is the fuller version.
For the full workflow — what deserves a backlog entry, searching memory before starting, annotating before completing — see the AI agent guide and the agent starter prompt. For the session-end retrospective that has the agent record what each task taught, see A self-improving agent.
tasqx api
The JSON API without a server: one request envelope in on stdin, one response out on stdout.
echo '{"tasqx":"1","method":"task.list","params":{"filter":"@working"}}' | tasqx apiMethod and params go in; a result or an error comes out. Every method the
engine has is callable this way — tasqx docs carries the full method table. Exit codes mirror the error model: 0 ok, 2 bad request,
4 not found, 5 conflict, 6 unsupported API version — plus 1 for
internal, or for a failure beneath the request such as a store that would
not open.
token.add self-reports a turn's token counts against a task — the primary
source the TOKENS column reads (tasqx tokens). And
tasqx export / tasqx import round-trip the store as
canonical JSON.
tasqx daemon
A long-lived server on a local socket (or named pipe on Windows). You don't need it for everyday use — every command works standalone — but it adds three things:
tasqx daemon- One writer, many clients. While it runs, ordinary commands automatically route through it, so several agents and terminals can hammer the same store without stepping on each other.
- Live pushes. Every change is announced to subscribers — that's what
tasqx watchlistens to. - Reminders fire. The daemon is the process that watches the clock; see Dates, Reminders and Recurrence.
Ctrl-C stops it cleanly. --no-daemon on any command skips the routing when
you need a command to run strictly in-process.
$TASQX_DB always wins over a daemon on another store. If it names the file
the daemon serves, commands route through the daemon as usual. If it names a
different file, a daemon found on the default socket is passed over and the
command runs in-process against $TASQX_DB. A daemon you named yourself, with
--socket or $TASQX_SOCK, that serves a different file is a contradiction:
the command is refused with exit 2, naming both files.
otlp.enabled = true (in config.toml) starts a local OTLP/HTTP receiver in
the daemon on 127.0.0.1 (otlp.port, default 4318), capturing an AI tool's own
token telemetry live instead of parsing its transcript after the fact. For
Claude Code:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318Gemini CLI takes the same two OTEL_EXPORTER_OTLP_* variables, and Codex the
[otel] table in ~/.codex/config.toml. The receiver is independent of
tokens.enabled: self-reported counts (task.done, token.add) work with
both settings off. The
token accounting guide has
the rest.
tasqx tokens
Maintenance for token accounting — the feature that answers "what did agent work on this backlog actually cost?"
Agents self-report token counts when completing a task (that's the primary channel — only the agent knows which task a conversation served). A log-parsing fallback fills gaps by reading session transcripts, and it refuses to guess: a sample claimed by two tasks' time windows is dropped rather than attributed to the wrong one.
| Command | What it does |
|---|---|
tasqx tokens add 602 --total 37898 | Record a count that arrived after completion, as one unsplit number |
tasqx tokens add 602 --in 18400 --out 2600 | The same, split into buckets |
tasqx tokens recompute | Dry run: shows what would change, writes nothing |
tasqx tokens recompute --apply | Actually rewrite the log-parse attributions |
Stop any running daemon before --apply — this one runs strictly in-process.
Where the numbers surface: the task table's TOKENS column, the dashboard's
TOKENS panel, and the HTML report's header tiles — always as separate buckets
(input, output, cache read, cache creation), never a misleading blended total.
A count reported as one number stays one number, shown as total (unsplit).
What each confidence grade means, and why a self-report is medium, is in the
token accounting guide.
Token accounting
When an agent (or script) drives the CLI instead of MCP, the same reporting
travels as flags on start and done:
tasqx start 42 --client 'claude-code 2.1' --session-id $SID
tasqx done 42 --client 'claude-code 2.1' --session-id $SID--client selects the transcript parser; without these flags no transcript is
ever read and the task simply reports zero tokens.
One rule for anything automated
A bare tasqx on what looks like an interactive terminal opens the
full-screen dashboard and waits
for a key. Harnesses that allocate a pty look interactive. So in scripts and
agent tooling, always spell the verb: tasqx list, never bare tasqx.
Import and Export
Your data, in and out, as plain JSON. This is the backup story, the migration story, and the "way back" for anything archived.
Your data is yours. export emits canonical JSON — stable UUIDs, every field,
sorted keys — that is git-diffable, greppable, and round-trips exactly.
tasqx export
Dump tasks as canonical JSON to stdout, or to a file with --out.
tasqx export project:work > work.json: any filter narrows ittasqx export --out backup.json: writes the same document to a file, replacing it whole, so an export that fails halfway leaves the previous backup as it was, and prints where it went and what it left out
tasqx export > backup.json
tasqx export project:work > work.json
tasqx export --out backup.json- The document carries projects, your memory docs, your quick-capture notes, the graph's links and your default-project setting too, so a restore gives back the store, not just its tasks.
- A filtered export that cuts across a dependency (one task in, its
prerequisite out) drops that edge and says so in the answer
(
dropped_dependencies), rather than exporting a broken reference. A link (link.addin the API) is kept only when both of its ends are in the document, and the rest are counted underdropped_linksfor the same reason. - An unfiltered export carries every project, every memory doc, every link
and the event log — the full backup. What it leaves out is what you
removed, and each kind is counted (see Checking a restore).
The one link it leaves out is
one pointing at an annotation you removed: a removed annotation is not in the
document, so the edge to it is dropped and counted under
dropped_linkslike any other. Any other filter scopes those four to what the exported tasks actually need, and reports what it left out (dropped_projects,dropped_docs,dropped_notes,dropped_links,dropped_events; notes follow a doc's project rule): sharingtasqx export project:ledgerno longer ships every other project's knowledge along with it. A doc that carries no project ships only with--include-unscoped, which is refused on an unfiltered export — there is nothing to widen from. The store's default project is carried only when it is among the projects the filter kept, so a filtered export whose scope drops it comes back with no default rather than a documenttasqx importwould refuse.
tasqx export project:work --include-unscoped > work-and-global.jsonWith no filter you get everything. With a filter you get a slice. Human output is the JSON document — there is no separate pretty mode to drift from it.
tasqx export +api{
"default_project": "work",
"docs": [],
"dropped_dependencies": 0,
"dropped_docs": 0,
"dropped_events": 0,
"dropped_links": 0,
"dropped_notes": 0,
"dropped_projects": 0,
"events": [ ... ],
"links": [],
"notes": [],
"projects": [ ... ],
"tasks": [
{
"_rev": 4,
"annotations": [
{
"body": "Blocked on the D12 decision",
"created": "2026-07-16T08:51:09.6830568Z",
"id": "019f6a1f-62f3-75f0-bf57-e5ff9c7c452a"
}
],
"completed": null,
"created": "2026-07-16T08:51:09.2509427Z",
"depends_on": [],
"due": "2026-07-17T00:00:00Z",
"estimate": "PT4H",
"id": "019f6a1f-6142-70d3-be5b-e28dc6060e6c",
"modified": "2026-07-16T08:51:09.6830568Z",
"priority": "H",
"project": "work.tasqx",
"recurrence": null,
"remind": null,
"scheduled": null,
"short_id": 1,
"status": "pending",
"tags": [
"api",
"release"
],
"title": "Ship the v1 JSON API freeze",
"urgency": 17.5,
"wait": null
}
]
}Filtered exports and dependency edges
A filter selects a subset, so a dependency pointing out of that subset cannot travel with it — the target is not in the document. Those edges are trimmed, and you are told, on stderr:
tasqx export +docs > slice.jsonnote: 1 dependency points outside the export, left out — widen the filter to keep itThe note is on stderr because stdout is the JSON. A note there
would corrupt every pipe. tasqx export +docs > slice.json gives you a
clean file and a visible warning, both.
The --json form reports the same thing as data — dropped_dependencies is
always present, and is 0 for an unfiltered export:
tasqx export +api --json{
"default_project": "work",
"docs": [ ... ],
"dropped_dependencies": 0,
"projects": [ ... ],
"tasks": [ ... ]
}What a document carries
An export is a self-contained document, and a project is part of it:
the projects array carries every project row — name, description, archived
state, identity — and default_project names the project a bare tasqx add
inherits. So are your memory docs: the docs array carries
every knowledge document, all of them regardless of any filter — a filter
selects tasks, and knowledge is not attached to a task. Restoring gives you back
the store you exported, not just its tasks. These sections are optional on
import, so a file written by an older tasqx still restores: with no projects
section there is nothing to check a task’s project against, so the row is
created from the tasks and import says which ones it made. With one, the
document is authoritative — a task naming a project it does not define is
refused, exactly as tasqx add refuses a --project no init ever created.
An import never moves your default. If the destination store already has one, the document’s is ignored and the result names the default that stands.
tasqx import
Load tasks from a JSON file, or from stdin with -.
tasqx export | tasqx import -: round-trip
tasqx import backup.json
tasqx export | tasqx import -Takes a file, or - for stdin. It accepts either the document export prints
or a bare array of tasks (which is what every older export wrote). Import is
an upsert on the UUID — re-importing the same document is a no-op, not a
duplicate.
tasqx import slice.jsonimported 2 tasks · 1 project · 3 memory docstasqx export +api | TASQX_DB=/tmp/other.db tasqx import -imported 1 task · 1 project · no memory docsThe doc count is printed even when it is zero, and a document with
no docs section at all (written by a tasqx older than the memory-docs format) says so
explicitly: note: the document carried no `docs` section, so no memory docs were restored. Present-and-empty and absent used to print the
identical line.
A task whose number is already taken by a different task in this store keeps
its id and gets the next free number instead of refusing the import; every move
is listed under renumbered, with the number it came in as and the one it took.
Dependencies, annotations and checks follow the id, so nothing breaks — but a
branch name or a #n written down somewhere still points at the old number.
A task this store already holds keeps the number it has here, so importing the
same document twice changes nothing.
A memory doc whose source a different doc in this store already holds is the
same doc under two ids — which is what two machines that each imported the same
docs/ folder end up with — so it merges onto the doc already here instead of
refusing the import. The stored id is kept, the payload's is dropped, and the
later modified wins the text; a payload no newer than the copy here leaves it
untouched. Every merge is listed under docs_merged, with the source, the kept
id, the copy that won, and the dropped id — null when the payload document
carried no id of its own — and links pointing at the dropped id follow the doc
that kept the source.
Links come in last, after everything they point at, and are counted under
links_imported. Both ends are resolved against what this store holds after
the import — a project it already knew keeps its own row, and the link follows
it there. A link naming an end that is neither in the document nor already
here refuses the whole import, naming the link and the missing end, rather
than restoring an edge nothing can see.
The export document also records each project's archived flag, and importing
restores it. To un-archive a project directly, use
tasqx unarchive.
A project this store already knows keeps its own description when it has one:
projects carry no modified stamp the way a task does, so there is no
later-wins comparison to make, only a present-vs-absent one. A non-empty local
description always wins over the payload's, which is dropped and reported
under project_description_conflicts; a local description that is empty, or
a project this store has never seen, takes the payload's.
imported no tasks · 1 project · no memory docs note: project "api" kept its description here; the import's was "B's api description"
tasqx import backup.json --dry-run runs the whole import against this store
and rolls it back instead of keeping it: every renumbering, merge and refusal
prints exactly as a real import would, and the run ends with nothing was written. There is no separate preview logic — it is the same import, undone.
tasqx import backup.json --dry-runtasqx import other.json --merge folds a second live store into this one
instead of restoring over it. A task the document carries and this store has
never seen is imported exactly as it would be without the flag. For a task this
store already holds:
- The annotations, checks, tags and dependency edges written on either side are unioned — nothing written on either side is thrown away.
- A field that can only have one value (title, status, priority, project, the dates) is taken from whichever side changed that field last, going by each side's history, so a task completed here and retitled there comes back completed and retitled. Status travels with its completion time, and the four dates travel together.
- Tracked time adds up: the time the other store logged since the two last met joins the total here instead of replacing it, and importing the same document again adds nothing.
- The
_revcheck is skipped, because two stores count their own revisions and neither number means anything to the other. - One line per task names each field that differed and the side it came from, and how much tracked time arrived.
A note or a check both sides hold under the same id stays one row: a check
follows its own last edit, so a passed recorded here is not rolled back by a
copy that never saw it pass, and a note follows the task's. A memory doc both
stores hold under the same id keeps the copy with the later modified,
whatever its _rev; a tie keeps the one here.
Removals travel too. A tag, check, note, dependency edge, link or memory doc
removed on one machine stays removed when that machine merges the other's
older copy, and is removed on the other machine when it merges this one. Each
side's history decides: a removal counts when it is later than the last time
the row was added or changed, so a tag added back after it was removed
elsewhere stays. A removed note is scrubbed on the other machine the same way
tasqx unannotate scrubs it here: its text leaves the file.
Without --merge, the document stays authoritative about a task it names: its
notes, checks, tags and edges replace what is here. That is what a restore
should do, so it stays the default.
Either way, a note or a check id the document hands to one task while a different task here already holds it refuses the whole import, naming both tasks: one note, and one criterion, belongs to exactly one task.
A recurring task completed on both machines spawned its next occurrence on
each, under two different ids. When an import finds two tasks with the same
predecessor and the same due and scheduled dates, with or without --merge,
they fold into the older one: it takes the other's notes, checks, tags and
dependency edges, the status of whichever copy was started, stopped or
finished last, and the tracked time of both, and the other copy is deleted.
Each fold prints one line, for example
note: occurrence #12 duplicated #9 (both spawned from #4); kept #9, and is
listed under deduplicated. Both machines keep the same copy with the same
contents, so importing in the other direction, or the same file twice, ends
in the same place. If the two copies were wired into opposite ends of one
dependency chain, the survivor's edge that would close the loop is dropped and
the note names it. Completing a task again after reopening it does not spawn a
second next occurrence for the same date.
A field the schema does not name is rejected
A task object may carry only the keys the schema names, and nothing else (the
site's page lists them, generated from the code; tasqx docs opens it):
id, short_id, title, status, priority, project, tags, due, scheduled, wait, estimate, recurrence, remind, depends_on, annotations, tokens, urgency, created, modified, completed, _rev, status_unrecognized, tracked_seconds, tracked_adjustment_seconds, budget_tokens, active_since, checks, delivered_annotation_id, spawned_from, claimed_by, claim_expires, git_refs
An annotation may carry id, body, created. A key outside that set is a
bad_request naming the task and the field, because the alternative is worse
than an error: a misspelled tags used to import as ok with the tags silently
gone, so the report said your data arrived when it had not.
error [bad_request]: store.import: task 019f6a0f-99df-…, unknown task field `tag`
(accepted: id, short_id, title, …) — check the spelling or drop it; it was silently
ignored before, so the import reported success and the value never arrived
Two of those keys are accepted and deliberately ignored: urgency is
recomputed on the way in (it is derived from priority, due date and age, so a
supplied value could contradict the ranking rule) and status_unrecognized is a
read-side flag rather than stored state. They are listed because export emits
them and a round trip has to keep working.
A dangling edge is rejected, not repaired
A dependency target must be in the payload or already in the store. Anything
else fails with bad_request naming the id — and because the whole import is
one transaction, a rejection writes nothing.
error [bad_request]: store.import: task 019f6a0f-99df-… depends on 019f6a0f-99b5-…,
which is neither in the payload nor in the store (export the dependency too, or drop the edge)
That is a deliberate choice. An edge to an unknown id means the wrong slice was exported; repairing it quietly would hide exactly the mistake worth seeing. Payload order does not matter — tasks are written first, edges second, so a forward reference is fine.
Checking a restore
A store exported and imported into an empty one comes back with every task,
project, memory doc, quick-capture note, link, check, tag, dependency, timer
and task annotation, and the
event history behind them. Counted straight from the database, three numbers
still differ, on purpose, and tasqx export --out backup.json names each one
under the path it wrote (--json gives the counts as fields):
Wrote 240 tasks → /home/me/backup.json
Left out: 3 removed notes, 12 events earlier imports logged about themselves
removed_annotations: notes you removed. Removing a note scrubs its text and keeps an empty row; that row stays behind, and the record of the removal travels in the event log.dropped_events: on an unfiltered export, the history of a memory doc you removed, apart from the removal itself, because it names the title the removal took away. This one is also in the document.skipped_events: the entries an earlier import wrote about itself.
The import adds entries of its own, one per task, project and doc it brought
in, reported as events_logged beside events_imported. So the store's event
log is the document's events plus dropped_events and skipped_events, and
the restored one holds events_imported plus events_logged.
The simplest check is to export the restored store and compare: the document
is the same, apart from dropped_events and each task's urgency, which is
scored when the export runs.
tasqx export --out before.json
tasqx import before.json
tasqx export --out after.jsonRun the import and the second export against the new store.
Merging two machines
Each machine exports, and each imports the other's document. Rehearse with
--dry-run first — it prints the same report and keeps nothing.
On machine A:
tasqx export > a.jsonCopy a.json to machine B, then there:
tasqx import a.json --merge --dry-run
tasqx import a.json --merge
tasqx export > b.jsonCopy b.json back to machine A and do the reverse:
tasqx import b.json --merge --dry-run
tasqx import b.json --mergeBoth stores now hold both sides' work.
tasqx sync
The same merge, without carrying files around: each machine syncs with one shared remote. See Syncing Between Machines for setup, what merges and wins, secrets and headless use.
Recipes
| Want | Do |
|---|---|
| Back up | tasqx export > backup.json |
| Version your tasks in git | tasqx export > tasks.json && git commit -am wip — canonical output means clean diffs. |
| Move to another machine | tasqx export > all.json, copy, tasqx import all.json |
| Hand one project to a colleague | tasqx export project:work.tasqx > slice.json — mind the edge note. |
| Query beyond the filter grammar | tasqx export | jq '[.tasks[] | select(.urgency > 15)]' |
Good habits
A dated backup, in one line:
tasqx export --out "backup-$(date +%F).json"The store itself is a single SQLite file (tasqx config store prints its
path), so file-level backups work too — but the JSON export is
human-readable, diff-able, and version-control friendly.
Syncing Between Machines
Your tasks live in a single SQLite file on the machine you're on — see
Import and Export. Sync is how a second machine gets
the same store without you carrying a file around: each machine keeps its own
copy, both agree on one shared remote, and tasqx sync merges the two through
it. It is local-first — every machine works offline and merges when it next
syncs — never a shared live database, and each store has at most one remote.
The no-account start: a shared folder
The simplest remote is a folder every machine can already reach: a dedicated
folder inside your Dropbox, iCloud Drive or Syncthing share — not the one your
other files live in, so nothing else in it collides with tasqx's own files.
tasqx-remote-dir keeps the snapshot there.
tasqx sync setup dir --set path="$HOME/Dropbox/tasqx-sync"
tasqx syncNothing to sign up for: the folder's own sync client (Dropbox, iCloud Drive,
Syncthing) is what moves the bytes between machines: tasqx sync on one
machine writes into the folder, and the next tasqx sync on the other reads
it back once that client has caught up. When two machines write near the same
moment, the folder's client can leave more than one copy of the current
snapshot behind — a "conflicted copy" file, a HEAD 2, a
.sync-conflict-… name, depending on which client it is. tasqx-remote-dir
recognises all three shapes, hands every one of them back on the next pull
beside the real one, and a push that merges them removes exactly the ones it
merged — so a conflict from the folder client heals itself on the next sync
instead of piling up.
Cloudflare R2, step by step
R2 is the other connector, tasqx-remote-r2: one object in an R2 bucket
instead of a folder, for when there's no shared folder to reuse.
-
Create a bucket. In the Cloudflare dashboard, under R2, create a bucket. tasqx does not create it for you.
-
Create an API token scoped to that one bucket. Under R2 > Manage API tokens, create a token with Object Read & Write permission, scoped to the bucket from step 1 and nothing wider. Copy its Access Key ID and Secret Access Key — the secret is shown once.
-
Find your account ID. It's on the R2 overview page: 32 hexadecimal characters.
-
Run setup:
$tasqx sync setup r2You'll be asked for the account ID, the bucket, the Access Key ID and the Secret Access Key (masked, not echoed), and an object key — leave it empty and it defaults to
tasqx/snapshot.tqxinside the bucket. -
The passphrase. Every setup, whichever connector, ends by asking for a sync passphrase twice. This is what encrypts the snapshot before either connector ever sees it — the folder or the bucket holds ciphertext, never your tasks in the clear. Give every machine that syncs with this remote the same passphrase. Lose it and the remote's snapshots cannot be read by anyone, on any machine — each machine's own store is untouched, but there is nothing to recover the remote from. It's kept in a file beside your store, readable by you alone; it is never sent anywhere.
Repeat tasqx sync setup r2 with the same account/bucket/token and the same
passphrase on every other machine that should join this remote.
config edit's Sync section
tasqx config edit shows a Sync section below the settings: the connector in
use, the last version synced and how long ago, and whether a passphrase is
set up.
c— connect: lists thetasqx-remote-*connectors found on yourPATH, and once you pick one, shows its fields as a form (label, help text, a masked field for a secret), then the passphrase twice. This is the same writetasqx sync setupmakes — there's exactly one way "set up" happens either screen uses.s— sync now: runs the same synctasqx syncruns and reports one line.d— disconnect: after a confirmation, removes this store's sync setup (the connector record and the passphrase file). The remote itself is untouched — another machine still synced to it keeps working.
tasqx sync
tasqx sync
tasqx sync --statustasqx sync pulls every snapshot the remote holds, merges each one into this
store exactly the way tasqx import --merge does (see
Import and Export), then exports and
pushes the result back. If another machine pushed in between, it merges that
too and tries again, three times at most; failing on the third names it a
conflict and says to run tasqx sync again (exit code 5, conflict, so a
script can retry on it) — nothing already merged is lost, since merging is
idempotent. When the remote already holds exactly this store (nothing changed
on either side), nothing is pushed. Sync has no MCP tool: it runs in your
shell, not from an agent session.
tasqx sync --status prints the connector, the last version synced
(eight characters) and when (today, 2d ago, never synced, or
not set up before the first setup).
What merges, and what wins
The same rules tasqx import --merge applies (see
Import and Export for the full account):
- A field that can only hold one value — title, status, priority, project, the dates — is taken from whichever side changed that field last.
- Notes, checks, tags and dependency edges are unioned: nothing either side wrote is thrown away.
- Tracked time adds up across both stores' histories.
- Removals travel: a tag, check, note, edge, link or memory doc removed on one machine stays removed once its later history reaches the other.
- A recurring task completed on two machines, which each spawned its own next occurrence, folds the two duplicates into one.
Secrets
A connector's secret fields (an R2 Secret Access Key, for instance) are never
accepted on the command line: sync setup --set refuses a field the
connector marks secret, since argv lands in shell history and ps. Give it
at the no-echo prompt, or set TASQX_SYNC_<KEY> (the field's key,
upper-cased, with anything that isn't a letter or a digit turned into _ —
secret_access_key becomes TASQX_SYNC_SECRET_ACCESS_KEY) when there's no
terminal to prompt at. tasqx sync setup reads it once and hands it straight
to the connector; it is never stored by tasqx itself. tasqx-remote-r2 keeps
its own copy of the secret in the OS keyring, not in a file.
Headless machines and CI
Off a terminal, every setup field must come from --set (a missing one is
refused, naming it) or the environment — a secret field still cannot come
from --set. For tasqx-remote-r2 specifically, TASQX_R2_SECRET_ACCESS_KEY
answers the Secret Access Key without a keyring, which a headless box may not
have. TASQX_R2_ENDPOINT replaces the default
https://<account_id>.r2.cloudflarestorage.com for a bucket in a jurisdiction,
such as https://<account_id>.eu.r2.cloudflarestorage.com for the EU. The sync
passphrase itself, needed by sync setup and read again by every later sync,
comes from TASQX_SYNC_PASSPHRASE.
TASQX_R2_SECRET_ACCESS_KEY=... TASQX_SYNC_PASSPHRASE=... \
tasqx sync setup r2 --set account_id=... --set bucket=tasqx \
--set access_key_id=... --set object_key=
TASQX_SYNC_PASSPHRASE=... tasqx syncLimits
-
One remote per store. Two stores on one machine (a second
$TASQX_DB, or a scratch store) sync to their own remotes and never see each other's setup. -
No background sync.
tasqx syncruns once and returns; nothing polls or watches a remote on its own. Schedule it yourself — cron on Linux/macOS, Task Scheduler on Windows:$# crontab -e — sync every 15 minutes */15 * * * * TASQX_SYNC_PASSPHRASE=... /usr/local/bin/tasqx sync >>~/tasqx-sync.log 2>&1$schtasks /create /tn "tasqx sync" /sc minute /mo 15 /tr "tasqx sync" -
A running daemon is fine.
tasqx syncis a client of the same dispatch every other command uses, so withtasqx daemonrunning its merges go through the daemon like any other write instead of opening the store beside it.
Settings and Themes
Settings live in a TOML file, but you never have to hand-edit it — every read and write goes through a command, and there's a full-screen editor with live theme preview.
Default output should be something you want to look at. Themes drive the terminal and the HTML report from the same palette, and degrade honestly when the terminal cannot keep up.
tasqx config
| Command | What it does |
|---|---|
tasqx config list | Every setting: value, source, default |
tasqx config get theme.name | One resolved value |
tasqx config set theme.name gruvbox | Write it (your file comments survive) |
tasqx config unset theme.name | Back to the default |
tasqx config path | Where config.toml lives |
tasqx config store | Which task store you'd be writing to |
tasqx config edit | Interactive full-screen editor |
Worth knowing:
- Resolution order is: command-line flag, then
$TASQX_*environment variable, thenconfig.toml, then the built-in default.config list's SOURCE column names the layer that won, so "why is this setting on?" has a lookup, not a hunt. config editopens an interactive screen — arrow through themes and watch the whole screen repaint in each one before anything is written. It needs a real terminal; scripts useset/unset.config storeanswers which SQLite file a command would actually write to, and why: through a running daemon, or in-process — including the case where your$TASQX_DBnames a different file than the daemon serves, so the daemon was passed over.
Every setting
tasqx config list prints these with their live value and source.
| Setting | Default | What it does |
|---|---|---|
theme.name | nord | The terminal theme (below). Also --theme and $TASQX_THEME |
dashboard.enabled | true | A bare tasqx on a terminal opens the dashboard; false prints the list instead. $TASQX_DASHBOARD overrides it |
dashboard.panels | tasks,projects,burndown,pulse,effort,tokens | Which panels the dashboard draws, in tab order |
dashboard.refresh | auto | auto re-reads on a timer, manual only on r |
dashboard.window | week | The burndown window it opens on (week, 14d, 30d); w cycles it |
notify.enabled | false | Lets the daemon raise OS notifications for reminders; needs a build with the notify-os feature (Reminders) |
tokens.enabled | false | Lets the daemon attribute AI token use from local tool transcripts |
otlp.enabled | false | Runs a local OTLP/HTTP receiver in the daemon for token telemetry |
otlp.port | 4318 | The port it binds on 127.0.0.1 |
board.port | 0 | The port tasqx board binds; 0 takes a free one each run, a fixed one keeps the board's token so a bookmark survives |
daemon.idle_timeout | 0 | Minutes of idleness after which tasqx daemon exits; 0 stays up |
default_project | none | Where a bare tasqx add files; kept in the store, set with tasqx use |
mcp.add_status | pending | Status of a task added over MCP when the call names none; triage holds agent proposals until tasqx accept |
detail.time_format | both | How show and the MCP task view write timestamps: iso, relative or both |
tasqx theme
| Command | What it does |
|---|---|
tasqx theme list | Built-ins + your own theme files |
tasqx theme show nord | Preview a theme's colors and roles |
tasqx theme set nord | Persist the choice |
Built-ins: nord, gruvbox, dracula, solarized, mono — plus any theme
file you drop in yourself. A one-off try is
tasqx --theme dracula list, and $TASQX_THEME sets it per environment.
Themes
tasqx theme list THEME
* nord
gruvbox
dracula
solarized
monoThe * marks the theme in effect. tasqx theme show [name] previews every
role, a sample drawn in it beside its colour and emphasis, plus the urgency
ramp's bands, rendered at your terminal's real capability. Set one
permanently:
# config.toml
[theme]
name = "gruvbox"
Drop a .toml in $TASQX_CONFIG_DIR/themes/ and it appears in theme list
alongside the built-ins.
Capability is detected, not assumed. Pipe tasqx into cat and the
colour goes away; on a terminal without Unicode the block glyphs degrade to
ASCII rather than emitting mojibake. mono is there for when you want that
unconditionally.
Width is detected too. tasqx list sizes its columns to what is
actually in them and to the terminal it is printing into: a column no task
fills — DUE on a store with no due dates — is not drawn at all, and the
space goes to the titles. Through a pipe there is no width to detect, so the
table lays out for a fixed 100 columns and two runs of the same store stay
diffable.
Four environment variables override the detection:
| Variable | Effect |
|---|---|
NO_COLOR | Set to anything: drop all colour, keep bold/underline. Wins over everything below. |
CLICOLOR_FORCE | Set to anything but 0: force colour even through a pipe — for less -R and CI logs. |
TASQX_FORCE_COLOR | Set to anything: same as CLICOLOR_FORCE, scoped to tasqx. |
COLUMNS | How many columns wide to lay tables out. Beats both the terminal's own answer and the piped default — for a multiplexer that misreports its size, or to pin the width of captured output. Clamped to 40–160. |
Shell Completion
Tab completion for bash, zsh, fish, elvish and PowerShell — the same five
shells on Linux, macOS and Windows. It completes more than most tools: verbs,
flags, value sets, file paths, your actual task ids, project and tag names,
the capture sugar (+tag, project:x, !high) and the whole filter grammar.
tasqx completions
Completion isn't on after install unless Homebrew installed tasqx. Turn it
on by adding one line to your shell's startup file. tasqx completions <shell>
prints that line, so for bash this is the whole setup:
tasqx completions bash >> ~/.bashrc| Command | What it does |
|---|---|
tasqx completions <shell> | print the line for your startup file |
tasqx completions bash >> ~/.bashrc | add it, for bash |
tasqx never edits the file itself. With no shell named, it reads $SHELL.
This is where each line belongs.
bash, in ~/.bashrc:
source <(TASQX_COMPLETE=bash tasqx)zsh, in ~/.zshrc, after your compinit line:
source <(TASQX_COMPLETE=zsh tasqx)fish, in ~/.config/fish/completions/tasqx.fish:
TASQX_COMPLETE=fish tasqx | sourceelvish, in ~/.elvish/rc.elv:
eval (E:TASQX_COMPLETE=elvish tasqx | slurp)PowerShell, in $PROFILE:
$env:TASQX_COMPLETE = "powershell"; tasqx | Out-String | Invoke-Expression; Remove-Item Env:\TASQX_COMPLETEPlatform notes
zsh: the line must come after compinit runs. Earlier, zsh prints
command not found: compdef, registers nothing, and carries on at exit 0.
oh-my-zsh and prezto run compinit for you; a hand-written .zshrc may not.
Windows: no Windows shell sets $SHELL, so name the shell, and let
PowerShell expand its own profile path — $PROFILE is a PowerShell variable,
not an environment variable, and it differs between Windows PowerShell 5.1,
PowerShell 7 and the ISE:
tasqx completions powershell >> $PROFILEAnd PowerShell must be allowed to run your profile at all: a stock Windows
client ships with execution policy Restricted, which silently never runs it.
Get-ExecutionPolicy tells you; Set-ExecutionPolicy -Scope CurrentUser RemoteSigned is the minimum that does.
cmd.exe can't be completed by any program (that's cmd, not tasqx), and nushell completes external commands through its own mechanism that tasqx can't activate yet. Any other shell is refused, naming the five above.
How it behaves
- Task ids come with their titles in zsh, fish and PowerShell (bash and elvish can only show bare ids — their completion protocol has nowhere to put a title; that is upstream's protocol, not a tasqx setting).
- The id menu shows every task by urgency, not just open ones —
reopenandwhyneed the closed ones. - An alias only surfaces when no canonical name claims the prefix:
ls<TAB>givesls,mod<TAB>givesmodify.
Before you switch it on
The activation variable is TASQX_COMPLETE, deliberately not the generic
COMPLETE that clap tools usually take. The protocol cannot tell a callback
from a real command: with a recognised shell name in that variable,
tasqx add -- "a real task" writes nothing, exits 0, and does not add the
task. A tasqx-specific name makes that state improbable rather than
impossible, so do not export it by hand. COMPLETE on its own does nothing to
tasqx.
A Tab press READS your store — through a running daemon if there is one,
otherwise the SQLite file opened read-only — inside a 150 ms budget. If
anything fails, you get no candidates rather than an error smeared across the
line you're typing. That read leaves tasks.db-shm and tasks.db-wal beside
your store: SQLite's doing, and an ordinary tasqx list creates the same two
and removes them again. Your database is never altered by a Tab press — no
migration, not a byte. TASQX_NO_COMPLETE_LOOKUP=1 turns the store lookups
off; verbs, flags and value sets still complete.
Feature development with tasqx
The pattern: one sub-project per feature, tasks ordered by dependencies, context in annotations. It replaces a board for solo work — the dependency graph does the sprint grooming for you.
Set up the feature
A feature is a sub-project under your product, named with a dot:
tasqx init myapp: once, for the producttasqx init myapp.checkout: the feature
tasqx init myapp
tasqx init myapp.checkoutCapture the work, then wire the order. A task with an unresolved dependency is blocked and stays out of your working set until its turn comes:
tasqx dep 2 1: #2 waits for #1tasqx dep 3 2: #3 waits for #2
tasqx add "Design the checkout data model" project:myapp.checkout !high est:4h
tasqx add "Implement cart-to-order endpoint" project:myapp.checkout !high est:6h
tasqx add "Payment provider integration" project:myapp.checkout est:1d
tasqx dep 2 1
tasqx dep 3 2Put the context on the task
Annotations are stored verbatim — newlines, markdown, code blocks and all. This is where acceptance criteria, links and implementation notes live:
tasqx annotate 2 '## Acceptance criteria
- [ ] POST /api/orders returns 201 + order id
- [ ] Prices recomputed server-side; client prices are a hint, never truth
- [ ] Idempotency-key header prevents duplicate orders on retry
See ADR-012 for the locking decision.'tasqx show 2 prints it back, unmangled.
Work it, one task at a time
tasqx next: the single highest-urgency unblocked tasktasqx done 1: prints#2 unblocked - <title>under the done line for each task it released
tasqx next
tasqx start 1
tasqx done 1If your pull requests follow the task/<id>-<slug> branch naming, tasqx git event,
called from a CI step, a hook or a script, can start and complete the task when
the PR opens and merges (a merge completes it only when every check passed and
nothing blocks it); see
Working on Tasks. Moving to another
machine? See Syncing Between Machines.
done announces what its completion released — you never have to work out what is
next by hand. tasqx list project:myapp.checkout shows the whole feature;
tasqx report project:myapp.checkout totals the feature's estimates — done work
included, since a plain report skips only cancelled tasks. Name a status when
you want less: tasqx report "project:myapp.checkout status:pending" totals
what you have not started yet.
Where this stops
tasqx is single-user and local. There are no sprints, and tasqx claim keeps agents off the same task but
assigns nothing to a person. The moment a second person needs to work the same
backlog, you have outgrown this pattern.
Personal task management with tasqx
The capture-everything, trust-the-list workflow. The design goal: adding a task is one line with no ceremony, and the list you look at shows only what is actionable now.
Capture without friction
Everything after add is the title, except the parts tasqx recognises as structure:
tasqx add Buy milk
tasqx add "Renew passport due:friday !high +errands"
tasqx add "Water the plants" repeat:"every 3 days"
tasqx add "Call the dentist due:tomorrow remind:-1h"Dates take natural language — tomorrow, friday 17:00, in 3 days, eom — and
reminders anchor to the due date, so when the due date moves, the reminder moves
with it.
Not sure it is a task yet? tasqx note "read the RFC about retries" captures it
outside your list, and tasqx notes shows them later; see
Notes.
Look only at what matters
tasqx list is the working set: pending and active tasks that are not blocked,
hottest first. (A bare tasqx opens the dashboard on a terminal, and prints this
same table everywhere else — in a pipe, a redirect, or under --json.) Urgency is computed from priority, due proximity and age — and
tasqx why 42 shows the arithmetic, so the ordering is never a mystery.
Things you cannot act on yet stay out of sight until they become actionable:
| Command | What it does |
|---|---|
tasqx add "Book the campsite wait:2026-08-01" | Backlog until August 1 |
tasqx add "Prep the demo scheduled:monday" | Surfaces on Monday |
Defer, don't delete
| Command | What it does |
|---|---|
tasqx modify 42 due:monday | Push it out, reminder moves along |
tasqx cancel 17 | Reversible, kept in history |
tasqx reopen 17 | Changed your mind |
cancel is not delete: the task stays in the event log and out of your reports'
way. There is no destructive path in daily use.
The five-minute weekly review
| Command | What it does |
|---|---|
tasqx list "status:done completed.after:-7d" | What got finished |
tasqx report status | Open vs done; cancelled stays out |
tasqx chart heatmap | Completion density, calendar-style |
Standups, reviews and reports
Everything below is a pure read of the same API the CLI uses — reports never mutate.
The morning standup
What you finished, what is in flight, what is stuck:
tasqx list "status:done completed.after:yesterday": yesterday's outputtasqx list: today's working set
tasqx list "status:done completed.after:yesterday"
tasqx list
tasqx list "status:pending" --json | jq '[.tasks[] | select(.blocked)]'The weekly view
| Command | What it does |
|---|---|
tasqx report | Per-project: count, estimates, overdue, tracked, tokens |
tasqx report status | The same, grouped by lifecycle state |
tasqx chart throughput | Added vs done per ISO week |
tasqx chart heatmap | Completion density, calendar-style |
tasqx chart burndown | Remaining open tasks over the last N days |
Charts render natively in the terminal from the event log — every add, done and cancel was recorded transactionally, so the history is complete by construction, not sampled.
A report you can send someone
tasqx report --html --out review.htmlOne self-contained file: inline CSS and SVG, no external requests, both light and
dark schemes. Open it in a browser, attach it to a mail, drop it in a channel.
Five built-in themes (--theme nord, gruvbox, dracula, solarized, mono).
It opens with what changed in the last 7 days against the 7 before, then a
standup — done yesterday, in progress, blocked and by what, the next five — so
it doubles as the notes for the meeting. --since/--until pick another
period. Annotation bodies stay out of the file unless you add --with-notes.
Time tracking honesty
tasqx start/stop accumulate tracked time per task, and reports show tracked
against estimate per project — which is how you find out your "4h" tasks are 9h
tasks before you promise the next deadline.
The retrospective the numbers can carry
tasqx report says what the work cost. --outcomes says how it went:
tasqx report --outcomes project --since -30dREWORK counts completions that were later reopened. SILENT counts completions
nobody annotated — the ones that taught the next session nothing. CALIB is the
median tracked-over-estimate ratio, which is the "4h tasks are 9h tasks"
sentence above as a number. DROPPED is work that was started and then
cancelled. FORCED counts completions that overrode an open blocker with
--force. OVER counts completions that went past their token budget, UNPROVEN
those that closed with an acceptance check still open or failed, and UNBRIEFED
those whose task has no recorded tasqx brief (or memory-carrying
tasqx_start_timer); each is read against the completions that could have been
one. Reports and Charts
has the full column table.
Every rate reads count/n, never a percentage, and the reason is worth a
sentence at a standup: 3/12 and 3/3 are the same percentage and very different
news. Check the denominator before you act on the number — a rework rate over
three completions is not evidence of anything.
Driving tasqx from an AI agent
tasqx ships an MCP server, so an agent reads and mutates your tasks through the same
core API your shell uses. No glue code, no scraping --json output.
Wire it up
For Claude Code, Codex CLI and Gemini CLI, one command sets up the server and
installs the tasqx-workflow and retro skills for each tool it finds on
your PATH:
tasqx setupIt's a checklist screen — Space toggles an item, Enter installs what's
ticked, --yes installs everything not yet present without the screen. Run
it again later and it shows what's already in place. To wire up only the
server by hand:
claude mcp add --scope user tasqx -- tasqx mcp serve --scope write
codex mcp add tasqx -- tasqx mcp serve --scope write
gemini mcp add --scope user tasqx tasqx -- mcp serve --scope writeAny other MCP client takes the same command/args shape:
{
"mcpServers": {
"tasqx": {
"command": "tasqx",
"args": ["mcp", "serve", "--scope", "write"]
}
}
}
Leave off --scope write and the server is read-only: the write tools are not just
refused, they are absent from the tool list, so the agent cannot even try. Start
read-only; grant write once you have watched what the agent does with read.
Shelling out: always spell the verb
An agent that runs shell commands must write tasqx list, never a bare tasqx.
A bare tasqx opens a full-screen dashboard whenever stdin and stdout are both
terminals — and a harness that gives its child a pty (pexpect, node-pty, tmux,
docker run -t) satisfies exactly that, however unattended it is. The child then
switches to the alternate screen and blocks on a keypress that never comes:
measured at 80x24 and 120x40, no exit, no parseable output, and the tool call
times out. Setting CI=true does not help, because nothing reads it — the stream
check is the whole condition.
Killing it does not undo the screen either: the leave-alternate-screen sequence is never sent, so a harness that reuses one pty runs its next command inside the alternate buffer.
Either of these is enough:
tasqx list: the verb always means the tableTASQX_DASHBOARD=false tasqx: switches the screen off for the whole image instead
tasqx list
TASQX_DASHBOARD=false tasqxThe MCP server above is unaffected: it speaks JSON-RPC over pipes, so it is on the non-interactive side by construction.
The loop an agent runs
The tool surface is designed around one loop — work the backlog one task at a time:
-
tasqx_list_taskswith"project:myapp.checkout"— see the feature, blocked tasks marked. The default row is compact —short_id, title, status, priority, urgency, blocked, due, project, tags— so passfieldsfor any other column, orfields: []for the whole row. -
tasqx_brief_task— everything needed before starting, in one call: the task and its annotations, each prerequisite with what that task concluded, what this one blocks, and relevant memory under a query tasqx derives from the task's own title, tags and project. The answer is one content block: tasqx-rendered markdown. That is the intended reading — layout is tasqx's job, so the agent uses it as-is rather than recomposing the detail from JSON — and thedetail.time_formatconfig key (iso,relativeorboth) decides how it writes timestamps. Passinclude_json: truewhen a script needs the raw JSON beside it; it is the same result again, so the agent that reads the markdown is paying twice for it.tasqx_get_taskis still the right call when you only want the task itself. -
tasqx_start_timer, do the work,tasqx_complete_task— the completion result names any tasks it unblocked, which is the agent's cue for what to pick up next. Pass the turn's token counts on completion (input_tokens,output_tokens,cache_read_tokens,cache_creation_tokens): the agent is the only party that knows which task the spend served, so self-report is the primary measurement channel — completing without counts earns atokens_hintin the response saying exactly that. Passview: "card"when a person will read the outcome: the completion then leads with the box card of the task as completed (Delivered row, checks), so notasqx_get_taskre-read is needed. -
tasqx_annotate_task— write back what was done, decisions made, anything the next session needs.
tasqx_add_check puts acceptance criteria somewhere a completion can be held to
them: an annotation is prose and nothing can ask whether it was met, while a
check has a state. tasqx never runs one — the criterion is a claim and the
evidence a citation, both stored verbatim — so the agent marks them itself, or
names them in checks_passed on completion. Completing with a criterion still
open is not refused; it is counted, and tasqx_outcomes reports the count.
tasqx_add_dependency lets the agent decompose a feature itself: capture subtasks
with tasqx_add_task, wire the order, then work the chain.
Several agents, proposals and notes
- Claims. With more than one agent on a store,
tasqx_claim_taskfirst andtasqx_release_taskwhen done: a claimed task leaves the other agents'nextand@working, and@claimedlists what is held. See Working on Tasks. - Proposals.
tasqx config set mcp.add_status triagemakes a task an agent adds over MCP without naming astatusa proposal that waits for yourtasqx triage,accept,declineorduplicate. See Working on Tasks. - Duplicates.
tasqx_add_taskreturnssimilar, the open tasks in the same project that read like the new one; look before adding another. - Notes.
tasqx_add_noteand its siblings capture a thought that is not yet a task or a memory doc. See Notes. - Pull requests. A CI step, hook or script that calls
tasqx git eventstarts or completes the task a PR names; tasqx does not watch GitHub itself. See Working on Tasks. - Stale rulings. A memory hit carries
stale_refs, the files and symbols it cites that the checkout no longer has. See Memory. - Setup.
tasqx setup --listshows what is installed and--yes --only retroinstalls one item without the checklist. See AI Agents and Automation.
Give the agent memory
tasqx memory import docs/ turns a directory of markdown docs — ADRs, runbooks,
company patterns — into a searchable knowledge store. tasqx_brief_task consults
it for you at the moment you pick a task up; tasqx_search_memory is for
consulting it mid-task (both work read-only, deliberately). Imported docs carry
no project, which makes them global: a brief scoped to one project still
surfaces them, and only another project's notes are held back. The
import is not recursive: a directory means its own *.md files, so run it once
per folder that holds docs rather than pointing it at the root of a tree. Task
annotations are searchable through the same tool, so decisions written down
during one task resurface during the next:
tasqx_brief_taskbefore touching payment code — the query is derived from the task, so nothing is guessed.- Hits come back ranked by words and by meaning, with snippets — docs and past
annotations alike, each saying which of the two found it.
Half the slots are held for docs, so an imported ruling still reaches the page
when a project's own task notes share its words; annotations take the rest.
The brief's page defaults to five hits; a caller-named
memory_limitwidens it. tasqx_search_memorywith"payment idempotency"for anything the brief's scope did not reach.- After the work,
tasqx_add_memorystores what the next session should know.
Read how the last run went
tasqx_outcomes is the other half of measurement: tasqx_summary answers what
the work cost, this answers whether it worked. Over the tasks that closed,
it reports rework (completions that were later reopened), estimate calibration,
token cost, silent completions (no annotation written), abandoned work, token
overruns, unproven completions, forced completions (completions that
overrode open blockers) and unbriefed completions (a task closed without a
tasqx_brief_task, or a tasqx_start_timer that carried memory, ever having
read the project's rulings) — each rate beside the n it was computed over,
because a rework rate over three completions is not evidence.
tasqx report --outcomes project --since -30dIt is on the read scope, so a read-only agent has it, and it is worth a call before a big piece of work on a project you have touched before: a high silent count means the annotations your next session will search for are not being written, and a high rework count means completions are being called too early. A retrospective (A self-improving agent) that cites these numbers is answering from evidence rather than from its own memory of the session.
Safety properties you get for free
tasqx_modify_taskis optimistic-concurrency-checked: the server pins the task's revision before writing, so if you edited the task in another shell mid-flight the agent gets aconflictand re-reads instead of clobbering. The other writes — complete, tag, annotate, timers, dependencies — take an optionalexpected_revbut the server does not pin one for you, so without it they are last-write-wins, which is why field edits belong on modify.- There is deliberately no bulk-delete tool. Cancelling goes through the same reversible, logged path as everything else.
- Every agent action lands in the append-only event log, so
tasqx chartand the history are as true for agent work as for yours.
Giving an agent memory in any client
The initialize result carries an instructions field: a condensed form of the
block below, so a host that surfaces server instructions — Claude Code and others
do — gets the nudge with no setup at all. Paste the block below when your host
ignores or truncates instructions, when you want the reasons behind each rule,
or to tune the placeholders: the import directory, and cutting the write half for
a read-only server.
The block works anywhere the tools do: CLAUDE.md, AGENTS.md, .cursorrules
or .cursor/rules/, a Zed rules file, a Codex instructions file, or the system
prompt of a bare MCP host. It covers the backlog as well as memory, because the
two are one store: an annotation written while working a task is what the next
session's search finds. Paste it whole. The searching half works under a
read-only server; the storing and tracking halves need
tasqx mcp serve --scope write, which
Driving tasqx from an AI agent argues you grant after
watching a read-only session rather than before — until then the block tells the
agent to hand you the text it could not store.
Paste this
## tasqx is your organiser: your backlog and your long-term memory
tasqx holds a backlog of tasks and one searchable index over two things: knowledge
docs, and the annotations written on tasks. Reach it over MCP (`tasqx_*` tools) and,
for the verbs MCP does not carry (`undo`, `memory import`, `report --html`),
the `tasqx` CLI — always spell the verb (`tasqx list`, never a bare `tasqx`, which opens
a full-screen dashboard and hangs a tool call). Use it instead of in-conversation todo
lists and instead of any other memory file.
Reading and writing are equally mandatory — an agent that only searches leaves the
store as empty as it found it. If `tasqx_add_memory` and `tasqx_annotate_task` are
missing from your tool list the server is read-only: say so once, keep searching, and
put what you would have stored into your reply. Never silently drop the write. If no
`tasqx_*` tool responds and the CLI is missing too, say so and track the work in the
conversation — do not pretend it was recorded.
### Search before you decide
Call `tasqx_search_memory` before you:
- resume work, or answer "where were we"
- choose between two designs, libraries, schemas or file layouts
- touch a convention-bearing file (config, CI, migration, public API)
- contradict something that looks deliberate, or answer "why is it built this way"
- assert that something is "how this project does it"
Search first, form the opinion second. Report what you found; if a decision contradicts
a hit, say which one and why.
Query with two or three keywords, not a sentence. Search matches meaning as well as
words, so `sign-in failures` finds a note about "login errors"; each hit's `via` says
whether its words (`lexical`), its meaning (`semantic`) or `both` matched. An entry with
every word ranks above one with only some, so `retry idempotency` and `tokens.css` work
as typed; a hit with only some of the words says `partial: true`, and the result says
`relaxed: true` when one is on the page. `matched` shows the all-words expression that
ran, `matched_any` the any-word one. For an exact name, id or error string, pass
`mode: lexical` to match its characters and nothing else.
Run two searches with different wording before concluding nothing is there; no tool
lists the store, so searching is the only way in. A hit is a snippet, not the document:
read a doc whole with `tasqx_get_memory` on its `id`, and a hit whose `source` reads
`task:#<id>` is an annotation — read that task with `tasqx_get_task` before acting on it.
### Track multi-step work as tasks
A real unit of work — a feature, anything multi-step, anything that could outlive this
session or block something else — goes in the backlog. A question, a single command or
a one-line fix does not; do it and move on.
- One project per repo or initiative. Check `tasqx_list_projects` before
`tasqx_create_project`; name it after the repo so every session finds the same one.
- Decompose with `tasqx_add_task` (title, project, priority H/M/L, estimate) and order
the pieces with `tasqx_add_dependency`. Acceptance criteria go in `tasqx_add_check`,
where something can ask at completion time whether they were met; other context goes
in an annotation, not the title.
- Pick work from `tasqx_list_tasks` with filter `project:<name> @working`. Blocked and
waiting tasks are hidden there by design: an empty working set with open tasks means
everything is blocked, not that the work is gone. The default row is compact — short_id,
title, status, priority, urgency, blocked, due, project, tags — so pass `fields` for
any other column, or `fields: []` for the whole row.
- Per task: `tasqx_brief_task` for everything you need before starting — the task, what
each prerequisite concluded, and relevant memory under a query tasqx derives, so you
supply no search terms. Then `tasqx_start_timer`, do the work, annotate the outcome,
and `tasqx_complete_task` naming in `checks_passed` what you actually proved. Its
`unblocked` result names what to pick up next; pass token counts when you know them.
- Work that became obsolete is cancelled (`tasqx_cancel_task`, or `tasqx cancel <id>`),
never left open. Nothing in the backlog is hard-deleted.
### Day one: import what already exists
Memory starts empty, and an empty store makes the search rule a no-op — if searches
keep returning nothing, it was never seeded. From the repo root, once per folder that
holds markdown docs:
tasqx memory import [YOUR DOCS DIRECTORY]
No MCP tool imports; run it in the shell, or ask me to run it. It is non-recursive — a
directory means its own `*.md` files. Re-running replaces docs from the same source
rather than duplicating, keyed on the repo's directory name and each file's path inside
the repo (`clouter/README.md`), so any spelling of the directory finds the same docs and
two repos' files never replace each other.
### Store what the next session needs
Call `tasqx_add_memory` (`title`, `body`, `source`) when a decision is made and we stop
arguing about it, when you learn a convention written down nowhere, when a session ends
with work in flight, and when you get something wrong and find out why. Record the
ruling *and* the why; a ruling without its reason gets reopened. Write for retrieval:
the words a future search will use in the title, the ruling in the first line, the
reasoning under it, and the repo or path in `source`. Do not store what is already a
file in the repo — import it instead — and do not store transcripts, task lists or
progress narration; the backlog and its annotations already hold those.
When an entry turns out to be wrong, retract it rather than filing a correction beside
it: search ranks both with nothing to say which is true. Read it with
`tasqx_get_memory`, then remove it with `tasqx_remove_memory` on that id and add the
corrected entry. Removal is permanent — `tasqx undo` does not cover memory — so remove
only what you have read, and ask first if you did not write it.
### Annotate as the work happens
`tasqx_annotate_task` (`ref`, `body`) stores the body verbatim and indexes it into the
same store `tasqx_search_memory` reads — not bookkeeping, but how the next session finds
this work at all. Annotate the approach when you start, every decision, blocker or
change of direction as it lands, and what was delivered, measured and deliberately
skipped before `tasqx_complete_task`. Only the body is indexed, never the task title, so
name the files, symbols and terms inside the note.Tune it
One placeholder, in the import command: the directory holding your markdown. Fill it
before the first paste — an unfilled import command fails rather than importing
something wrong. In a user-level file that applies to every repo, such as
~/.claude/CLAUDE.md, there is no one directory to name; replace the placeholder with
the instruction to find the repo's markdown folders and ask before importing them.
Running the server read-only on purpose? Cut the tracking, storing and annotating sections and keep the search half, which works read-only by design. A store the agent only reads is thinner than one it maintains, and still worth the paste.
Import is the bulk load; tasqx_add_memory in a loop is not.
The one way re-importing bites
The source key is the repo's directory name plus the file's path inside the repo, not the path as it was handed in, so the obvious spellings of one directory land on the same document:
tasqx memory import ~/notes # 1 doc
tasqx memory import ~/notes/ # the same doc, unchanged
tasqx memory import ~/./notes # the same doc, unchanged
What makes a second document is a key written under another root or by an older
build. Import tells you when it sees one: is an older key of the same file as,
with the tasqx memory rm <id> that removes the old copy. tasqx_remove_memory
does the same over MCP, and tasqx memory search prints the source path with
each hit, which is how you spot one the note did not catch.
Where this stops
A pasted prompt is a suggestion the client re-reads at its own discretion, so it will
not survive an aggressively compacted context the way a tool description does. In Claude
Code the skill at .claude/skills/tasqx-workflow/SKILL.md
covers the backlog loop in more depth and loads on demand instead of sitting in context;
it carries no trigger list for when to search and no read-only fallback, so keep those
two and let the skill do the rest. Read
Driving tasqx from an AI agent first for the wiring, the work
loop and the safety properties this file assumes you already have.
A self-improving agent: a session-end retrospective
Every completed task is a moment where the agent knows what worked and what
did not: the edit it reverted, the command it re-ran twice, the convention it
found out the hard way. By the next session all of it is gone, unless
something writes it down where tasqx_search_memory can find it again.
tasqx gives the agent two ways to do that: a retrospective block on every
completion, and an optional skill for a closer look at the end of a session.
This guide assumes tasqx is already wired into your agent over MCP. If it is not, start with Driving tasqx from an AI agent.
The retro block
On a write-scope connection, the result of tasqx_complete_task carries a
retro block of about 1 KB. It holds six questions, the routing rules and a
retraction step, so any MCP client runs the retro with no hook and no skill
installed:
- Did the user correct or redirect you?
- Goal versus delivered?
- Rework?
- A discovery written down nowhere?
- Would you redo anything?
- A procedure others will repeat?
A task that went straight through answers in one line (straight through, nothing to record) and stops. Anything worth keeping goes in a retro:
annotation on the task. Each correction from the user becomes a standing: true memory doc; the write-scope instructions tell the agent to store it the
moment it lands. Before adding a ruling, the agent searches memory for a doc
on the same subject and, if the new one supersedes it, removes the old one with
tasqx_remove_memory, so two contradictory docs never stand side by side.
Pass include_retro: false to leave the block out, for a batch of completions
that already ran one retro. A read-scope connection cannot complete tasks and
never receives the block.
The retro skill
The retro skill is an optional convenience for Claude Code, Codex and Gemini.
Nothing triggers it automatically: run /retro at the end of a session, or
/retro 123 for one task. It looks at every task the session completed, but
only writes about the ones that had rework, an undocumented discovery, a
blocked or refused step, or an executor whose result came back wrong. A task
that went straight through is named as such and gets no annotation.
For each task that qualifies, the session assembles a small evidence bundle:
- The task read whole:
tasqx_get_taskwithinclude_json: falseandannotations_limitset to the task'sannotations_total. A long history is still cut to the response budget; a non-nullannotations_next_offsetmeans there is more, so page until it is null. - The executor's final report for that task, verbatim.
tasqx_outcomesfor the task's project.- Two or three lines on anything the planning session itself reworked.
It hands the bundles to a fresh general-purpose agent, not a fork, so the
cost does not grow with the length of the session. That agent answers five
questions per task (goal versus delivered, rework, discovery, redo,
procedure), routes each finding, and writes the retro: annotation. A fact
goes to memory after a search shows it is not already there. A prohibition is
named to you as a hook candidate; creating the hook is your call. A procedure
must pass two gates, procedural and non-obvious, and is promoted to a skill
only on its second sighting.
Install it with:
tasqx setup --only retroThe skill text is compiled into tasqx, so each release installs the version
that matches it; the source is
.claude/skills/retro/SKILL.md in the
repository. With Claude Code
on your PATH it installs ~/.claude/skills/retro; with Codex or Gemini it also
installs ~/.codex/skills/retro and ~/.gemini/skills/retro (--only codex:retro, --only gemini:retro). A SKILL.md that differs from the
bundled one, your own edit or an older copy, is kept;
tasqx setup --yes --only retro --force replaces it.
What accumulates, and how to read it
| What | Where it lands | How to find it |
|---|---|---|
| What a task taught | A retro: annotation on that task | tasqx_get_task, or tasqx show <id> |
| Facts and conventions | Memory docs | tasqx_search_memory, or tasqx memory search |
| Procedures | Tasks titled skill candidate: …, tagged skill-candidate | tasqx list +skill-candidate |
The agent finds the first two by itself: both live in the store
tasqx_search_memory covers, so the next task on the same code reads last
time's annotation (Memory explains the ranking). Candidates
are the part you look at. On a second sighting the skill drafts a real
SKILL.md, records the path on the candidate and cancels it; list the open
ones now and then, and promote one by hand when you already know it recurs.
Other clients
A client without a skills mechanism needs nothing more: the block arrives with
every completion. To run the longer procedure there, put the five questions,
the scope gate and the routing rules from
.claude/skills/retro/SKILL.md into the
agent's system prompt, next to the block from
Giving an agent memory in any client, and tell it to
run them at the end of a session against the same evidence bundle.
Counting AI token spend per task
tasqx attributes AI token usage to the task the spend served — always as four
separate buckets (input, output, cache read, cache write), never a blended
total: cache-read tokens cost a fraction of fresh ones, so one number would
destroy exactly the split a cost report needs. The terminal report's TOKENS
column names the largest bucket and its own count (cacheR 13.6M);
tasqx report --json and the HTML report carry all four.
Turn it on
tasqx config set tokens.enabled trueThat switch lets the daemon parse tool transcripts after a completion — nothing
is measured through it without a running daemon (tasqx daemon). Self-reports
on completion need no daemon and no switch.
Three channels
Self-report — the primary channel. An agent completing a task over the API
or MCP passes what it spent: input_tokens, output_tokens,
cache_read_tokens, cache_creation_tokens (any one count is enough) plus an
optional tool and model. The agent is the only party that knows which task
the spend served; completing without counts earns a tokens_hint saying so.
A harness that reports only one number passes total_tokens instead of the
four, never beside them. It is stored as its own kind, shown as
total (unsplit), and never split into input and output. A count that
arrives after the task was completed goes through tasqx_add_tokens over MCP,
or from a shell:
tasqx tokens add 602 --total 37898
tasqx tokens add 602 --in 18400 --out 2600 --tool claude-codeLog-parse — the fallback. Name the calling tool on start/done and the
daemon reads the tool's own transcript afterwards:
tasqx done 4 --client "claude-code 2.1" --session-id "$SID" --transcript-path "$TP"Parsers exist for Claude Code, Codex, Gemini CLI and GitHub Copilot CLI, picked
from --client by substring — without it no transcript is ever read.
Telemetry — the opt-in OTLP receiver below. Samples are matched to the task by session id, and beat log-parsing when both are available.
How much to trust a figure
Every measurement carries a confidence. It grades how firmly the tokens are tied to this task, not how precise the counts are: every channel reads per-request figures.
| Channel | Earns | When |
|---|---|---|
Telemetry (otel) | high | Always: samples are matched to the task by session id |
| Log-parse | high | An explicit --transcript-path, with the completion's session id confirmed in the file |
| Log-parse | medium | An explicit path to a file with no per-session anchor (Gemini, Copilot) |
| Log-parse | low | No path: the transcript was found by scanning and matched on time overlap alone |
| Log-parse | low | tokens recompute found the transcript gone, so it kept the counts and lowered the grade |
| Self-report | medium | Every report on done, tasqx_add_tokens and tokens add; token.add through tasqx api also accepts low |
A self-report is medium and can never be high. Only the agent knows which
task the spend served, so self-report is the primary channel. But nothing
tasqx holds can check the number, and the grade describes how checkable a
figure is, not how much the reporter is trusted. A report stays unverifiable
however much you trust whoever sent it.
The OTLP receiver
tasqx config set otlp.enabled true makes the daemon listen on
127.0.0.1:4318 (otlp.port to change it) — loopback only, never exposed.
Point Claude Code at it with four environment variables:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318The endpoint is the base URL — the exporter appends /v1/logs itself, which is
the path tasqx parses (/v1/metrics is accepted so exporters do not retry, but
never parsed). Receiver and exporter are both off by default, so no telemetry
moves until you turn on both.
The rules
One task never mixes channels: a self-report is authoritative, and log-parsing
stands down for a task the agent already measured. A transcript sample banked
by one task is claimed globally — no other task can earn it, whatever its
re-read timestamp later says — and a sample falling inside two tasks' windows
is contested and banked for no one. Only contest ever removes tokens: a
transcript that goes missing or re-reads differently keeps its counts with
confidence downgraded to low, never deleted blind.
Budgets: the other direction
Accounting tells you what a task cost once it is over. A budget is the same number read before that:
tasqx add Port the payment adapter --budget-tokens 200000It counts fresh tokens — input, output and cache creation — and ignores
cache reads, because a budget dominated by re-reads measures how often the agent
re-read its own context rather than how big the work was. An unsplit
total_tokens counts in full: nothing can say how much of it was cache reads,
and counting none of it would let an unsplit report hide an overrun. That is not a blend of
the four buckets; those stay split everywhere they are reported.
It stops nothing. tasqx is a store an agent calls between turns: it never
sees the turn and could not interrupt one. An overrun is a signal, and usually
one signal in particular — the task was too big to hand over whole.
tasqx report --outcomes counts overruns so you can see which kinds of work
keep being cut too large.
Repairing old history
| Command | What it does |
|---|---|
tasqx tokens recompute | Dry-run: per-task delta, writes nothing |
tasqx tokens recompute --apply | Write the repair |
The one verb in the API built to delete measurement rows, so the dry-run
default is the safety, not a convenience. It parses transcripts and runs
in-process only — a daemon refuses it over the socket; stop the daemon and run
tasqx --no-daemon tokens recompute.
Writing a connector
tasqx sync keeps its remote behind an interface, not a library: a connector
is a small standalone executable that speaks one JSON protocol on stdin and
stdout (DESIGN.md D198). Point it at a database, an object store, an SFTP
drop, whatever holds bytes for you — tasqx never links against it, and it
never links against tasqx.
The protocol
tasqx runs your executable once per verb. It writes one JSON request to its
stdin, reads one JSON reply from its stdout, and judges you by the exit code:
0 is an answer, 2 means "push conflict" and nothing else, anything else is
a failure whose stderr is shown to the person running tasqx sync. Every
request and every reply carries "protocol": 1 — the version this tasqx
speaks; answering with anything else, or nothing, is treated as a protocol
mismatch.
| verb | request | reply |
|---|---|---|
describe | — | {name, version, protocol, fields: [{key, label, secret, help, help_url?}]} |
configure | {values: {key: string}} | {ok: true} or {ok: false, error} |
pull | {out_dir} | {snapshots: [{version, path}]} |
push | {in_path, expected_version: string | null} | {version} or {conflict: true} |
These are copied straight from the request and reply types in remote.rs
(the crate's remote module) — Request, Description, Field,
ConfigureReply, PullReply, Snapshot and PushReply — so a mismatch
here is a bug in this page, not a second source of truth.
describe — say who you are and what you need. A real reply, spelled the
way tasqx-remote-dir answers it:
{
"protocol": 1,
"name": "dir",
"version": "0.16.0",
"fields": [
{
"key": "path",
"label": "Folder",
"secret": false,
"help": "An existing folder every machine can reach: a network share, or one Dropbox, Syncthing or iCloud Drive keeps in step."
}
]
}
Every field needs a non-empty key and label; keys must be unique. Mark a
field secret: true when it's a password or a token — tasqx then masks it
while typing and refuses it on sync setup --set's command line.
configure — validate the user's answers (by actually connecting to the
remote, typically) and keep whatever you need to pull/push later, in your
own state directory (below). A rejection is an answer, not a failure: exit
0 with {"ok": false, "error": "…"}. tasqx stores none of these values
itself.
{"protocol": 1, "verb": "configure", "values": {"path": "/mnt/share/tasqx"}}{"protocol": 1, "ok": true}
pull — write every snapshot the remote holds into out_dir (created
for you) and name each file, absolute or relative to out_dir; tasqx refuses
a name that resolves outside it. An empty remote answers with an empty list.
Normally there is exactly one; a connector may hand back more (a folder
connector's conflict copies).
{"protocol": 1, "verb": "pull", "out_dir": "/tmp/tasqx-sync-x1/pull"}{
"protocol": 1,
"snapshots": [{"version": "3f9c2e…", "path": "3f9c2e….tqx"}]
}
push — upload in_path if, and only if, the remote's current version
equals expected_version; null means "only if the remote is empty". This
is the whole conflict story: a real conditional write (S3-style
If-Match/If-None-Match, a database row's own version column, a lock file
plus a compare), not read-then-write with a gap in the middle. Losing the
race answers {"conflict": true} and exit 2, and changes nothing; the
caller pulls, merges, and tries again.
{"protocol": 1, "verb": "push", "in_path": "/tmp/tasqx-sync-x1/push/out.tqx", "expected_version": "3f9c2e…"}{"protocol": 1, "version": "8a71b0…"}
The blob is opaque. It is always sealed ciphertext by the time your connector sees it — you never need to parse or validate it, only store and hand back exactly the bytes you were given.
On PATH, named tasqx-remote-<name>
Build a binary named tasqx-remote-<name> (.exe on Windows) and put it on
PATH, the way git finds git-remote-<scheme>. tasqx sync setup <name>
and tasqx config edit's connector picker both find it by searching every
directory on PATH for that exact name — on Unix it also has to carry an
execute bit, or it's treated as though it weren't there.
State directory
Every call carries TASQX_REMOTE_STATE_DIR in the environment: a directory
of your own, created for you before the first call, where you keep whatever
configure decided to save. It's scoped per store and per remote name
(remotes/<name>/ beside the store file), so two stores never share one, and
it's created private to its owner (0700 on Unix) — keep secrets in the OS
keyring rather than a file there when you can; the state directory is meant
as a fallback, not a vault. tasqx-remote-r2 uses the keyring for its Secret
Access Key and only a non-secret config in the state directory;
tasqx-remote-dir has nothing secret to keep at all.
Environment rules
You inherit tasqx's environment — HOME, PATH, the keyring's session bus
(XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS), proxies, CA bundles,
SYSTEMROOT on Windows, and any override you define yourself
(tasqx-remote-r2's own TASQX_R2_SECRET_ACCESS_KEY) — with one exception:
every variable whose name starts with TASQX_SYNC_ is stripped before you're
run. Those are tasqx sync's own secrets — the encryption passphrase and the
TASQX_SYNC_<KEY> answers to your secret fields — and you're never meant
to see them; a secret field's value reaches you exactly once, in the JSON
configure request.
Timeouts
The runner kills a call that runs too long: 120 seconds by default, except
pull and push, which tasqx sync gives 10 minutes — a multi-megabyte
snapshot on a slow link needs the room. Keep your own per-request timeout,
if you have one, comfortably under that: tasqx-remote-r2's HTTP timeout is
540 seconds, held there by a unit test that compares it against the runner's
constant.
Testing against the conformance suite
The suite every connector is held to lives behind a feature flag in the
core crate, not copied into each connector: add it as a dev-dependency with
features = ["conformance"], same as both shipped connectors do, and call
run on your own built binary.
[dev-dependencies]
tasqx-core = { version = "0.16", features = ["conformance"] }
Then, in a test of your own, import remote::conformance::{self, Setup} off
that dev-dependency, build a Setup { program, work_dir, good, bad } — the
executable under test, a scratch directory the suite may fill, the values
that configure an empty remote, and at least one value set configure must
refuse — and call conformance::run(&setup).
crates/tasqx-remote-dir/tests/conformance.rs is a real, working copy of
exactly that, Setup and all, ready to copy and point at your own binary.
Run it the same way any other test runs: cargo test -p tasqx-remote-dir
(or your own crate's name). The suite walks describe, configure with both
good and bad values, an empty pull, a push-then-pull round trip, and a
conflicting push, stopping at the first check that fails and naming it —
everything goes through the same Connector runner tasqx itself uses, so a
connector that passes is one tasqx can actually drive.
Two reference connectors
Read crates/tasqx-remote-dir/src/main.rs first — the simplest connector
that does something real, one setting (a folder path), content-addressed
blobs, and its handling of the conflict copies a Dropbox/Syncthing/iCloud
client leaves behind. crates/tasqx-remote-r2/src/main.rs is the other
shape: a hand-rolled SigV4 signer over a cloud object store, a secret kept in
the OS keyring instead of the state directory, and conditional PUT for
compare-and-swap. Between the two they cover every decision this page
describes in working code.
Every command
Every verb is a thin translation to exactly one core API method: it builds a params object, dispatches, and renders. Add --json to any of them to get the raw API result instead of the table. Each section below is generated from the same command table tasqx <verb> -h prints and from clap's own argument list, so a flag that exists is a row here.
Global flags
These work on every subcommand.
--jsonflagoptional- Print the raw JSON API result instead of the human table.
--theme<name>optional- Override the theme (
nord,gruvbox,dracula,solarized,mono, or a user file). Beats$TASQX_THEMEand the config. --socket<addr>optional- Socket / named pipe of a daemon. Overrides
$TASQX_SOCK. Refused byapi,mcp,chartandreport --html, which open the store in-process and cannot honour it (D73). --no-daemonflagoptional- Never route through a daemon; always run in-process. The escape hatch for scripts.
--help, --versionflagoptional- The source of truth for this page.
The verb table
57 verbs, each with a section below. The method column is the API call the verb makes — that mapping is the whole contract.
| Verb | Aliases | Method | What it does |
|---|---|---|---|
init | — | project.create | Create a project — just a name, no folder. |
use | — | project.use | Set the default project for bare adds. |
archive | — | project.archive | Retire a project — out of rotation, tasks untouched. |
unarchive | — | project.unarchive | Put an archived project back into rotation. |
add | a, new | task.add | Capture a task — title plus inline sugar. |
modify | mod, m, edit | task.modify | Change a task — set fields or --clear them. |
list | ls, l | task.list | List tasks matching a filter. |
agenda | ag, cal | task.list | What is coming up, when — list ordered by time. |
next | — | task.next | The one highest-urgency unblocked task. |
dashboard | dash | task.list + report.summary + project.list + event.list | Open the overview screen, or ask for its panels as data. |
pick | p, fzf | task.list + task.get + task.start | Browse tasks on a full-screen list: read one, search them, start one. |
show | get | task.get | Show one task in full detail. |
why | — | task.get | Explain a task's urgency score. |
start | s | task.start | Mark a task active. |
stop | st | task.stop | Pause an active task. |
claim | — | task.claim | Claim a task so other agents skip it. |
release | — | task.release | Give a claim up. |
adjust | — | task.adjust_tracked | Correct a task's tracked time. |
done | d, x, complete | task.done | Complete a task. |
cancel | delete, del, rm | task.cancel | Cancel a task. |
reopen | — | task.reopen | Reopen a completed or cancelled task. |
triage | — | task.triage_list | List the proposals waiting in triage, oldest first. |
accept | — | task.triage | Accept a proposal from triage: it becomes pending. |
decline | — | task.triage | Decline a proposal from triage: it is cancelled. |
duplicate | — | task.triage | Mark a proposal in triage a duplicate of another task. |
undo | u | event.revert | Take back the last thing this store recorded. |
annotate | — | annotation.add + annotation.update + annotation.move | Attach a timestamped note to a task, correct one, or move it. |
unannotate | — | annotation.remove | Permanently scrub one annotation's text. |
tag | — | tag.add | Attach tags to a task. |
untag | — | tag.remove | Remove tags from a task. |
dep | — | dependency.add | Make one task depend on another. |
undep | — | dependency.remove | Remove a dependency edge. |
projects | — | project.list | List projects (default marked with *). |
brief | — | task.brief | Everything needed before starting a task, in one read. |
check | — | check.add + check.set + check.remove | Acceptance criteria on a task: add, mark, drop. |
git | — | git.event | Let a pull request start or complete its task. |
report | — | report.summary + report.outcomes | Summary counts, or outcomes, optionally grouped, as text or HTML. |
chart | — | event.list | Render throughput, heatmap, or burndown charts. |
theme | — | — (no store) | List or preview terminal themes. |
config | — | — (registry + core.capabilities) | Read and change tasqx settings. |
memory | — | memory.search + get/add/remove/list/update + memory.import + memory.refresh | Store and search knowledge: docs, patterns, and your task annotations (D41). |
note | — | note.add + get/update/remove/promote | Jot down a quick note — an idea, a URL to read later — outside the backlog (D241). |
notes | — | note.list | List your notes, newest first (D241). |
tokens | — | tokens.recompute + token.add | Record token spend after the fact, or repair stored attribution (D50, D167). |
export | — | store.export | Dump tasks as canonical JSON. |
import | — | store.import | Load tasks from a JSON file or stdin. |
sync | — | store.import + store.export | Sync this store with its remote: pull, merge, push. |
api | — | (any) | One JSON envelope in on stdin → one out on stdout. |
daemon | — | (serves all) | Long-lived single-writer server. |
watch | — | task.list + push | Live-updating task view. |
board | — | task.list + push + lifecycle verbs | Live kanban in your browser: drag a card to change the task. |
mcp | — | (subset) | Serve MCP over stdio with an operator-selected scope. |
setup | — | — (no store) | Install the Claude Code, Codex and Gemini integrations: the MCP server and the bundled skills. |
docs | — | — (no store) | Open the exhaustive browser guide (self-contained HTML). |
manual | man | — (no store) | Browse the complete guide in your terminal. |
about | — | — (no store) | Who made tasqx, where to find it, and what build this is. |
completions | — | — (no store) | Turn on Tab completion for your shell. |
Getting started
next
Maps to task.next.
The one highest-urgency unblocked task.
tasqx next [filter…] [--card [--ascii]] [--holder ID]<filter…>stringsoptional- Filter DSL, ANDed with
@working(default:@workingalone) --cardflagoptional- Print the task as a 72-column box-drawn card, for pasting into a document
--asciiflagoptional- With --card: draw the borders with + - | instead of box-drawing characters
--holder<id>optional- Choose as this holder: a task anyone else has claimed is never picked (D236). Default
user, the shell
- The single highest-urgency unblocked task — the "what now" button.
- A filter narrows
@workingrather than replacing it:tasqx next project:workstill skips blocked and backlog tasks in that project, unliketasqx list project:workwhich shows every status once a filter is given. - Tasks in an archived project are left out (D215) unless the filter names the project:
tasqx next project:oldreaches an archivedold.tasqx unarchive oldputs the whole project back. --cardprints the picked task as a fixed 72-column box-drawn card meant to be pasted into a document (a chat, a PR);--asciidraws its borders with+ - |. The default screen is unchanged (D146).
tasqx next project:worktasqx next --cardtasqx nextnext #47 Renew the TLS certificate for api.example.dev H ▄▄▄▄ 18.1 infra due 2d ago +ops tasqx start 47 · tasqx why 47
dashboard
Also dash. Maps to task.list + report.summary + project.list + event.list.
Open the overview screen, or ask for its panels as data.
tasqx dashboard [--json] [--panels <LIST>]--panels<list>optional- Which panels
--jsonanswers with, comma-separated (now,next,due,blocked,recent,projects,burndown,tokens). Narrowsdashboard.panelsfor this call only, and only on the--jsonpath — the interactive screen still readsdashboard.panelsfrom config (#152)
--json is not the screen in text: the screen has 6 panels, and the document carries a payload for 4 of them (tasks, projects, burndown, tokens), beside the status header it always writes and a panels array naming what was asked for. The other two are drawn from the screen's own model, so --panels pulse,effort is a valid request that answers with no panel payload at all.
Every key below is generated from the same table the in-screen ? overlay renders, so this page cannot drift from it.
| Key | Does |
|---|---|
1-6 | focus a panel (or place it in the analytics slot) |
s | cycle the list order: urgency, due, touched |
tab / S-tab | next / previous panel |
j / k | move the cursor in the focused panel |
g / G | cursor to the first / last row |
r | refresh now |
R | toggle auto-refresh |
w | cycle the burndown window |
enter | open the row under the cursor |
p | browse the tasks: enter reads one, s starts it |
N | browse your notes: a archives, t and m promote, x removes |
l | leave and print the task list |
? | this help |
q / esc | close |
ctrl-c | close (inside pick, go back to the dashboard) |
- The same screen a bare
tasqxopens on a terminal. Spelling it explicitly works even whendashboard.enabledis off — that setting protects the meaning of the BARE invocation, and typing the verb is not a breaking change to anything. - It needs a terminal of at least 56x14 on stdin AND stdout, and says which it got when it refuses. A bare
tasqxin a window that small falls back to the working-set table instead, silently: whoever typed nothing did not ask for a dashboard. --jsonskips both of those checks, because it opens no screen. It is the only verb where--jsondecides whether the terminal gate applies, and it is what makes the panel data reachable from a script.--panels tasks,burndownnarrows the--jsondocument to those panels, on that one call — it does not touchdashboard.panelsor the interactive screen. The task rows are row-capped per group withtotal/truncatedalongside them, because the list tracks the store's size rather than the screen's (#152). The five panel names D80 retired —now,next,due,blocked,recent— still parse, and all meantasks, which is where their rows went.- Read-only, with two exceptions:
popenspick, the task browser, over the working set — or, from a PROJECTS row, over that project. Enter there reads a task andsstarts it, which brings you back here;qorescthere comes back having started nothing.Nopens the notes browsertasqx notesopens, where a note can be archived, promoted or removed; closing it comes back here.q,escand ctrl-c close the dashboard. - Every key (also behind
?in the screen itself):1-6focus a panel;scycles the list order between urgency, due and touched;tab/S-tabcycle panels;j/kmove the cursor;g/Gjump to the first/last row;rrefreshes now;Rtoggles auto-refresh;wcycles the burndown window;enteropens the row under the cursor;Nbrowses your notes;lleaves and prints the task list.
tasqx --json dashboardthe same panels as one JSON document
tasqx dashboard┌tasqx · 17 open · 1 active · 1 overdue · 1 blocked · 2 triage · 3 done/week───────────────────────────────────────────────────────┐ ├──2─ PROJECTS ──────────────────────────────────────────────────┬──1─▸TASKS ──────────────────────────────────────────────────────┤ │ *website 5 open · 36h25/38h │ ▍ infra — 3 open · 1 overdue │ │ api 4 open · 4h15/16h │▸ #47 H ▄▄▄▄ 18.1 Renew the TLS certificate fo… +ops 2d ago │ │ infra 3 open · 1 overdue · 5h10/15h │ #54 M ▄▂▁▁ 4.3 Cut build time under five minutes +ci │ │ mobile 3 open · 9h50/18h │ #53 M ▄▂▁▁ 4.2 Move nightly backups to object sto… +ops │ │ home 2 open │ ▍ website — 4 open │ ├──3─ BURNDOWN ──────────────────────────────────────────────────┤ ▶ #48 H ▄▄▄▄ 17.8 Ship the v2 pricing … +launch 0s │ │17┤──────────────────────────────────────────────────────────── │ #56 M ▄▄▁▁ 6.0 Accessibility pass on the s… +a11y 28 Sep │ │ │ ········· │ #57 L ▃▁▁▁ 2.0 Replace the carousel on the home pa… +ui │ │ │ ········· │ #59 L ▃▁▁▁ 2.0 Archive last year's analytics dashboards │ │ │ ········· │ ▍ mobile — 3 open │ │ │ ········· │ #49 H ▄▄▄▄ 17.0 Fix token refresh ra… +bug tomorrow 17:00 │ │ │ ········· │ #52 M ▄▄▃▁ 8.6 Dark mode for the settings sc… +ui 25 Sep │ │ │ ········· │ #60 - ▁▁▁▁ 0.1 Investigate flaky checkout test +bug +ci │ │ 0┤ ····· │ ▍ api — 3 open │ │ ┴ 7 days · now 17 · net +1 │ #50 H ▄▄▄▄ 15.9 Rate-limit the /search endpoint +perf Sat │ ├──4─ PULSE ─────────────────────────────────────────────────────┤ ⊘ #51 M ▄▄▄▄ 12.2 Write the migration guide for … +docs Mon │ │done 3 7d · 6 14d · 13 30d │ #55 M ▄▂▁▁ 4.0 Add OpenAPI examples for every en… +docs │ │cycle 10.0d median · 15.5d last 14d, slower │ ▍ home — 2 open │ │oldest #58 38d · 8 untouched ≥14d │ #58 L ▄▄▄▁ 9.4 Book the dentist Tue │ │scope -1 over the window │ #61 - ▁▁▁▁ 0.4 Sketch ideas for the Q4 offsite │ ├──5─ EFFORT ────────────────────────────────────────────────────┤ │ │48h est left · 0s tracked on it │ │ │website ▄▄▄▄▄▄▄▄ 14h │ │ │mobile ▄▄▄▄▄▄▄▁ 12h │ │ │api ▄▄▄▄▄▄▄▁ 12h │ │ │infra ▄▄▄▄▄▄▁▁ 10h │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘ 1-6 panel tab cycle j/k move g/G ends r refresh R auto w window enter open p pick l list ? help q close
pick
Also p, fzf. Maps to task.list + task.get + task.start.
Browse tasks on a full-screen list: read one, search them, start one.
tasqx pick [filter…] [--holder ID]<filter…>stringsoptional- Filter DSL, e.g. "project:work +api" (default: the working set)
--holder<id>optional- Choose as this holder: a task anyone else has claimed is not offered, and
sstarts as this holder (D236). Defaultuser, the shell
The keys are generated from the tables the screen's own key bar is drawn from.
In the list
| Key | Does |
|---|---|
j / k | move down / up (the arrows too) |
/ | search ids, titles, projects and tags |
enter | read the task under the cursor |
s | start the task under the cursor, and leave |
g / G | first / last task |
q / esc | leave, starting nothing (back to the dashboard, when opened from it) |
In the list, with a search kept
| Key | Does |
|---|---|
j / k | move down / up (the arrows too) |
/ | change the search |
enter | read the task under the cursor |
s | start the task under the cursor, and leave |
g / G | first / last task |
esc | clear the search |
q | leave, starting nothing (back to the dashboard, when opened from it) |
In the search
| Key | Does |
|---|---|
enter / esc | keep the filter and go back to moving |
up / down | move while typing (ctrl-p / ctrl-n too) |
ctrl-u | clear the query (ctrl-w deletes a word) |
On a task's card
| Key | Does |
|---|---|
j / k | scroll down / up |
space / b | a page down / up |
g / G | top / bottom |
s | start this task, and leave |
esc / q | back to the list |
- Each row is the row
tasqx listprints: the running and blocked rail, the priority and urgency gauge, the title, the project, the deadline as a calendar day and the tags. The header names the filter and what the set holds — overdue, due today, running, blocked — aslist's summary does. j/kor the arrows move,g/Gjump to the ends. Enter opens the task'stasqx showcard; therej/k/space/bscroll andescorqgoes back to the list.sstarts the task under the cursor, from the list or from its card — the one key on this screen with a side effect, and the same single-active ruletasqx startfollows.qleaves./opens the search on the header line: a fuzzy SUBSEQUENCE match over id, title, project and tags, sowacfindsWrite API conformance tests, and a term found whole ranks above the same letters scattered. Whitespace splits it into terms that must all match. Every letter is a letter there; up/down or ctrl-p/ctrl-n move, ctrl-u clears, ctrl-w deletes a word, and Enter orescgoes back to the list with the filter kept.escin the list clears the filter, and only then leaves.- Leaving without starting a task exits 0:
pickis a browser, and a browser you close is not a failed run —tasqx pick && …, a prompt indicator and any script that opens it to look all surviveq. A filter that matches no task still exits 4, and so does an empty working set: that is a requestpickcould not serve, not a session you ended. - It needs a real terminal on stdin AND stdout, so
tasqx pick | …and$(tasqx pick)refuse with exit 2 rather than writing escape codes into your pipe (D26). Non-interactively,tasqx nextanswers the same question andtasqx start <ref>acts on it.
tasqx pick project:work +apinarrow the candidates first
tasqx pickpick @working 14 tasks · 1 overdue · 1 due today · ▶ #48 running ID URG TASK PROJECT DUE TAGS ▸ 47 H ▄▄▄▄ 18.1 Renew the TLS certificate for api.example.dev infra 2d ago +ops ▶ 48 H ▄▄▄▄ 17.8 Ship the v2 pricing page website today 17:00 +launch 49 H ▄▄▄▄ 17.0 Fix token refresh race on Android mobile tomorrow 17:00 +bug 50 H ▄▄▄▄ 15.9 Rate-limit the /search endpoint api Sat +perf 58 L ▄▄▄▁ 9.4 Book the dentist home Tue 52 M ▄▄▃▁ 8.6 Dark mode for the settings screen mobile 25 Sep +ui 56 M ▄▄▁▁ 6.0 Accessibility pass on the signup flow website 28 Sep +a11y 54 M ▄▂▁▁ 4.3 Cut build time under five minutes infra +ci 53 M ▄▂▁▁ 4.2 Move nightly backups to object storage infra +ops 55 M ▄▂▁▁ 4.0 Add OpenAPI examples for every endpoint api +docs 57 L ▃▁▁▁ 2.0 Replace the carousel on the home page website +ui 59 L ▃▁▁▁ 2.0 Archive last year's analytics dashboards website 61 - ▁▁▁▁ 0.4 Sketch ideas for the Q4 offsite home 60 - ▁▁▁▁ 0.1 Investigate flaky checkout test mobile +bug +ci j/k move / search enter open s start g/G ends q leave
config
Maps to — (registry + core.capabilities).
Read and change tasqx settings.
tasqx config <list|get <key>|describe <key>|set <key> <value>|unset <key>|path|store|edit>tasqx config list
Show every setting with its value, source and default
tasqx config get
Print one setting's resolved value
<key>stringrequired- Setting key, e.g.
theme.name
tasqx config describe
Print one setting's description, units and default (#236.2)
<key>stringrequired- Setting key, e.g.
daemon.idle_timeout
tasqx config set
Set a setting in config.toml
<key>stringrequired- Setting key, e.g.
theme.name <value>stringrequired- New value
tasqx config unset
Remove a setting so it falls back to its default
<key>stringrequired- Setting key
tasqx config path
Print the path of config.toml (it may not exist yet)
tasqx config store
Print which store this command would actually write to
tasqx config edit
Edit settings on an interactive screen, previewing themes live
describeprints a setting's meaning, unit and default — fordaemon.idle_timeoutthe numberget/listshow is minutes, and the unit is otherwise only visible in the daemon's own startup banner.storeanswers which store you are actually writing to, and why: through a daemon (whose store$TASQX_DB, when set, names too), in-process, or in-process because$TASQX_DBnames a different store than the daemon on the default socket serves (D204).listshows both homes. Most settings live inconfig.toml;default_projectlives in the store and is set withtasqx use(D21).- Resolution order is
--flag, then$TASQX_*, thenconfig.toml, then the built-in default (D9). The SOURCE column names the layer that won. editopens an interactive screen: up/down to move, enter to toggle a switch or open a theme picker, esc to leave. Moving through the theme list repaints the screen in that theme before anything is written.editneeds a real terminal. Piped or redirected it refuses and exits 2 rather than writing escape codes into your pipe — scripts should useset/unset(D26).tokens.enabledallows the daemon to attribute AI token usage from local tool transcripts;otlp.enabled(+otlp.port) runs a local OTLP/HTTP receiver instead — seetasqx manual reportsandtasqx manual daemon. Both require a running daemon; self-reported counts (token.add) work with both off.
tasqx config get theme.nametasqx config describe daemon.idle_timeouttasqx config pathtasqx config storetasqx config set theme.name gruvboxwrites config.toml, preserving your comments
tasqx config editfull-screen editor; arrow through themes and watch them apply
tasqx config listSETTING VALUE SOURCE theme.name nord default dashboard.enabled true default dashboard.panels tasks,projects,burndown,pulse,effort config.toml dashboard.refresh auto default dashboard.window week default notify.enabled false default tokens.enabled false default otlp.enabled false default otlp.port 4318 default board.port 0 default daemon.idle_timeout 0 default default_project website store mcp.add_status pending default detail.time_format both default
docs
Maps to — (no store).
Open the exhaustive browser guide (self-contained HTML).
tasqx docs [--out PATH | --no-open | --stdout] [--screen NAME]--out<path>optional- Write the guide to this path instead of a temp file. Implies --no-open: naming an output file is asking for the file, not for a browser
--no-openflagoptional- Write the file but never launch a browser. Prints the path instead
--stdoutflagoptional- Write the HTML to stdout instead of a file (pipe it anywhere)
--screen<name>optional- Write ONE captured screen as a standalone page — the terminal styling of the guide, nothing else — to --out, or to stdout. An unknown name exits 2 listing the screens there are
- The exhaustive browser guide (self-contained HTML). For a quick in-terminal guide,
tasqx manual. --screen NAMEwrites ONE captured screen instead of the guide: a standalone page carrying the guide's terminal styling and nothing else, to--outor to stdout. It is the rasterisation path behind the README's pictures (scripts/snap.sh), and an unknown name exits 2 listing every screen there is.
See also manual.
tasqx docsopen the browser guide
tasqx docs --stdouttasqx docs --screen listone captured screen, standalone
No captured screen: its output is this page — either the whole HTML document or the temporary path it was written to. Every other block of output on this page is a recording of this build running against the demo store (D149).
about
Maps to — (no store).
Who made tasqx, where to find it, and what build this is.
tasqx about [--notices]--noticesflagoptional- Print the third-party notices instead: the attribution and licence texts of the embedding model compiled into this binary (D196)
- Six lines: the author, two links, the build this binary was made from, the store it would open, and
times UTC— every clock tasqx reads and prints is UTC, including one typed without an offset (D132). The build is the stringtasqx --versionprints — the crate version plus the commit — and readsunknownon a build from a source tarball, which has no git to ask. - It opens no store and no network. The store line is the path a command WOULD open, resolved without creating anything, so asking where things live never authors a data directory.
- A credits screen is not data, so there is no API method and
--jsonis declined with a note (D31's carve-out list, D127). --noticesprints the third-party notices instead: the attribution and licence texts of the embedding model memory search uses, which is compiled into the binary (D196). It is theNOTICEfile the release archives carry, so an install that kept only the binary still has it.
tasqx abouttasqx about --noticesthe third-party licence texts
No captured screen: it prints this machine's store path and the commit this binary was built from. Every other block of output on this page is a recording of this build running against the demo store (D149).
manual
Also man. Maps to — (no store).
Browse the complete guide in your terminal.
tasqx manual [<command|topic>]<topic>stringoptional- A command (e.g.
init), an alias, or a topic slug (e.g.filters)
- No store, no network.
tasqx docsis the fuller browser guide.
See also docs.
tasqx manual inittasqx manual filterstasqx manualTASQX MANUAL TOPICS getting-started Getting started projects Projects capturing Capturing tasks dates Dates & recurrence filters Filter grammar screens Screens & browsing reminders Reminders reports Reports & charts daemon Daemon & watch automation Automation (MCP & API) json-api JSON API completion Shell completion COMMANDS init Create a project — just a name, no folder. add Capture a task — title plus inline sugar. modify Change a task — set fields or --clear them. list List tasks matching a filter. agenda What is coming up, when — `list` ordered by time. next The one highest-urgency unblocked task. dashboard Open the overview screen, or ask for its panels as data. pick Browse tasks on a full-screen list: read one, search them, start one. show Show one task in full detail. why Explain a task's urgency score. start Mark a task active. stop Pause an active task. claim Claim a task so other agents skip it. release Give a claim up. adjust Correct a task's tracked time. done Complete a task. cancel Cancel a task. reopen Reopen a completed or cancelled task. triage List the proposals waiting in triage, oldest first. accept Accept a proposal from triage: it becomes pending. decline Decline a proposal from triage: it is cancelled. duplicate Mark a proposal in triage a duplicate of another task. undo Take back the last thing this store recorded. annotate Attach a timestamped note to a task, correct one, or move it. unannotate Permanently scrub one annotation's text. tag Attach tags to a task. untag Remove tags from a task. dep Make one task depend on another. undep Remove a dependency edge. use Set the default project for bare adds. archive Retire a project — out of rotation, tasks untouched. unarchive Put an archived project back into rotation. projects List projects (default marked with `*`). check Acceptance criteria on a task: add, mark, drop. git Let a pull request start or complete its task. brief Everything needed before starting a task, in one read. report Summary counts, or outcomes, optionally grouped, as text or HTML. chart Render throughput, heatmap, or burndown charts. theme List or preview terminal themes. config Read and change tasqx settings. memory Store and search knowledge: docs, patterns, and your task annotations (D41). note Jot down a quick note — an idea, a URL to read later — outside the backlog (D241). notes List your notes, newest first (D241). tokens Record token spend after the fact, or repair stored attribution (D50, D167). export Dump tasks as canonical JSON. import Load tasks from a JSON file or stdin. sync Sync this store with its remote: pull, merge, push. api One JSON envelope in on stdin → one out on stdout. daemon Long-lived single-writer server. watch Live-updating task view. board Live kanban in your browser: drag a card to change the task. mcp Serve MCP over stdio with an operator-selected scope. docs Open the exhaustive browser guide (self-contained HTML). about Who made tasqx, where to find it, and what build this is. manual Browse the complete guide in your terminal. completions Turn on Tab completion for your shell. setup Install the Claude Code, Codex and Gemini integrations: the MCP server and the bundl… ▸ Jump to a topic or command: tasqx manual <name> ▸ Full browser guide: tasqx docs
Projects
init
Maps to project.create.
Create a project — just a name, no folder.
tasqx init <name> [--desc <text>]<name>stringrequired- Project name, e.g. work.tasqx
--desc<desc>optional- Optional description
- A project is just a name in the store — no folder is created.
- init claims the default project only if the store has none yet.
tasqx init keuken-verbouwentasqx init work --desc "Day job"tasqx init home && tasqx use homeclaim then set default
tasqx init researchresearch created default stays website tasqx use "research"
use
Maps to project.use.
Set the default project for bare adds.
tasqx use <name><project>stringrequired- An existing, non-archived project name
- The project must already exist and not be archived.
tasqx projectsmarks the default with*.
tasqx use keuken-verbouwenmove where a bare add lands
tasqx use apiapi now the default was website a bare tasqx add lands here
archive
Maps to project.archive.
Retire a project — out of rotation, tasks untouched.
tasqx archive <name><project>stringrequired- An existing project name
- Archiving is a shelf, not a delete: the tasks keep their history and their project, and
tasqx projects --allstill lists the project. - An archived project is out of rotation for WRITES —
userefuses it (exit 5), and so does anadd/modifythat names it, and so does a secondarchiveof it (project is already archived, exit 5). No write may name an archived project, this one included;store.importrestoring the flag from a document is the one write that still can. Reads are unaffected:list,reportandagendastill show it and its tasks — archiving is a rotation change, not a hide. tasqx unarchive <name>is the way back (D215).undodoes not reverse an archive: the archive may also have cleared your default project, which the log does not restore.- Tasks in an archived project are left out of
tasqx nextand of@working— the working set is what to do next — unless the filter names the project, as intasqx next project:old.list,reportandagendaare unaffected. - Archiving the project that IS the default clears the default: a bare
tasqx addthen has no project untiltasqx use <project>. The line says which of the two happened.
tasqx archive keuken-verbouwenretire it; archiving your default project clears the default
tasqx archive homehome archived 2 open tasks left in it default unchanged tasqx list project:home
tasqx archive homeerror [conflict]: project is already archived: home (`tasqx projects --all` lists it; archiving it again would change nothing)unarchive
Maps to project.unarchive.
Put an archived project back into rotation.
tasqx unarchive <name><project>stringrequired- An archived project name
- Only an archived project can be unarchived: a project that is not is a
conflict(exit 5), an unknown name is exit 4. Its tasks never left, so nothing is restored besides the project itself. - It does not re-point the default project an archive may have cleared;
tasqx use <name>does that.
tasqx unarchive keuken-verbouwenput it back among the projects use, add and next take
No captured screen: it needs a project that is already archived, and the manifest runs each row against a fresh copy of the demo store. Every other block of output on this page is a recording of this build running against the demo store (D149).
projects
Maps to project.list.
List projects (default marked with `*`).
tasqx projects [--all]--allflagoptional- Include archived projects
--allis the only way to see an archived project: without it the table shows the live ones, which is whataddandusewill accept.
tasqx projects --alltasqx projectsPROJECT DESCRIPTION api Public REST API and its SDKs home infra CI, deploys and on-call mobile iOS and Android apps * website Marketing site and docs portal
Capturing tasks
add
Also a, new. Maps to task.add.
Capture a task — title plus inline sugar.
tasqx add <title…> [--project p] [--due d] [--scheduled s] [--wait w] [-p H|M|L] [-t tag]… [--repeat r] [--remind r] [-e est] [--budget-tokens n] [--triage] [--discovered-from id]<title…>stringsoptional- The task title (may carry inline sugar)
--project<project>optional- File the task under an existing project (see
tasqx projects) -p, --priorityH|M|Loptional- Priority: H, M, or L (or high/medium/low)
--due<due>optional- Due date — natural language ok (e.g. friday, "in 3 days", eom, -1d)
--scheduled<scheduled>optional- Scheduled date — natural language ok
--wait<wait>optional- Wait-until date — natural language ok (task stays in backlog)
--repeat<repeat>optional- Recurrence rule, e.g. "every 3 days" or "weekly on mon,wed,fri"
--remind<remind>optional- Reminder: a due-anchored offset (-1h, -30m, -2d) or an absolute date ("friday 9am"). Without this, the task never notifies (§9)
-e, --estimate<estimate>optional- Effort estimate — human duration (4h, 90m, 1h30m, 2d) or ISO PT4H
--budget-tokens<n>optional- Token budget — a size gauge over FRESH tokens (input + output + cache creation; cache reads are not counted). It stops nothing: tasqx is a store an agent calls between turns and cannot preempt one. An overrun is a signal that the task was probably too big to hand over whole
-t, --tag<tag…>optional- Repeatable tag flag
--triageflagoptional- File it in triage: a proposal kept out of
@workinguntiltasqx accept(D238) --discovered-from<id>optional- The task this was found while working on; links the two with
derived_from(D238)
Flags win over inline sugar when both name the same field. The sugar column is bound to the parser's own key table, so a spelling here is a spelling that parses:
| Flag | Sugar | Notes |
|---|---|---|
--project <p> | project:, proj: | Free-form dotted name. |
--priority <p> / -p | !high, !h | H, M, L (or high/medium/low). |
--tag <t> / -t | +tag | Repeatable. |
--due <d> | due: | Natural language. |
--scheduled <d> | scheduled:, sched: | When you plan to start. |
--wait <d> | wait: | Hide in the backlog until then. |
--repeat <r> | repeat:, every:, recur: | Recurrence rule. |
--remind <r> | remind: | Offset or absolute. |
--estimate <e> / -e | est:, estimate: | 4h, 90m, 1h30m, 2d, or ISO PT4H. |
- Inline sugar:
+tag,project:p(orproj:),!high,due:…,scheduled:…(orsched:),wait:…,repeat:…(orevery:/recur:),remind:…,est:4h(orestimate:). - A bare add lands in the default project (
tasqx useto change it). - --triage files the task as a proposal: it stays out of
@working,nextand the urgency ranking untiltasqx accept(D238). --discovered-from links it to the task it was found on. Tasks an agent adds over MCP land in triage whenmcp.add_statusistriage. - Anywhere a command takes a
<ref>, it accepts either the short id you see in the table (1) or the full UUID. Short ids are for your fingers; UUIDs are stable forever and are whatexportcarries.
tasqx add Buy milktasqx add Ship it due:friday +api !high --project worktasqx add Water plants repeat:"every 3 days"tasqx add Call bank due:"friday 9am" --remind -30mreminder 30m before due
tasqx add Retry the flaky e2e test --triage --discovered-from 42a proposal, held out of @working until accepted (D238)
tasqx add "Draft the Q4 roadmap" project:website !H due:friday +planning▌ #64 Draft the Q4 roadmap ▌ added H ▄▄▄▄ 16.6 website due Fri +planning
modify
Also mod, m, edit. Maps to task.modify.
Change a task — set fields or --clear them.
tasqx modify <ref> [words/sugar…] [--project p] [--due d] [--scheduled s] [--wait w] [-p H|M|L] [-t tag]… [--repeat r] [--remind r] [-e est] [--tracked t] [--budget-tokens n] [--clear <field>]… [--expected-rev N] [--holder ID] [--force-claim]<ref>stringrequired- short_id or UUID
<rest…>stringsoptional- New title words and/or inline sugar (due:friday, +tag, project:p, !high, est:4h, repeat:"every week", remind:-1h). Bare words become the new title; omit them to leave the title alone
--project<project>optional- Move the task to an existing project (see
tasqx projects) -p, --priorityH|M|Loptional- Priority: H, M, or L (or high/medium/low)
--due<due>optional- Due date — natural language ok (friday, "in 3 days", eom, -1d)
--scheduled<scheduled>optional- Scheduled date — natural language ok
--wait<wait>optional- Wait-until date — natural language ok
--repeat<repeat>optional- Recurrence rule, e.g. "every 3 days" or "weekly on mon,wed,fri". Clear it with
--clear recurrence --remind<remind>optional- Reminder: a due-anchored offset (-1h, -30m) or an absolute date
-e, --estimate<estimate>optional- Effort estimate — human duration (4h, 90m, 1h30m, 2d) or ISO PT4H
--tracked<tracked>optional- Correct the tracked-time total — human duration (4h, 90m, 1h30m, 2d) or ISO PT4H. Overwrites the stored total outright rather than adding to it; clear it back to zero with
--clear tracked --budget-tokens<n>optional- Token budget — a size gauge over FRESH tokens (input + output + cache creation; cache reads are not counted). It stops nothing: tasqx is a store an agent calls between turns and cannot preempt one. An overrun is a signal that the task was probably too big to hand over whole
-t, --tag<tag…>optional- Add a tag (repeatable). Routed to tag.add
--clear<field…>optional- Clear a field back to unset (repeatable). The only removal syntax for a steering field; a tag comes off with
tasqx untag <ref> <tag> --expected-rev<rev>optional- Optimistic concurrency: fail with
conflict(exit 5) unless the task is still at this rev, so a concurrent edit is reported instead of clobbered.tasqx show <ref> --jsonreports the current_rev, and every successful modify prints the new one --holder<id>optional- Act as this claim holder, for a task claimed with
claim --holder(D236) --force-claimflagoptional- Override another holder's live claim; the event records whose (D236)
--clear is repeatable over a closed set:
project priority due scheduled wait remind recurrence estimate tracked budget_tokens- Setting is
due:friday/--due friday; removal is only ever--clear <field>— there is no magic empty value. --clearcovers the steering fields only.modify 42 +apiadds a tag; taking one off istasqx untag 42 api.titleandstatusare absent from--clearon purpose: a task without a title is not a task, and lifecycle moves throughstart/done/cancelso their invariants hold. Naming a field in both a set and a--clearis abad_request, not a precedence puzzle.--expected-revfails with conflict (exit 5) if the task moved on.
tasqx modify 42 due:friday !high est:4hset fields
tasqx modify 42 --clear due --clear remindclear fields
tasqx modify 42 repeat:"every monday"set a recurrence
tasqx modify 55 due:friday !high▌ #55 Add OpenAPI examples for every endpoint ▌ modified H ▄▄▄▄ 16.7 due Fri api +docs est 4h rev 2
show
Also get. Maps to task.get.
Show one task in full detail.
tasqx show <ref> [--card [--ascii]]<ref>stringrequired- short_id or UUID
--cardflagoptional- Print the task as a 72-column box-drawn card, for pasting into a document
--asciiflagoptional- With --card: draw the borders with + - | instead of box-drawing characters
- Full detail: tags, annotations, dependencies, blocked state,
_rev. --cardprints a fixed 72-column box-drawn card meant to be pasted into a document (a chat, a PR);--asciidraws its borders with+ - |. The default screen is unchanged (D146).
tasqx show 1tasqx show 1 --cardtasqx show 51▌ #51 Write the migration guide for SDK 3.0 ▌ ▌ ⊘ blocked by #50 · Rate-limit the /search endpoint ▌ ▌ status pending urgency M ▄▄▄▄ 12.2 ▌ project api due Mon (in 5 days) ▌ estimate 5h tags +docs ▌ created 16 Aug (31 days ago) ▌ modified 16 Aug (31 days ago) ▌ rev 1 ▌ git branch task/51-sdk-migration-guide · open ▌ git pr https://github.com/acme/api/pull/412 · open ▌ ▌ 1 [x] Every renamed symbol has a row in the table ▌ 2 [x] One worked example per SDK (js, py, go) ▌ 3 [ ] The rate-limit ceiling is stated with its real number ▌ 4 [ ] Reviewed by whoever ships 3.0 ▌ ▌ [1] Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week ▌ late is a support ticket that already happened. ▌ [2] Blocked until the rate limit lands: the guide has to state the real ceiling, and quoting a ▌ number that then changes is worse than waiting. ▌ [3] Scope: the 2.x → 3.0 rename table, the auth change, and a worked example per SDK. Not the ▌ reference — that generates itself.
start
Also s. Maps to task.start.
Mark a task active.
tasqx start <ref> [--keep] [--holder ID] [--client TOOL] [--session-id ID] [--transcript-path PATH]<ref>stringrequired- short_id or UUID
--keepflagoptional- Keep other active tasks running (opt out of single-active)
--holder<id>optional- Start as this claim holder, to start a task claimed with
claim --holder. The clock itself stays anonymous, as a shell's always was (D236) --client<tool>optional- Calling tool as "<name> <version>", e.g. "claude-code 2.1". Selects the transcript parser; without it no transcript is ever read
--session-id<id>optional- Agent session id, verified against the transcript to earn high confidence. Requires --client
--transcript-path<path>optional- Absolute path to the session transcript the tokens will be found in. Requires --client
- The correlation flags are for AI agents: they tell the attribution engine which session and transcript to measure this interval from. Without them a task is never attributed and reports zero tokens.
- --session-id and --transcript-path require --client, which is what selects the transcript parser. Given without it, attribution would store a permanent zero instead of a measurement.
tasqx start 1single-active by default
tasqx start 1 --keepkeep others running
tasqx start 1 --client 'claude-code 2.1' --session-id $SIDrecord who is working, for token attribution
tasqx start 49▌ #49 Fix token refresh race on Android ▶ started H ▄▄▄▄ 17.0 mobile due tomorrow 17:00 UTC +bug est 4h #48 stopped · Ship the v2 pricing page
stop
Also st. Maps to task.stop.
Pause an active task.
tasqx stop <ref> [--holder ID] [--force-claim]<ref>stringrequired- short_id or UUID
--holder<id>optional- Act as this claim holder, for a task claimed with
claim --holder(D236) --force-claimflagoptional- Override another holder's live claim; the event records whose (D236)
tasqx stop 1tasqx stop 48▌ #48 Ship the v2 pricing page ▌ stopped H ▄▄▄▄ 17.8 website due today 17:00 UTC +launch est 6h
claim
Maps to task.claim.
Claim a task so other agents skip it.
tasqx claim <ref> [--lease DURATION] [--holder ID]<ref>stringrequired- short_id or UUID
--lease<duration>optional- How long the lease runs: 30m, 2h (estimate's grammar). Default 30m, at most 24h
--holder<id>optional- Who holds it. Default
user, the shell; an agent's own id otherwise
- A claim is a lease (D236): 30m unless
--leasenames another length, at most 24h. Claiming again as the same holder renews it. Without--holderthe shell holds it asuser; over MCP the holder is the connection.tasqx start --holderstarts a task claimed under a named holder, the timer itself staying the shell's;next,listandpicktake--holderto see that holder's claims as yours. - While the lease is live, a claim or
startby anyone else exits 5 naming the holder and the expiry, and the task leaves theirnext,pickandlist --holder.tasqx list @claimedlists every live claim. - Nothing sweeps an expired lease: it is read against the clock, so once it runs out anyone can claim the task, and that claim is logged as a takeover.
doneandcancelend a claim;tasqx releasegives one up.
tasqx claim 12 --holder builder-2hold #12 for builder-2 for 30 minutes
tasqx claim 12 --holder builder-2 --lease 2hrenew it for two hours
tasqx claim 47 --holder builder-2▌ #47 Renew the TLS certificate for api.example.dev ▌ claimed by builder-2 until today 09:30 UTC (in 30 minutes) H ▄▄▄▄ 18.1 infra due 2d ago
release
Maps to task.release.
Give a claim up.
tasqx release <ref> [--holder ID] [--force-claim]<ref>stringrequired- short_id or UUID
--holder<id>optional- The holder giving it up. Default
user, the shell --force-claimflagoptional- Release a claim someone else holds, e.g. one a dead session left behind
- Only the holder releases (D236): a task with no claim, or another holder's, exits 5. Completing or cancelling a task ends its claim without this.
--force-claimreleases a claim someone else holds — one left by an agent that crashed or reconnected under a new id — and the log records it as forced, under your holder.
See also claim.
tasqx release 12 --holder builder-2builder-2 is done with #12 without finishing it
No captured screen: it needs a task that is already claimed, and the manifest runs each row against a fresh copy of the demo store. Every other block of output on this page is a recording of this build running against the demo store (D149).
adjust
Maps to task.adjust_tracked.
Correct a task's tracked time.
tasqx adjust <ref> <delta> --reason TEXT<ref>stringrequired- short_id or UUID
<delta>stringrequired- Signed duration: -2h25m takes time off, +30m (or 30m) adds it
--reason<reason>required- Why the time is being corrected (required; kept in the event log)
- Works on any task, done included, so calibration in
report --outcomescan be corrected after the fact. A correction that would take the banked total below zero is refused. - Each correction is its own event with its reason, and
tasqx undotakes the newest one back.showprints the net of every correction beside the total:tracked 2h30m (adjusted -2h25m).
tasqx adjust 1 -2h25m --reason 'idle gap'take off time the clock ran while nobody worked
tasqx adjust 1 +30m --reason 'forgot to start'add time that never reached the clock
tasqx adjust 42 -2h25m --reason "idle gap"▌ #42 Paginate the audit log ▌ adjusted -2h25 tracked 6h25 of 4h H ▄▄▁▁ 6.2 mobile
done
Also d, x, complete. Maps to task.done.
Complete a task.
tasqx done <ref> [--force] [--holder ID] [--force-claim] [--client TOOL] [--session-id ID] [--transcript-path PATH] [--tool TOOL] [--model MODEL] [--input-tokens N] [--output-tokens N] [--cache-read-tokens N] [--cache-creation-tokens N] [--total-tokens N]<ref>stringrequired- short_id or UUID
--forceflagoptional- Complete the task even though some of its dependencies are still open (recorded;
report --outcomescounts it under FORCED) --holder<id>optional- The claim holder completing it, for a task claimed with
claim --holder(D236) --force-claimflagoptional- Override another holder's live claim; the event records whose (D236)
--client<tool>optional- Calling tool as "<name> <version>", e.g. "claude-code 2.1". Selects the transcript parser; without it no transcript is ever read
--session-id<id>optional- Agent session id, verified against the transcript to earn high confidence. Requires --client
--transcript-path<path>optional- Absolute path to the session transcript the tokens will be found in. Requires --client
--tool<tool>optional- The AI tool doing the work, free-form (e.g. "claude-code"). Recorded on the completion event on its own; when token counts are present it also names the measurement, defaulting to --client if omitted
--model<model>optional- The model doing the work, e.g. "claude-opus-5". Recorded on the completion event on its own, and carried on the measurement when token counts are present
--input-tokens<n>optional- Self-reported input tokens this task cost (0 or more)
--output-tokens<n>optional- Self-reported output tokens this task cost (0 or more)
--cache-read-tokens<n>optional- Self-reported cache-read tokens this task cost (0 or more)
--cache-creation-tokens<n>optional- Self-reported cache-creation tokens this task cost (0 or more)
--total-tokens<n>optional- One unsplit token count, for a harness that reports only a total — instead of the four counts above, never beside them (D167)
- A task whose dependencies are still open is refused, naming them. --force completes it anyway; the override lands on the done event and
tasqx report --outcomescounts it under FORCED (D150). Cancelling a blocked task needs no flag. - The correlation flags carry the same meaning as on
start, and are recorded per occurrence: a task can start and finish many times, and attribution pairs the two events of one interval. - --tool/--model and the four *-tokens flags are the self-report channel (D50/D65): any present token count records a measurement, attributed to --tool (or --client if --tool is omitted). --tool/--model alone, with no count, still land on the completion event. Skip all of them and the response carries a tokens_hint explaining what was not recorded — printed under the Done line.
tasqx done 1completes; spawns the next recurrence if any
tasqx done 1 --forcecomplete it although a dependency is still open; the override is recorded
tasqx done 1 --client 'claude-code 2.1' --session-id $SIDclose the interval an agent opened with the same ids
tasqx done 1 --tool claude-code --model claude-opus-5 --input-tokens 4200 --output-tokens 900self-report the turn's spend — the primary measurement channel (D50)
tasqx done 47▌ #47 Renew the TLS certificate for api.example.dev ▌ done today infra due 2d ago +ops est 1h note: no token counts were self-reported — tasqx done --help names the flags
cancel
Also delete, del, rm. Maps to task.cancel.
Cancel a task.
tasqx cancel <ref> [--holder ID] [--force-claim]<ref>stringrequired- short_id or UUID
--holder<id>optional- The claim holder cancelling it, for a task claimed with
claim --holder(D236) --force-claimflagoptional- Cancel it although another holder has a live claim on it (D236)
- There is no destructive delete.
delete/rmare aliases for cancel: the task keeps its history, stays in the event log, andtasqx reopen <ref>undoes it.
tasqx cancel 1a cancelled dependency releases its dependents (D11)
tasqx delete 1same thing — tasqx has no hard delete; reverse it with reopen
tasqx cancel 59▌ #59 Archive last year's analytics dashboards ▌ cancelled website est 1h
reopen
Maps to task.reopen.
Reopen a completed or cancelled task.
tasqx reopen <ref><ref>stringrequired- short_id or UUID
tasqx reopen 1tasqx reopen 46▌ #46 Tune the autoscaler ▌ reopened M ▄▂▁▁ 4.2 website est 2h
triage
Maps to task.triage_list.
List the proposals waiting in triage, oldest first.
tasqx triageTakes no arguments of its own; the global flags still apply.
- Each row names the tool that filed it — the
clientan MCP add records — and the task it was found on (--discovered-from, aderived_fromlink). Settle each withaccept,declineorduplicate(D238).tasqx list status:triageis the same set as a table.
tasqx triagetasqx triage2 proposals in triage, oldest first — `tasqx accept|decline|duplicate <id>` settles one #62 Retry the flaky checkout e2e test 2d ago · by claude-code 2.1 · found on #48 Ship the v2 pricing page #63 Cache the pricing API response yesterday · by claude-code 2.1 · found on #48 Ship the v2 pricing page
accept
Maps to task.triage.
Accept a proposal from triage: it becomes pending.
tasqx accept <ref><ref>stringrequired- short_id or UUID
- A future
waitorscheduledstill holds it in backlog.tasqx undoputs it back in triage. A task not in triage exits 5 (D238).
tasqx accept 12tasqx accept 63▌ #63 Cache the pricing API response ▌ accepted api
decline
Maps to task.triage.
Decline a proposal from triage: it is cancelled.
tasqx decline <ref> [--reason text] [--holder id] [--force-claim]<ref>stringrequired- short_id or UUID
--reason<reason>optional- Why it was declined; stored as a note on the task
--holder<id>optional- The claim holder acting, for a task claimed with
claim --holder(D236) --force-claimflagoptional- Act although another holder has a live claim on it (D236)
tasqx undoputs it back in triage; the reason note stays. A task not in triage exits 5 (D238).
tasqx decline 12 --reason "out of scope for this release"the reason is kept as a note on the task
tasqx decline 63 --reason "the CDN already caches it"▌ #63 Cache the pricing API response ▌ declined api
duplicate
Maps to task.triage.
Mark a proposal in triage a duplicate of another task.
tasqx duplicate <ref> --of <ref> [--holder id] [--force-claim]<ref>stringrequired- short_id or UUID
--of<id>required- The task it repeats
--holder<id>optional- The claim holder acting, for a task claimed with
claim --holder(D236) --force-claimflagoptional- Act although another holder has a live claim on it (D236)
- The proposal is cancelled, its notes move to the original (as
annotate --movewould) and aduplicateslink joins the two.tasqx undodoes not take it back: the newest event is the link, which it refuses by name (D238).
tasqx duplicate 12 --of 7cancels 12, moves its notes and dependents to 7 and links the two
tasqx duplicate 62 --of 60▌ #62 Retry the flaky checkout e2e test ▌ duplicate of #60 website
undo
Also u. Maps to event.revert.
Take back the last thing this store recorded.
tasqx undoTakes no arguments of its own; the global flags still apply.
- It takes no ref, and that is the design: only the NEWEST event can be reversed exactly, because nothing has happened since to have read or overwritten what the inverse puts back.
- Twelve operations are undoable —
done,cancel,modify,tag,stop,untag,undep,annotate,annotate --edit,annotate --move,adjustandaccept(D238). Every other one exits 5 naming itself and the verb that does take it back (start->tasqx stop,archive->tasqx unarchive). - Undoing a
doneputs the task back in the status it left, takes a running interval back off the total, and reopens the criteria the completion proved. A recurring task's next instance is removed with it when nothing has touched that instance; if it was touched, or the completion recorded a token measurement, undo refuses and says which. Undoing acancelrestores the status it left. Undoing amodifyputs back every field it changed, and the answer lists them; amodifyortagrecorded by an older tasqx did not note what it replaced, so undo refuses those and says so. - Undo APPENDS: the event it reverses stays in the log and a new
undoevent lands behind it, sotasqx chartand the audit trail readX happened, then it was undone. - There is no redo, so
tasqx undotwice in a row exits 5: the second one would find the first undo as the newest event and the pair would toggle forever. - It reverses the newest RECORDED event, which is not always the last command you typed. A command that changed nothing records nothing —
tasqx undep 1 2where no such edge exists, ortasqx starton a task already running — soundoreaches past it to the previous change. That is why the answer names what it undid: read it before assuming it hit what you were aiming at. - A single event outside the undoable twelve permanently blocks undo for everything BEFORE it, not just for itself: the newest event on an active store is often
addorstart, and once one of those lands, an annotation from ten seconds earlier can never be reached (#228.2). This is a same-breath affordance — undo the thing you just did — not an undo stack.
tasqx undoreverses the newest event, and says which one
tasqx untag 42 api && tasqx undothe tag comes back off the shelf
No captured screen: it reverses the newest event, so a capture would have to record two commands and the manifest records one. Every other block of output on this page is a recording of this build running against the demo store (D149).
annotate
Maps to annotation.add + annotation.update + annotation.move.
Attach a timestamped note to a task, correct one, or move it.
tasqx annotate <ref> [--edit <annotation>] [--move <annotation> --to <task>] [<text…>]<ref>stringrequired- short_id or UUID
<text…>stringsoptional- The annotation text;
-, or nothing on a pipe, reads it from stdin; nothing on a terminal opens $VISUAL/$EDITOR --edit<annotation>optional- Replace the body of this annotation instead of adding one; its id, timestamp and position are kept
--move<annotation>optional- Move this annotation to the task named by
--toinstead of adding one; its id, text and timestamp are kept (maps to annotation.move, D211) --to<task>optional- The task
--movesends the annotation to: short_id or UUID
- A long or multi-line note:
tasqx annotate <ref> -reads the whole of stdin, verbatim (D206); so does a baretasqx annotate <ref>when stdin is a pipe. On a terminal a bareannotate <ref>opens$VISUALor$EDITOR(elsevi); an empty or unchanged file stores nothing. Empty stdin is refused. A note that is just-can no longer be written as an argument.--edittakes the same forms, the editor pre-filled with the note's current text. - A note is named by its full id, a unique prefix of 8+ characters, or the
[n]positiontasqx showprints beside it, oldest first (D211). A prefix shared by two notes exits 5 and lists them; notes written within about a minute share their first eight characters, so the position is the short way. - Wrote something you shouldn't have?
tasqx unannotate <ref> <annotation>scrubs it — there is no other way back. - Put it on the wrong task?
--move <annotation> --to <task>sends the note there with its id, text and timestamp (D211), andtasqx undoputs it back. - Wrote something wrong?
--edit <annotation>replaces that note's text in place (D165): its id, timestamp and position stay, so a corrected first note is still the card's Description, andtasqx undoputs the old text back.
See also show · modify · unannotate.
tasqx annotate 1 Called the plumber, waiting on a quotetasqx annotate 50 "Ship it behind a flag; the ceiling is 60 requests a minute per key."▌ #50 Rate-limit the /search endpoint ▌ annotated Ship it behind a flag; the ceiling is 60 requests a minute per key.
unannotate
Maps to annotation.remove.
Permanently scrub one annotation's text.
tasqx unannotate <ref> <annotation><ref>stringrequired- short_id or UUID
<annotation_id>stringrequired- The annotation: its full id, a unique prefix of 8+ characters, or the
[n]positiontasqx show <ref>prints
- A HARD delete (D113): the text is overwritten in the store, not merely hidden — use it to take back a secret, a customer name, or a wrong root cause pasted into a note by mistake. This also redacts the original
annotateevent's own body, sotasqx chart(event.list) andtasqx exportstop showing it too — not onlytasqx show. - Name the note by the
[n]positiontasqx show <ref>prints, a unique id prefix of 8+ characters, or its full id (annotations[].idin--json). tasqx undodoes NOT cover this: the body is already gone from the log by the time the removal event exists, so there is nothing left to restore.- An unknown id, or one already removed, exits 4 (not_found) rather than answering ok for nothing.
tasqx unannotate 1 018f2f7e-...No captured screen: it takes the id of one annotation, which is a UUID out of the store it is run against. Every other block of output on this page is a recording of this build running against the demo store (D149).
tag
Maps to tag.add.
Attach tags to a task.
tasqx tag <ref> <tag…><ref>stringrequired- short_id or UUID
<tag…>stringsrequired- Tag names, with or without the leading
+
- A tag is written the same way here as in
add/modifysugar:+apiandapiname one tag, and duplicates collapse. - Re-adding a tag the task already has is not an error — the answer is the resulting tag set, so nothing has to be guessed.
tasqx tag 1 api releasetwo tags, one call
tasqx tag 1 +apithe leading + is optional — same tag either way
tasqx tag 55 openapi sdk▌ #55 Add OpenAPI examples for every endpoint ▌ tagged +docs +openapi +sdk M ▄▂▁▁ 4.0 api est 4h
untag
Maps to tag.remove.
Remove tags from a task.
tasqx untag <ref> <tag…><ref>stringrequired- short_id or UUID
<tag…>stringsrequired- Tag names, with or without the leading
+
- Removing a tag the task does not have exits 4 and removes nothing, naming the tags it does have. A typo may not answer ok.
- There is no
--clear tags: a tag comes off by name, which is why this verb exists.
tasqx untag 1 apiremoves it, and prints what remains
tasqx untag 1 api releaseall or nothing: one unknown tag removes neither
tasqx untag 55 docs▌ #55 Add OpenAPI examples for every endpoint ▌ untagged +docs M ▄▂▁▁ 4.0 api est 4h
dep
Maps to dependency.add.
Make one task depend on another.
tasqx dep <ref> <depends_on><ref>stringrequired- The dependent task (short_id or UUID)
<depends_on>stringrequired- The task it depends on (short_id or UUID)
<ref>becomes blocked until<depends_on>is done or cancelled; a<ref>that is itself already closed is never blocked.
tasqx dep 2 1task 2 waits on task 1
tasqx dep 52 49▌ #52 Dark mode for the settings screen ⊘ blocked by #49 · Fix token refresh race on Android M ▄▄▃▁ 8.6 mobile due 25 Sep +ui
undep
Maps to dependency.remove.
Remove a dependency edge.
tasqx undep <ref> <depends_on><ref>stringrequired- The dependent task (short_id or UUID)
<depends_on>stringrequired- The task it depended on (short_id or UUID)
See also dep.
tasqx undep 2 1tasqx undep 51 50▌ #51 Write the migration guide for SDK 3.0 ▌ no longer waits on #50 M ▄▄▄▄ 12.2 api due Mon +docs est 5h
check
Maps to check.add + check.set + check.remove.
Acceptance criteria on a task: add, mark, drop.
tasqx check <add <ref> <criterion…>|set <ref> <check_id|position> <state> [--evidence e]|remove <ref> <check_id|position>>tasqx check add
Add an acceptance criterion to a task
<ref>stringrequired- short_id or UUID
<body…>stringsoptional- The criterion, in your own words. May begin with
--
tasqx check set
Mark a criterion passed or failed, with the evidence for it
<ref>stringrequired- short_id or UUID
<check_id>stringrequired- The check's id, from
tasqx show— or its 1-based position there <state>open|passed|failedrequired- open | passed | failed
--evidence<evidence>optional- Your proof: a test name, an excerpt of output, a commit sha
tasqx check remove
Also rm.
Drop a criterion that was the wrong thing to ask
<ref>stringrequired- short_id or UUID
<check_id>stringrequired- The check's id — or its 1-based position in
tasqx show
- tasqx NEVER RUNS a check. The criterion is a claim and the evidence is a citation; both are stored verbatim and neither is interpreted. A hook you installed is what runs commands, calling
check setlike any other client. - Completing with a criterion still open or failed is not refused — nothing is blocked — but
donesays so andtasqx report --outcomescounts it as an unproven completion: only a task whose every check passed is proven. failedis a normal outcome. Usermonly when the criterion was the wrong thing to ask; removing one the work failed hides the finding.
tasqx check add 1 the notes name every breaking changeadd a criterion
tasqx check set 1 019f-abc passed --evidence "suite green"mark it
tasqx check rm 1 019f-abcdrop a criterion
tasqx check set 1 2 failedthe second criterion, by position
tasqx check add 50 "p99 stays under 200ms at the new ceiling"▌ #50 Rate-limit the /search endpoint ▌ check added 0/1 passed 1 [ ] p99 stays under 200ms at the new ceiling
brief
Maps to task.brief.
Everything needed before starting a task, in one read.
tasqx brief <ref> [--memory-limit N] [--card [--ascii]]<ref>stringrequired- short_id or UUID
--memory-limit<n>optional- How many memory hits to show
--cardflagoptional- Print the task as a 72-column box-drawn card, for pasting into a document
--asciiflagoptional- With --card: draw the borders with + - | instead of box-drawing characters
- The task, what each of its prerequisites concluded, what it blocks, and memory found under a query tasqx derives from the task's own title, tags and project — so nobody has to guess search terms.
- Half the memory page (rounded up) is reserved for knowledge docs, so a ruling is never buried under a sibling task's own notes; annotations fill whatever slots docs leave (D147).
- Memory is scoped to the task's project and does not widen when that finds nothing:
tasqx memory searchis the wider read. --cardprints a fixed 72-column box-drawn card meant to be pasted into a document (a chat, a PR);--asciidraws its borders with+ - |. The default screen is unchanged (D146).
tasqx brief 1tasqx brief 1 --memory-limit 3tasqx brief 1 --cardtasqx brief 51▌ #51 Write the migration guide for SDK 3.0 ▌ ▌ ⊘ blocked by #50 · Rate-limit the /search endpoint ▌ ▌ status pending urgency M ▄▄▄▄ 12.2 ▌ project api due Mon (in 5 days) ▌ estimate 5h tags +docs ▌ created 16 Aug (31 days ago) ▌ modified 16 Aug (31 days ago) ▌ rev 1 ▌ git branch task/51-sdk-migration-guide · open ▌ git pr https://github.com/acme/api/pull/412 · open ▌ ▌ 1 [x] Every renamed symbol has a row in the table ▌ 2 [x] One worked example per SDK (js, py, go) ▌ 3 [ ] The rate-limit ceiling is stated with its real number ▌ 4 [ ] Reviewed by whoever ships 3.0 ▌ ▌ [1] Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week ▌ late is a support ticket that already happened. ▌ [2] Blocked until the rate limit lands: the guide has to state the real ceiling, and quoting a ▌ number that then changes is worse than waiting. ▌ [3] Scope: the 2.x → 3.0 rename table, the auth change, and a worked example per SDK. Not the ▌ reference — that generates itself. DEPENDS ON #50 Rate-limit the /search endpoint pending FROM MEMORY Docs — 1 of 1 release-process docs/release.md [api] description How an SDK release is cut, tagged and announced Cut the…
note
Maps to note.add + get/update/remove/promote.
Jot down a quick note — an idea, a URL to read later — outside the backlog (D241).
tasqx note <text…> [--project p] | tasqx note <id> [edit <text…>|archive|unarchive|rm|promote <task|memory> [--title t]]<text…>stringsrequired- The note's text, stored verbatim; several words are joined by spaces
--project<project>optional- File it under this project. Omitted, the note has none
tasqx note edit
Replace a note's text (maps to note.update)
<id>stringrequired- The note's short id or UUID
<text…>stringsrequired- The new text, stored verbatim
tasqx note archive
Archive a note: out of tasqx notes, kept under --archived (maps to note.update)
<id>stringrequired- The note's short id or UUID
tasqx note unarchive
Bring an archived note back (maps to note.update)
<id>stringrequired- The note's short id or UUID
tasqx note rm
Delete a note for good (maps to note.remove); tasqx undo cannot bring it back
<id>stringrequired- The note's short id or UUID
tasqx note promote
File a note as a task or a memory doc, and archive it (maps to note.promote)
<id>stringrequired- The note's short id or UUID
<to>task|memoryrequired- What it becomes
--title<title>optional- The title. Omitted, the note's first line, cut at 80 characters
- A note is not a task: it has no urgency, stays out of
@working,nextand the reports, andmemory searchnever sees it. Notes have their own numbers, so note 3 and task 3 are different things. - The text is stored verbatim, words joined by spaces; quote it to keep a shell from reading
#,*or?. Noproject:sugar: a note's words are its own, so --project files it. - The id comes first and the subverb after it, as on every task verb. Any other first word is text, so
tasqx note edit the readmenotes "edit the readme"; a subverb alone or followed by a number (tasqx note archive 7) is refused (exit 2) with the right spelling. A number followed by text is refused (exit 2):tasqx note 42 "…"once annotated task 42, and that istasqx annotate 42 "…"now. After--every word is text:tasqx note -- 7notes "7". A flag the subverb does not take is a usage error (exit 2), wherever it stands. promote taskfiles a task in the note's project (else your default), titled --title or the note's first line, the whole note as its first annotation when the title does not say all of it;promote memoryfiles a memory doc. Either way the note is archived with a pointer to what it became, in one transaction. Promotion is final: a promoted note cannot be promoted again or unarchived.rmis permanent andtasqx undodoes not cover notes;archiveis the way to put one out of sight.
tasqx note https://example.com/blog/sqlite-tips --project infraa link to read later, filed under a project
tasqx note 3print note 3 whole
tasqx note 3 edit try the OTLP exporter insteadreplace its text
tasqx note 3 archiveout of tasqx notes, kept under --archived
tasqx note 3 promote task --title "Evaluate the OTLP exporter"file it as a task; the note is archived
tasqx note 3 promote memoryfile it as a memory doc instead
tasqx note 3 rmdelete it for good
tasqx note -- -5 degrees tonightafter --, a number or a leading dash is text
tasqx note try the new tracing exporterNoted #3 · try the new tracing exporter
notes
Maps to note.list.
List your notes, newest first (D241).
tasqx notes [--links] [--archived|--all] [--search <text>] [--project p]--linksflagoptional- Only notes holding an
http://orhttps://link --archivedflagoptional- Archived notes instead of open ones
--allflagoptional- Open and archived notes
--search<text>optional- Only notes whose text contains this, ignoring case
--project<project>optional- Only notes filed under this project
- One line per note: its number, its first line, a link mark (
↗,Lwithout Unicode) when it holds anhttp://orhttps://link, its project and the day it was added. A note that is only a link is never cut, so it stays copyable; its row runs past the terminal instead. - Open notes by default; --archived shows the archived ones (promoted notes among them), --all both. --search is a plain substring, not a ranked search.
tasqx note <id>prints one note whole.- On a terminal it opens a full-screen browser over the same notes instead, and the flags set where it opens; piped, or with --json, it prints the table.
j/kmove,g/Gjump to the ends,/searches the text,enterreads a note whole (a link is shown whole, never opened),escclears the search or leaves,qleaves.aarchives or unarchives the note under the cursor,tandmpromote it to a task or a memory doc,xremoves it after a y/n,lkeeps only the notes holding a link. Every write carries the revision the screen read, so a note changed elsewhere is a conflict on the screen, not a lost write. The dashboard'sNopens the same browser.
tasqx notes --linkstasqx notes --search exporter --alla substring, ignoring case, in open and archived notes
tasqx notes2 notes ID NOTE PROJECT ADDED 2 idea: a dark-mode toggle in the docs header yesterday 1 read https://www.rfc-editor.org/rfc/rfc9457 before the error-format chan… ↗ api 2d ago
Dates & recurrence
agenda
Also ag, cal. Maps to task.list.
What is coming up, when — `list` ordered by time.
tasqx agenda [filter…] [--days N]<filter…>stringsoptional- Filter DSL, e.g. "project:work +api" (default: the working set)
--days<days>optional- How many days ahead to look (1-3650; default 14). Overdue tasks are shown regardless
- A task is placed on the EARLIER of its
dueandscheduled— the first day it asks anything of you — and the WHEN column says which of the two that was. - Overdue tasks are always shown, whatever
--dayssays: a horizon is a question about the future. - This view holds a row back for exactly two reasons, and both are COUNTED under the table rather than dropped in silence: no date at all, and past the horizon. Each count names the way to see them — the horizon line quotes the exact
--daysthat reaches the furthest one, or, when the row is further out than the widest window--daysaccepts, says so and points attasqx list. - Done and cancelled tasks are left out unless your filter names a status, the same rule
reportapplies to cancelled tasks (D24).tasqx agenda status:doneshows them. - Days are UTC days, because a date typed without a time is stored as midnight UTC; grouping by local time would file
--due 2026-08-05under the 4th west of Greenwich.
tasqx agenda --days 3tasqx agenda project:worktasqx agendathrough 30 Sep (+14d) 8 tasks · 1 overdue · ▶ #48 running · ⊘ 1 blocked ID URG TASK PROJECT WHEN TAGS Overdue 47 H ▄▄▄▄ 18.1 Renew the TLS certificate for api.example.dev infra due 2d ago +ops Today · Wed 16 Sep ▶ 48 H ▄▄▄▄ 17.8 Ship the v2 pricing page website due 17:00 +launch Tomorrow · Thu 17 Sep 49 H ▄▄▄▄ 17.0 Fix token refresh race on Android mobile due 17:00 +bug Sat 19 Sep 50 H ▄▄▄▄ 15.9 Rate-limit the /search endpoint api +perf Mon 21 Sep ⊘ 51 M ▄▄▄▄ 12.2 Write the migration guide for SDK 3.0 api +docs Tue 22 Sep 58 L ▄▄▄▁ 9.4 Book the dentist home Fri 25 Sep 52 M ▄▄▃▁ 8.6 Dark mode for the settings screen mobile +ui Mon 28 Sep 56 M ▄▄▁▁ 6.0 Accessibility pass on the signup flow website +a11y 7 undated — no due or scheduled date, so nothing puts them on a day; `tasqx list` shows them
Filter grammar
list
Also ls, l. Maps to task.list.
List tasks matching a filter.
tasqx list [filter…] [--sort key]… [--limit N] [--offset N] [--fields f,…] [--holder ID]<filter…>stringsoptional- Filter DSL, e.g. "project:work status:pending +api"
--sort<sort…>optional- Sort key(s), e.g.
dueor-duefor descending (repeatable, or comma-separated). Maps straight ontotask.list's ownsortparam — seetasqx docsfor the valid names. Default:-urgency, unchanged from before this flag existed --limit<limit>optional- Cap the number of rows returned. Maps to
task.list'slimit. Defaults to 100 when omitted, and is clamped to 10,000 even when given explicitly --offset<offset>optional- Skip this many matching rows before
--limitis applied. Maps totask.list'soffset --fields<fields…>optional- Return only these task fields (repeatable, or comma-separated) — see
tasqx docsfor the valid names. Maps totask.list'sfields; an unknown name is refused by the engine, same as above --holder<id>optional- Choose work as this holder: with
@working(the default), tasks anyone else has claimed are left out (D236). Without it the list is a view and shows them
Callers of the JSON API — and --sort here — may use any of urgency, short_id, priority, due, created, modified, title, tokens, estimate, tracked, each optionally prefixed with - for descending. An unknown key is rejected rather than ignored.
--fields trims each row to the keys you need. The valid fields are _rev, active_since, blocked, budget_tokens, claim_expires, claimed_by, completed, created, depends_on, due, estimate, id, modified, priority, project, recurrence, remind, scheduled, short_id, status, status_unrecognized, tags, title, tokens, tracked, tracked_adjustment, urgency, wait. An unknown field is rejected rather than dropped, so a typo fails loudly instead of rendering an empty column forever.
- Bare
tasqxistasqx listover the working set. - A value containing a space is double-quoted, and the quotes must reach tasqx:
tasqx list 'project:"Home Renovation"'. Nothing is guessed back together: a shell-strippedproject:Home Renovationis the projectHomeplus a title word. - A bare word matches the title, case-insensitively, and several must all appear:
tasqx list weekly review. Quote a phrase to match it as one,tasqx list '"memory explorer"', or spell ittitle:review.status:any(orstatus:all) lists every status, done and cancelled included. --sort,--limit,--offsetand--fieldsmap straight ontotask.list's own params (core.capabilitiesnames all five) —tasqx list --sort due --limit 20 --fields short_id,title,due. An unknown sort key or field name is refused, naming the valid set.- The piped table (no TTY) is a fixed 100 cells wide; on a terminal it sizes to
$COLUMNS(clamped 40–160) or the terminal's own width, whichever it can read. Widen a truncated title withCOLUMNS=200 tasqx listor narrow the row with--fields.
tasqx list project:work status:pending +apitasqx list due.before:fridaytasqx list@working 14 tasks · 1 overdue · 1 due today · ▶ #48 running ID URG TASK PROJECT DUE TAGS 47 H ▄▄▄▄ 18.1 Renew the TLS certificate for api.example.d… infra 2d ago +ops ▶ 48 H ▄▄▄▄ 17.8 Ship the v2 pricing page website today 17:00 +launch 49 H ▄▄▄▄ 17.0 Fix token refresh race on Android mobile tomorrow 17:00 +bug 50 H ▄▄▄▄ 15.9 Rate-limit the /search endpoint api Sat +perf 58 L ▄▄▄▁ 9.4 Book the dentist home Tue 52 M ▄▄▃▁ 8.6 Dark mode for the settings screen mobile 25 Sep +ui 56 M ▄▄▁▁ 6.0 Accessibility pass on the signup flow website 28 Sep +a11y 54 M ▄▂▁▁ 4.3 Cut build time under five minutes infra +ci 53 M ▄▂▁▁ 4.2 Move nightly backups to object storage infra +ops 55 M ▄▂▁▁ 4.0 Add OpenAPI examples for every endpoint api +docs 57 L ▃▁▁▁ 2.0 Replace the carousel on the home page website +ui 59 L ▃▁▁▁ 2.0 Archive last year's analytics dashboards website 61 - ▁▁▁▁ 0.4 Sketch ideas for the Q4 offsite home 60 - ▁▁▁▁ 0.1 Investigate flaky checkout test mobile +bug +ci
Reports & charts
why
Maps to task.get.
Explain a task's urgency score.
tasqx why <ref> [--card [--ascii]]<ref>stringrequired- short_id or UUID
--cardflagoptional- Print the task as a 72-column box-drawn card, for pasting into a document
--asciiflagoptional- With --card: draw the borders with + - | instead of box-drawing characters
- Explains the urgency score component by component (DESIGN D1).
--cardprints the fixed 72-column box-drawn card (D146) first, with the arithmetic underneath instead of the plain text header;--asciidraws the card's borders with+ - |.
tasqx why 1tasqx why 1 --cardtasqx why 51#51 Write the migration guide for SDK 3.0 priority M 3.9 deadline due Mon 8.0 age 30 days 0.3 urgency 12.2 blocked by #50 Rate-limit the /search endpoint — `next` skips this task
report
Maps to report.summary + report.outcomes.
Summary counts, or outcomes, optionally grouped, as text or HTML.
tasqx report [group_by] [filter…] [--all] [--outcomes] [--since WHEN] [--until WHEN] [--metrics list] [--html] [--with-notes] [--out FILE]<args…>stringsoptional- Optional group_by (project|status|priority) then optional filter DSL
--htmlflagoptional- Emit a single self-contained HTML report (inline CSS + SVG, no external requests) instead of the terminal table
--out<path>optional- Write the HTML report to this file (default: stdout). Requires --html
--allflagoptional- Count cancelled tasks too. By default a report excludes cancelled tasks, unless the filter itself names a status (DESIGN D24)
--since<when>optional- Window
tracked_total/the token buckets to work done or spend measured at or after this instant, instead of each task's lifetime total — same date grammar as--due(D97) --until<when>optional- The other end of
--since's window: excludes anything at or after this instant --metrics<metrics…>optional- Show the four token buckets (cache read/write, in, out) as their own columns instead of the single largest-bucket cell (#212, D48a) — comma-separated, from the same vocabulary
report.summary'smetricsparam accepts. Naming a non-token metric here is accepted but has no effect: COUNT/EST/OVERDUE/ TRACKED already always show, and this flag only changes the TOKENS display --outcomesflagoptional- Report outcomes instead of counts (D137): rework, estimate calibration, cost, silent completions and abandoned work, over the tasks that CLOSED
--with-notesflagoptional- Embed annotation bodies in the HTML page (newest three per task), behind a toggle. Off by default (D212): the page is a file meant to be shared, and bodies are where client detail lives
- group_by ∈ project|status|priority.
--htmldefaults to stdout. - A filter scopes BOTH output modes — the HTML page and the terminal table answer the same question.
- Cancelled tasks are not counted, unless you pass
--allor your filter names a status. - The terminal's TOKENS column names the largest of four buckets by volume;
--metricsnaming a tokens_* bucket shows all four as their own columns instead, same as--htmland--json. --since/--untilwindowtracked_totaland the token buckets by WHEN the time or spend happened (D97) — a different axis fromcompleted.after:/completed.before:in the filter, which selects tasks by completion date. With--htmlthey set the page's period instead (default: the 7 days ending today).--htmlleads with what changed in the period, then a standup, a per-project grid that scopes the page, a net-flow chart, outcomes and a search over every task in scope (D212). Annotation bodies stay out of the file unless you pass--with-notes;--alladds cancelled tasks to its search;--metricsis refused with it.- --socket is refused with
--html(DESIGN.md D73): the HTML page renders from a direct local read of the store, never through a daemon.
tasqx report projecttasqx report --alltasqx report --metrics tokens_in,tokens_out,tokens_cache_read,tokens_cache_creationtasqx report --since -7dtasqx report --outcomestasqx report --outcomes project --since -30dtasqx report --html --out review.htmlself-contained HTML
tasqx report +urgent --html --out sprint.htmlscoped HTML
tasqx report --html --since 2026-09-01 --until 2026-10-01 --out september.htmlHTML for one month
tasqx reportPROJECT COUNT EST OVERDUE TRACKED TOKENS api 10 16h 0 4h 15m cacheR 7.0M ~medium home 2 - 0 - - infra 11 15h 1 5h 10m cacheR 2.8M ~medium mobile 16 18h 0 9h 50m cacheR 7.7M ~medium website 18 38h 0 36h 25m cacheR 2.4M ~medium TOTAL 57 87h 1 55h 40m cacheR 20.1M ~medium TOKENS shows the largest of four buckets (cacheR/cacheW/in/out) by volume; --metrics tokens_in,tokens_out,tokens_cache_read,tokens_cache_creation or --html shows all four.
chart
Maps to event.list.
Render throughput, heatmap, or burndown charts.
tasqx chart <throughput|heatmap|burndown> [filter…] [--weeks n|--days n] [--year] [--project p]tasqx chart throughput
Tasks added vs done per ISO week (from the events table)
<filter…>stringsoptional- Filter DSL, e.g. "project:work" (default: the whole store). Same grammar
list/report/agendatake (#663/D173): only the scoped tasks' events move a bar --weeks<weeks>optional- Number of weeks to show (1-520; default 12)
tasqx chart heatmap
GitHub-style completion density per day (from done events)
<filter…>stringsoptional- Filter DSL — see
throughput's --yearflagoptional- Show a full year (52 weeks)
--weeks<weeks>optional- Number of weeks to show (1-520; default 12; overrides --year)
tasqx chart burndown
Remaining open tasks over the last N days (reconstructed backwards from each task's current status, not read forwards off the events table — D59/D60)
<filter…>stringsoptional- Filter DSL, e.g. "project:work" (default: the whole store) — see
throughput's --project<project>optional- Restrict to a project (else all tasks). Shorthand for appending
project:<name>(quoted) tofilterabove — kept for scripts and docs already spelling it this way;project:workon the positional says the same thing and composes with a second predicate the flag alone cannot (chart burndown project:work +urgent) --days<days>optional- Number of days to show (1-3650; default 30)
- --socket is refused here rather than honoured (DESIGN.md D73): charts render from a direct local read of the store, never through a daemon.
- Each subcommand takes the same filter DSL
list/report/agendado (DESIGN.md D173), e.g.tasqx chart burndown project:work— only that project's tasks and events count. Omit it for the whole store, the same default as before. --project workis shorthand for theproject:workfilter term above, kept for scripts already spelling it that way. The filter term is the more general form — it composes with a second predicate, which--projectalone cannot.
See also report.
tasqx chart throughputtasqx chart heatmap --yeartasqx chart burndown --days 30tasqx chart burndown project:worktasqx chart burndownremaining open · all tasks 17 left · up 5 over 30 days 18 ┤ ╭───────────╮ ╭────── │ ╭───────────────────────╮ ╭────────╯ ╰───────────╯ │ ──────────╮ ╭──╯ ╰──────╯ │ ······╰─────╯······ │ ··················· │ ··················· │ ··················· 0 ┤ ·········· └─────────────────────────────────────────────────────────────────────────────────────────── 18 Aug 16 Sep
theme
Maps to — (no store).
List or preview terminal themes.
tasqx theme <list|show [name]|set <name>>tasqx theme list
List available themes (built-ins + user files)
tasqx theme show
Preview a theme's roles (default: the active theme)
<name>stringoptional- Theme name to preview
tasqx theme set
Persist a theme choice to config.toml
<name>stringrequired- Theme name: a built-in or a user file in the themes directory
theme listmarks the theme in effect with*, the marktasqx projectsputs on the default project.
tasqx theme show nordtasqx theme listTHEME * nord gruvbox dracula solarized mono
Daemon & watch
daemon
Maps to (serves all).
Long-lived single-writer server.
tasqx daemon [--db PATH]--db<path>optional- Store path for the daemon's single Engine (default: $TASQX_DB or the platform data dir). The socket address comes from the global
--socket/ $TASQX_SOCK / the platform default
- Long-lived single-writer; one-shot commands auto-route through it. Ctrl-C stops it cleanly.
tasqx daemonbind the socket/named pipe and serve
No captured screen: it binds a socket and serves until it is stopped; there is no last line to capture. Every other block of output on this page is a recording of this build running against the demo store (D149).
watch
Maps to task.list + push.
Live-updating task view.
tasqx watch [filter…]<filter…>stringsoptional- Filter DSL (default: the working set)
tasqx watch project:worklive view; needs a running daemon
No captured screen: it redraws until you leave it, and it needs a running daemon to follow. Every other block of output on this page is a recording of this build running against the demo store (D149).
board
Maps to task.list + push + lifecycle verbs.
Live kanban in your browser: drag a card to change the task.
tasqx board [--port PORT] [--no-open] [--scope read|write]--port<port>optional- Port to bind on 127.0.0.1 (default:
board.port, else a free one) --no-openflagoptional- Print the URL but do not launch a browser
--scoperead|writeoptionaldefaultwritewritelets a drag change tasks;readrefuses every write server-side (a wall screen or a standup display)
- Serves one page on 127.0.0.1 only, behind a secret token and an Origin check, and repaints from the daemon's pushes. Single-user and local: behind a reverse proxy, authentication is the proxy's job.
- A drag sends the verb you would type (start, stop, done, reopen, cancel, or a priority, wait or scheduled change) with the card's revision. The card's panel edits checks, notes, due, estimate, wait, scheduled and dependencies the same way, so a task changed elsewhere since is refused rather than overwritten. Events record
boardas the actor. --scope read refuses every write server-side. --portorboard.portfixes the port and keeps the token between runs, so a bookmark survives; otherwise a free port and a new token each run.
tasqx boardopen the board; needs a running daemon
tasqx board --scope reada wall screen: every change refused
No captured screen: it prints a URL and serves a browser page until you leave it, so there is no terminal screen to capture, and it needs a running daemon to follow. Every other block of output on this page is a recording of this build running against the demo store (D149).
Automation (MCP & API)
git
Maps to git.event.
Let a pull request start or complete its task.
tasqx git event <opened|merged|closed> --branch <branch> [--pr <url>] [--sha <sha>] [--task <ref>]tasqx git event
Report a PR event: opened starts the task, merged completes it when every check passed, closed adds a note. tasqx never asks GitHub
<event>opened|merged|closedrequired- opened | merged | closed
--branch<branch>required- The PR's branch;
task/<id>-<slug>names the task --pr<pr>optional- The PR's URL
--sha<sha>optional- The merge commit's sha
--task<task>optional- The task, when the branch does not name it (short_id or UUID)
- tasqx never asks GitHub, or any network. Something that watched the PR reports it: a script reading
gh pr view --json, a CI step, a git hook. - A branch named
task/<id>-<slug>names its task, whatever follows the id (task/42-pricing-rebased,task/42-v3).--tasknames it when the branch does not. mergedcompletes the task only when every check passed (or it has none) and nothing blocks it; otherwise the task stays open and a note names what did not pass.openedon a task that cannot start, andclosed, only add a note. Every write is recorded under the actorgit.- Report
mergedagain once the checks are fixed and it completes the task. A PR starts its task once, and a task its merge completed that you reopened stays open: a repeat never overrides you. When the task is claimed, the event acts as the claim holder. An event whose outcome already holds, or one a recorded merge settled (closedaftermerged), changes nothing and says why.
tasqx git event opened --branch task/42-pricing --pr https://github.com/acme/web/pull/7a pending task becomes active
tasqx git event merged --branch task/42-pricing --pr https://github.com/acme/web/pull/7 --sha 4f9c2e1complete it, the PR in the delivery note
tasqx git event closed --branch fix-typo --pr https://github.com/acme/web/pull/8 --task 43a branch that names no task
tasqx git event merged --branch task/51-sdk-migration-guide --pr https://github.com/acme/api/pull/412 --sha 4f9c2e1▌ #51 Write the migration guide for SDK 3.0 ⊘ noted Merged https://github.com/acme/api/pull/412 (4f9c2e1), but not completed: 2 checks not ▌ passed: open "The rate-limit ceiling is stated with its real number"; open "Reviewed by ▌ whoever ships 3.0"; blocked by #50
memory
Maps to memory.search + get/add/remove/list/update + memory.import + memory.refresh.
Store and search knowledge: docs, patterns, and your task annotations (D41).
tasqx memory <add <title> <body> [--source s] [--project p] [--standing]|search <words…> [--limit n] [--scope s] [--mode m] [--min-similarity n] [--source-prefix p] [--raw]|list [--limit n] [--offset n] [--project p] [--standing]|show <id>|update <id> [--title t] [--body b] [--source s] [--project p] [--standing true|false] [--expected-rev n]|rm <id>|import <path> [--project p]|import --refresh|verify [<id>] [--project p]>tasqx memory add
Store a knowledge doc (maps to memory.add). Body is stored verbatim
<title>stringrequired- Doc title
<body>stringrequired- Body text — multi-line markdown is fine
--source<source>optional- Where this came from: a path, URL, or ticket
--project<project>optional- Optional project scope. Omitted, the doc stays global
--standingflagoptional- Mark the doc standing: a ruling meant for every session of its scope, kept until cleared and sent to MCP clients at session start (D156, D157)
tasqx memory list
Browse docs without a query (maps to memory.list). Newest first
--limit<limit>optional- Max rows to return. Omit for everything from
--offseton. Asking for a page prints the table, never the browser --offset<offset>optionaldefault0- How many matching docs to skip. Like
--limit, prints the table --project<project>optional- Restrict to docs stored with this project
--standingflagoptional- Only standing docs. Like
--limit, prints the table
tasqx memory update
Correct a doc in place (maps to memory.update)
<id>stringrequired- The doc UUID, as printed by
memory add/memory search/memory list --title<title>optional- New title. Omit to leave it unchanged
--body<body>optional- New body. Omit to leave it unchanged
--source<source>optional- New source. Omit to leave it unchanged
--project<project>optional- New project scope. Omit to leave it unchanged
--standingtrue|falseoptional- Set or clear the standing flag. Omit to leave it unchanged
--expected-rev<rev>optional- Optimistic concurrency: fail with
conflict(exit 5) unless the doc is still at this rev. Supplied automatically from the doc's current rev when omitted
tasqx memory search
Search docs + annotations by meaning and words (maps to memory.search)
<query…>stringsrequired- Search words. Matched as phrases, so hyphens and dots are safe
--limit<limit>optional- Max hits (default 10)
--scopeall|docs|annotationsoptional- What to search (default: all)
--rawflagoptional- Treat the query as raw FTS5 syntax (prefix*, AND/OR, columns). Words only: it searches as
--mode lexical, and the engine refuses it beside a mode that runs meaning --modehybrid|lexical|semanticoptional- How to match (default: hybrid, meaning and words fused; lexical forces exact words)
--min-similarity<n>optional- The floor a match by meaning must reach, 0 to 1 (default 0.30)
--source-prefix<prefix>optional- Only docs whose source starts with PREFIX (literal, case-sensitive); annotations are left out
tasqx memory show
Also get.
Show one doc whole, by id (maps to memory.get)
<id>stringrequired- The doc UUID, as printed by
memory addandmemory search
tasqx memory verify
Check the files and symbols docs cite against this git checkout (D235): ok, missing_path, missing_symbol or unverifiable per doc
<id>stringoptional- One doc UUID. Omit to check every doc
--project<project>optional- Only docs stored with this project
tasqx memory rm
Remove a doc by id (maps to memory.remove)
<id>stringrequired- The doc UUID, as printed by
memory addandmemory search
tasqx memory import
Import markdown files as docs: one doc per file (maps to memory.add)
<path>stringoptional- A file, or a directory whose *.md files are imported (non-recursive)
--refreshflagoptional- Re-read every already-imported doc whose file changed since, and name the ones whose file is gone (maps to memory.refresh)
--project<project>optional- Scope every imported doc to this project. Omitted, they stay global, same as
memory addwith no--project; re-importing the same directory with a different one MOVES the scope (#657)
- A hit is two lines: the title, where it came from and the handle that opens it, then the words that matched — or, for a hit found by meaning alone,
≈and its similarity before its closest passage. The handle is a doc's id, whichtasqx memory show <id>reads, orannotation on #N, whichtasqx show Nopens —memory showrefuses an annotation's id and names the task instead (exit 4). - Search covers your imported docs AND task annotations, by meaning and by words (D196): an embedding model built into the binary finds a note that says the same thing in other words, and full-text search (bm25, with stemming: "reviewing" matches "review") finds the words themselves. --mode lexical is words only, --mode semantic meaning only; --min-similarity (0 to 1, default 0.30) is how close a match by meaning must be. Plain words are matched as phrases (hyphens and dots are safe); pass --raw for FTS5 operator syntax, which is words only. --source-prefix keeps only docs whose source starts with it, literally (D240). The response's total/has_more say what --limit left out.
- list browses every doc without a query — the enumeration search can't do without one — newest-modified first, paged the same way as
tasqx list. - update replaces title/body/source/project in place, guarded by the same optimistic-concurrency rev
tasqx modifyuses. rm is permanent; update is the correction path that keeps the id and doesn't pollute search with a stale duplicate. - Import is one transaction: a bad file imports nothing, and re-importing a directory replaces docs from the same source instead of duplicating them. --project scopes the whole batch; omitted, an existing doc keeps whatever scope it already had (like --standing), a new one stays global, and naming a different project on a re-import moves the doc's scope there.
import --refreshtakes no path: it re-reads every doc that recorded an origin file (D180) whose file has changed since, keeping each doc's id, scope and standing flag and bumping its rev; a file that only got a newer timestamp counts as unchanged, because the bytes are compared. A doc whose file can no longer be read is named and kept, never deleted — the file may be on another machine or an unmounted volume — and the command still exits 0.- A file's stored source is the git toplevel's directory name followed by its path inside that repo (clouter/README.md; a worktree uses its main checkout's name), or, outside a git work tree, the current directory's name followed by the path under it — so docs/, ./docs/ and its absolute path are one document per file from any starting directory, and on any machine whose checkout has the same directory name (independent clones under one name share keys and conflict; differently named checkouts get distinct keys), and two repos' README.md are two documents (D179, D203); an older spelling already in memory is named in a note rather than removed.
- Every doc a re-import replaced is named under the summary line, with the title it had. A doc whose recorded file still exists and is not the file being imported — two clones under one directory name — is not replaced: the whole import is refused with conflict (exit 5) naming it (D203).
- An MCP agent reaches the same store: tasqx_search_memory works even read-only, so agents can consult knowledge while executing tasks.
- A standing doc (
--standing) is a ruling meant for every session of its scope — a correction given once, sent to MCP clients at session start — andlist --standingshows them; more than 15 in one scope earns a hint to merge or retract (D156).
tasqx memory add "Deploy runbook" "deploys go through the blue-green pipeline"tasqx memory search blue-greentasqx memory search D41 --mode lexicalexact words only; an id with no word of three letters is always matched this way
tasqx memory search 'pipel*' --rawFTS5 operator syntax: prefix search, AND/OR, column filters
tasqx memory import docs/adrone doc per .md file; title from the first # heading
tasqx memory import docs/adr --project ledgerscopes every doc in the batch; a re-import naming a different project moves it (#657)
tasqx memory import --refreshre-reads every imported doc whose file changed; takes no path
tasqx memory verifyflags docs citing files or functions gone from this checkout (D235)
tasqx memory update 019f8422-7b3e-7c41-a2d9-6f1b0e5c8a12 --body "corrected text"in-place correction; add/remove leave the stale doc searchable or lose it for good
tasqx memory rm 019f8422-7b3e-7c41-a2d9-6f1b0e5c8a12by the id search printed
tasqx memory list5 docs TITLE PROJECT UPDATED ID pricing-page-decisions website 2d ago 0787b26d-9e2e-4be5-ab66-ec953102fad3 release-process api 6d ago eb864f1e-e68a-4d96-af89-597bd0d2d52e on-call-handover infra 3 Sep 1bce1a9b-5134-4ab7-8665-34cd79fe0c5f android-token-refresh mobile 27 Aug b3bd4390-2124-42ac-829d-f542ecde8a07 dentist 13 Aug 091eb5ff-05d5-4cb2-ba2f-0afdc77f7935
tasqx memory search release canary"release" "canary" 3 hits release-process docs/release.md eb864f1e-e68a-4d96-af89-597bd0d2d52e …Cut the release branch on Monday, tag after the canary has run… Write the migration guide for SDK 3.0 annotation on #51 some words …the guide ships WITH the 3.0 release, not after it. A… pricing-page-decisions notes/pricing.md 0787b26d-9e2e-4be5-ab66-ec953102fad3 some words …The release of v2 waits for legal to sign off the new… some words marks a hit that has only some of the words
tokens
Maps to tokens.recompute + token.add.
Record token spend after the fact, or repair stored attribution (D50, D167).
tasqx tokens recompute [--apply] | tasqx tokens add <ref> [--total N] [--in N] [--out N] [--cache-read N] [--cache-creation N] [--tool TOOL] [--model MODEL]tasqx tokens add
Record token spend on a task after the fact (maps to token.add), e.g. a count that arrived after the task was completed. Stored as a self-report at medium confidence (D50, D167). Give --total alone when only one number is known, or the split counts — never both
<ref>stringrequired- short_id or UUID
--total<n>optional- One unsplit count, kept apart from the four buckets (D167)
--in<n>optional- Input tokens
--out<n>optional- Output tokens
--cache-read<n>optional- Cache-read tokens
--cache-creation<n>optional- Cache-creation tokens
--tool<tool>optionaldefaultunknown- The AI tool that spent them, free-form. Default "unknown"
--model<model>optional- The model that did the work
tasqx tokens recompute
Re-run log-parse attribution over stored history and locate transcripts for done tasks (maps to tokens.recompute): samples claimed by more than one task's window drop out, a task whose transcript is gone keeps its counts with confidence downgraded to low, and a done Claude Code task with no measured (log-parse or OTLP) row gets a HIGH measurement if its transcript is found. Dry-run by default: prints the per-task delta and writes nothing
--applyflagoptional- Actually write the repair. The dry-run default is not a convenience but the safety: this is the one verb in the API built to delete measurement rows, so destruction must be asked for by name
addrecords a self-report at confidencemedium— whatdone's token flags record, for a count that arrives later (D167).--totalis one unsplit number, kept apart from the four buckets and counted in full by a budget; give it alone or give the split flags, never both.- A bare
tasqx tokens recomputeprints the per-task delta and writes NOTHING;--applyis the explicit opt-in for the one verb in the API built to delete measurement rows. - The pass runs two phases: rewrite D50-refusal rows (
source=log-parseonly, self-report/OTLP never touched), and downgrade confidence tolowfor any task whose transcript is gone; then locate the transcript of every done Claude Code task with no measured (log-parse or OTLP) row and append a HIGH measurement if found. Tasks it cannot measure — no transcript found, or every sample contested — are reported asskipped. - Stop any daemon on the store before
--apply: the verb parses transcripts and runs in-process only (a daemon refuses it over the socket), and applying beside a live daemon is two writers — convergence on rerun is the safety net, not a license.
tasqx tokens add 602 --total 37898a count that arrived after completion, as one unsplit number
tasqx tokens add 602 --in 18400 --out 2600 --tool claude-codethe same, split
tasqx tokens recomputetasqx tokens recompute --applywrite the repair, after reviewing the dry-run delta
No captured screen: the demo store records no AI token spend, so the dry run has nothing to repair and its whole screen is the one line that says so. Every other block of output on this page is a recording of this build running against the demo store (D149).
mcp
Maps to (subset).
Serve MCP over stdio with an operator-selected scope.
tasqx mcp serve [--scope read|write]tasqx mcp serve
Run the MCP stdio server: newline-delimited JSON-RPC 2.0 on stdin/stdout
--scoperead|writeoptionaldefaultread- Operator-selected capability scope for this stdio process
- Scope configures the local process; it is not authentication. Omitted scope is read-only.
tasqx mcp serveserve read-only over stdio JSON-RPC
tasqx mcp serve --scope writeserve with explicit write access
No captured screen: it speaks JSON-RPC on stdin and stdout for as long as the agent holds the pipe. Every other block of output on this page is a recording of this build running against the demo store (D149).
setup
Maps to — (no store).
Install the Claude Code, Codex and Gemini integrations: the MCP server and the bundled skills.
tasqx setup [--list | --yes [--force]] [--only NAME]... [--home DIR]--listflagoptional- Print each item and its status, then exit
--yesflagoptional- Install every item that is not installed, without the screen
--forceflagoptional- With --yes, also replace a skill file that differs from the bundled one
--only<name…>optional- Only this item (mcp, tasqx-workflow, retro, codex:mcp, gemini:retro, ...); repeat for more
--home<dir>optional- Treat DIR as the home directory, the one holding
.claude,.codexand.gemini
- Three items per tool:
mcpregisterstasqx mcp serve --scope writeat user level by running the tool's ownmcp add;tasqx-workflowandretroare skills written to~/.claude/skills,~/.codex/skillsor~/.gemini/skills(<name>/SKILL.md) from copies compiled into this binary, so they match the tasqx you run (D159). - Each tool is offered only when
claude,codexorgeminiis on PATH; with none, a hint says how to install one. Their items are namedcodex:mcp,gemini:retroand so on for --only; Claude's stay bare (mcp,retro). - A skill that exists and is not byte-equal to the bundled copy reads
differs— an older copy and your own edit look the same — and is kept unless you pass --force or tick it on the screen. So is an MCP registration that runs anything else, such as the read-onlytasqx mcp serve; replacing it runsclaude mcp removefirst. - It never writes a tool's config file itself (
~/.claude.json,~/.codex/config.toml,~/.gemini/settings.json). Piped, with no flags, it prints the list and exits 0. - ripwire not found on PATH: install it from
https://github.com/redhat-et/ripwireand put it on PATH.
See also mcp.
tasqx setup --listwhat is installed; writes nothing
tasqx setupa checklist: space ticks, enter installs what is ticked
tasqx setup --yesinstalls everything not installed, and keeps a skill you edited
tasqx setup --yes --force --only retroreplaces the retro skill with the copy this build carries
No captured screen: it reports what is installed in the home directory of the machine it runs on, which the demo store knows nothing about. Every other block of output on this page is a recording of this build running against the demo store (D149).
JSON API
export
Maps to store.export.
Dump tasks as canonical JSON.
tasqx export [filter…] [--include-unscoped] [--out PATH]<filter…>stringsoptional- Optional filter DSL
--include-unscopedflagoptional- Widen a filtered export's docs/projects to ones that carry no project (D171). Refused on an unfiltered export — there is nothing to widen from
--out<path>optional- Write the document to this file instead of stdout. The file is replaced whole (written beside it, then renamed over it), so an interrupted export never leaves half a backup
- Canonical JSON; a filtered export trims edges leaving the set and reports
dropped_dependencies(D12). - The document carries
projectsanddefault_projecttoo, so a restore gives back the store and not only its tasks (D37). - A filter narrows
projects/docs/links/eventsto what the exported tasks need — an unfiltered export still carries all of them;--include-unscopedwidens a filtered one back to docs with no project (D171). - The document carries the graph's explicit links too, so a restore gives back the edges and not only the nodes; a filtered export keeps a link only when BOTH its ends are in it, and counts the rest as
dropped_links(D181). - What a round trip leaves out is counted, never silent:
--outnamesremoved_annotations(a removed note's tombstone; its removal event travels),skipped_events(the bookkeeping a past import wrote about itself) anddropped_events, which on an unfiltered export is a removed doc's history before its removal (D197). The first two stay out of the document, which would otherwise change on every import. A restore's own bookkeeping comes back asevents_logged, and exporting the restored store gives the same document (D207).
tasqx exporttasqx export project:worktasqx export --out backup.jsonto a file, replaced whole
No captured screen: the document carries every project and knowledge doc whatever the filter says, so the shortest export on the demo store runs past two hundred lines. Every other block of output on this page is a recording of this build running against the demo store (D149).
import
Maps to store.import.
Load tasks from a JSON file or stdin.
tasqx import <file|-> [--dry-run] [--merge]<file>stringrequired- Path to a canonical JSON document (
{tasks, projects, default_project, docs}, astasqx exportwrites) or a bare array of tasks, or-for stdin --dry-runflagoptional- Run the import and roll it back (D184): report every renumbering, merge and refusal against this store without writing anything
--mergeflagoptional- Merge a task this store already holds instead of replacing it (D185): union its notes, checks, tags and edges, take each field from the side whose event last wrote it, and count tracked time from both stores' histories together (D189)
- A task whose number a DIFFERENT task here already holds keeps its id and takes the next free one; every move is printed and reported as
renumbered(D177). A task this store already holds keeps the number it has here. - A memory doc whose
sourcea DIFFERENT doc here already holds merges onto that doc rather than being refused: the stored id is kept, the latermodifiedwins the text, and every merge is printed and reported asdocs_merged(D183). - Links are restored after everything they point at, counted in
links_imported; a link naming an end neither the document nor this store holds refuses the whole import by name (D181). --dry-runruns the whole import against this store and rolls it back: every renumbering, merge and refusal prints exactly as a real import would, and the run ends withnothing was written(D184).--mergefolds a second live store in rather than restoring over this one: a task this store already holds keeps its own annotations, checks, tags and edges and takes the payload's beside them, its_revguard is skipped, and each of its fields follows the side whose event last wrote it, while tracked time from both sides adds up — one line per task names the fields that differed and where each came from, reported asmerged(D185, D189). Without it the payload replaces a known task's child rows wholesale, which is what a restore wants.- A project this store already knows keeps its own non-empty description over the payload's — a project carries no
modifiedstamp, so there is no later-wins call to make — reported asproject_description_conflicts(D190).archivedis still taken from the payload either way. - Two stores that both completed one recurring task each spawned its next occurrence. With or without
--merge, two tasks sharing their predecessor and dates fold into the older: it takes the other's notes, checks, tags and edges, the status of whichever copy changed it last and the tracked time of both copies' histories, and the fold is printed and reported asdeduplicated(D191).
tasqx import backup.jsonfrom a file
tasqx export | tasqx import -from stdin
tasqx import backup.json --dry-runpreview it first
tasqx import other-machine.json --mergefold in another store
No captured screen: it names a file, and a path from the machine that captured it is the one thing no second machine can reproduce. Every other block of output on this page is a recording of this build running against the demo store (D149).
sync
Maps to store.import + store.export.
Sync this store with its remote: pull, merge, push.
tasqx sync [--status] | tasqx sync setup <connector> [--set KEY=VALUE]...--statusflagoptional- Print the connector, the last version synced and when, and sync nothing
tasqx sync setup
Choose the remote: ask the connector what it needs, hand it your answers, and record it beside this store once it accepts them
<connector>stringrequired- The connector's name:
dirrunstasqx-remote-dirfrom PATH --set<key=value…>optional- Answer one of the connector's questions without being asked (
--set path=/mnt/share/tasqx); repeat for more. Required for every question when stdin is not a terminal. A secret is refused here, since argv lands in shell history andps: answer it at the prompt, which does not echo, or inTASQX_SYNC_<KEY>(the key upper-cased, e.g.TASQX_SYNC_SECRET_ACCESS_KEY)
- One remote per store. The remote is a connector,
tasqx-remote-<name>on PATH (D198);sync setupasks it what it needs, hands it your answers, and once it accepts them records its name in<store>.sync.jsonbeside the store — never in config.toml, so two stores can sync to two remotes. The connector keeps its own settings and secrets; tasqx keeps none (D201). - A secret field is never taken from
--set, which shell history andpswould keep: answer it at the prompt, which does not echo, or put it inTASQX_SYNC_<KEY>(the key upper-cased, e.g.TASQX_SYNC_SECRET_ACCESS_KEY), read once and never stored. - Every snapshot is encrypted before the connector sees it (XChaCha20-Poly1305 under an Argon2id key, D202), so a folder or a bucket only ever holds ciphertext.
sync setupasks for the passphrase twice at a prompt that does not echo, or readsTASQX_SYNC_PASSPHRASE; never--set. Every machine syncing with the remote needs the same one. It is kept in<store>.sync.keybeside the store, readable by you alone, andsync --statussays whether one is set. A snapshot that is not encrypted, or will not open with this passphrase, stops the sync before anything is merged. syncpulls every snapshot the remote holds and merges each into this store exactly asimport --mergedoes (D185, D189, D197), then pushes the result back. When another machine pushed in between, it starts over from the pull, three attempts at most; what it merged stays merged either way.- Nothing is pushed when the remote already holds exactly this store. The last version synced and when are kept in the same
<store>.sync.json, written only once the remote holds this store. - Through a daemon the merge goes through the daemon, the store's one writer, and the sync state sits beside the daemon's store. The snapshot travels to it as a file path, not inline, so a store of any size syncs.
tasqx sync setup dir --set path=/mnt/share/tasqxa shared folder as the remote
tasqx sync setup r2a Cloudflare R2 bucket; asks for each setting
tasqx synctasqx sync --statusNo captured screen: it talks to a remote this machine has set up, which the demo store has none of. Every other block of output on this page is a recording of this build running against the demo store (D149).
api
Maps to (any).
One JSON envelope in on stdin → one out on stdout.
tasqx api # one JSON envelope on stdin → one on stdoutTakes no arguments of its own; the global flags still apply.
- The stdio one-shot transport; the envelope key is
"tasqx":"1". - --socket is refused here rather than honoured (DESIGN.md D73):
apiruns against an in-process engine, never a daemon. --theme is likewise never read — there is nothing here to render.
tasqx api <<< '{"tasqx":"1","id":"1","method":"task.list","params":{}}'call any method
echo '{"tasqx":"1","id":"e1","method":"task.list","params":{"filter":"+docs","sort":["-urgency"]}}' | tasqx api{"id":"e1","ok":true,"result":{"count":2,"next_offset":null,"store_empty":false,"tasks":[{"_rev":1,"active_since":null,"blocked":true,"budget_tokens":null,"claim_expires":null,"claimed_by":null,"completed":null,"created":"2026-08-16T10:00:00Z","due":"2026-09-21T00:00:00Z","estimate":"PT5H","id":"46773aad-c4aa-435a-abe1-fcde8ce09658","modified":"2026-08-16T10:00:00Z","priority":"M","project":"api","recurrence":null,"remind":null,"scheduled":null,"short_id":51,"status":"pending","tags":["docs"],"title":"Write the migration guide for SDK 3.0","tracked":"PT0S","tracked_adjustment":"PT0S","urgency":12.2,"wait":null},{"_rev":1,"active_since":null,"blocked":false,"budget_tokens":null,"claim_expires":null,"claimed_by":null,"completed":null,"created":"2026-09-05T10:00:00Z","due":null,"estimate":"PT4H","id":"94362459-7e19-4ec0-a143-aa65f21c805c","modified":"2026-09-05T10:00:00Z","priority":"M","project":"api","recurrence":null,"remind":null,"scheduled":null,"short_id":55,"status":"pending","tags":["docs"],"title":"Add OpenAPI examples for every endpoint","tracked":"PT0S","tracked_adjustment":"PT0S","urgency":4.0,"wait":null}],"total":2},"tasqx":"1"}
Shell completion
completions
Maps to — (no store).
Turn on Tab completion for your shell.
tasqx completions [<shell>]<shell>stringoptional- Which shell (bash, elvish, fish, powershell, zsh). Detected from $SHELL when omitted; on Windows nothing sets $SHELL, so name it
- tasqx never edits a startup file: add the printed line yourself.
tasqx manual completionnames the file for each shell.
tasqx completions bashtasqx completions bash >> ~/.bashrcthe printed line is one line for exactly this
No captured screen: it prints one line of shell for your startup file — the same line --install writes into it for you. Every other block of output on this page is a recording of this build running against the demo store (D149).
Exit codes
Stable contract. Script against these.
| Code | Meaning |
|---|---|
0 | Success. |
1 | internal, or a failure beneath the request: the store would not open, a local write failed, or watch had no daemon to follow. |
2 | bad_request — a bad value, an unparseable date, contradictory flags. |
4 | not_found — no such task, project, or reference. |
5 | conflict — a lost --expected-rev race, or a lifecycle rule. |
6 | unsupported_version — the tasqx major you sent is not this build's. |
tasqx modify 1 --expected-rev 1 -p H ; echo "exit=$?"error [conflict]: expected_rev 1 but task is at rev 2
exit=5The JSON API
The load-bearing artifact. The CLI is a client; so is the MCP server; so could yours be. One envelope in, one envelope out.
The transport
tasqx api reads ONE request envelope on stdin and writes ONE response on stdout. No framing, no handshake, no daemon needed. For many calls on one connection, talk to the daemon socket instead — same envelopes, newline-delimited.
echo '{"tasqx":"1","method":"task.list"}' | tasqx apiecho '{"tasqx":"1","id":"e1","method":"task.list","params":{"filter":"+docs"}}' | tasqx api{"id":"e1","ok":true,"result":{ … },"tasqx":"1"}Request
{
"tasqx": "1", // API major. Required.
"id": "e1", // Optional. Echoed back if present.
"method": "task.list", // Required.
"params": { } // Optional; defaults to {}.
}Response
Success carries result; failure carries error. ok tells you which without inspecting further.
echo '{"tasqx":"1","id":"e1","method":"task.list","params":{"filter":"+docs","sort":["-urgency"]}}' | tasqx api{"id":"e1","ok":true,"result":{"count":2,"next_offset":null,"store_empty":false,"tasks":[{"_rev":1,"active_since":null,"blocked":true,"budget_tokens":null,"claim_expires":null,"claimed_by":null,"completed":null,"created":"2026-08-16T10:00:00Z","due":"2026-09-21T00:00:00Z","estimate":"PT5H","id":"46773aad-c4aa-435a-abe1-fcde8ce09658","modified":"2026-08-16T10:00:00Z","priority":"M","project":"api","recurrence":null,"remind":null,"scheduled":null,"short_id":51,"status":"pending","tags":["docs"],"title":"Write the migration guide for SDK 3.0","tracked":"PT0S","tracked_adjustment":"PT0S","urgency":12.2,"wait":null},{"_rev":1,"active_since":null,"blocked":false,"budget_tokens":null,"claim_expires":null,"claimed_by":null,"completed":null,"created":"2026-09-05T10:00:00Z","due":null,"estimate":"PT4H","id":"94362459-7e19-4ec0-a143-aa65f21c805c","modified":"2026-09-05T10:00:00Z","priority":"M","project":"api","recurrence":null,"remind":null,"scheduled":null,"short_id":55,"status":"pending","tags":["docs"],"title":"Add OpenAPI examples for every endpoint","tracked":"PT0S","tracked_adjustment":"PT0S","urgency":4.0,"wait":null}],"total":2},"tasqx":"1"}
Errors
An error is a value, not a crash. It carries a stable code, a human message, and machine-readable data — and the CLI's exit codes are these same codes.
| Code | Exit | Means |
|---|---|---|
bad_request | 2 | Malformed params, an unparseable date, contradictory input. |
not_found | 4 | No such task/project/reference. |
conflict | 5 | A lost expected_rev race, or a lifecycle rule. |
unsupported_version | 6 | The tasqx major you sent is not this build's. |
internal | 1 | A bug or an I/O failure. Should not happen. |
Version mismatches are caught before dispatch, and the error's data names the major that is supported.
echo '{"tasqx":"1","id":"e2","method":"task.get","params":{"ref":"999"}}' | tasqx api{"error":{"code":"not_found","data":{"short_id":999},"message":"no task with short_id 999"},"id":"e2","ok":false,"tasqx":"1"}echo '{"tasqx":"2","id":"v1","method":"task.list"}' | tasqx api{"error":{"code":"unsupported_version","data":{"supported":"1"},"message":"unsupported api major version: 2"},"id":"v1","ok":false,"tasqx":"1"}Coming in the CLI reference: the exit code every verb returns, verb by verb.
Feature detection
Do not guess what a build supports — ask it. core.capabilities is the handshake, and it also reports the current default project.
echo '{"tasqx":"1","id":"c1","method":"core.capabilities"}' | tasqx api{"id":"c1","ok":true,"result":{"api":"1","default_project":"work.tasqx","features":["dependencies","filter.boolean","reminders"],"methods":["project.create","project.list","project.use","project.archive","project.unarchive","task.add","task.list","task.get","task.brief","task.next","task.start","task.stop","task.claim","task.release","task.adjust_tracked","task.done","task.modify","task.cancel","task.reopen","task.triage","task.triage_list","tag.add","tag.remove","annotation.add","check.add","check.set","check.remove","annotation.remove","annotation.move","annotation.update","token.add","token.remove","dependency.add","dependency.remove","link.add","link.remove","link.list","graph.query","memory.add","memory.search","memory.get","memory.remove","memory.import","memory.refresh","memory.list","memory.update","note.add","note.list","note.get","note.update","note.remove","note.promote","tokens.recompute","report.summary","report.outcomes","store.export","store.import","git.event","event.list","event.revert","reminder.fire","core.capabilities","otlp.status"],"params":{"annotation.add":["ref","body","expected_rev"],"annotation.move":["ref","annotation_id","to"],"annotation.remove":["ref","annotation_id"],"annotation.update":["ref","annotation_id","body","expected_rev"],"check.add":["ref","body"],"check.remove":["ref","check_id","position"],"check.set":["ref","check_id","position","state","evidence","expected_rev"],"core.capabilities":[],"dependency.add":["ref","depends_on","expected_rev"],"dependency.remove":["ref","depends_on","expected_rev"],"event.list":["limit","ref","entity","from"],"event.revert":["ref","expected_rev"],"git.event":["event","branch","pr","sha","task"],"graph.query":["root","select","depth","node_types","relation_types","project","status","tags","modified_after","modified_before","include_inferred","max_nodes","max_edges"],"link.add":["from","to","relation","metadata","expected_rev"],"link.list":["ref","relation","limit","offset"],"link.remove":["id"],"memory.add":["title","body","source","project","standing"],"memory.get":["id","workdir"],"memory.import":["docs","project"],"memory.list":["limit","offset","project","standing"],"memory.refresh":["dry_run"],"memory.remove":["id"],"memory.search":["query","limit","scope","raw","project","include_unscoped","source_prefix","mode","min_similarity","workdir"],"memory.update":["id","title","body","source","project","standing","expected_rev"],"note.add":["body","project"],"note.get":["id","max_body_bytes"],"note.list":["status","links","query","project","limit","offset","max_body_bytes"],"note.promote":["id","to","title","expected_rev"],"note.remove":["id"],"note.update":["id","body","project","status","expected_rev"],"otlp.status":[],"project.archive":["name"],"project.create":["name","description"],"project.list":["include_archived"],"project.unarchive":["name"],"project.use":["name"],"reminder.fire":["ref","at"],"report.outcomes":["group_by","filter","metrics","since","until"],"report.summary":["group_by","filter","metrics","all","since","until"],"store.export":["filter","include_unscoped","out_path"],"store.import":["tasks","projects","default_project","docs","events","links","notes","dry_run","merge","path"],"tag.add":["ref","tags"],"tag.remove":["ref","tags"],"task.add":["title","project","priority","due","scheduled","wait","estimate","tags","recurrence","remind","checks","depends_on","budget_tokens","status","discovered_from","client"],"task.adjust_tracked":["ref","delta","reason"],"task.brief":["ref","memory_limit","annotations_limit","annotations_offset","max_body_bytes"],"task.cancel":["ref","expected_rev","actor","force_claim"],"task.claim":["ref","lease","actor","expected_rev"],"task.done":["ref","session_id","transcript_path","client","tool","model","checks_passed","evidence","force","input_tokens","output_tokens","cache_read_tokens","cache_creation_tokens","total_tokens","expected_rev","actor","force_claim"],"task.get":["ref","annotations_limit","annotations_offset","max_body_bytes","explain"],"task.list":["filter","sort","limit","offset","fields","actor"],"task.modify":["ref","set","expected_rev","actor","force_claim"],"task.next":["project","filter","resume","actor"],"task.release":["ref","actor","force_claim","expected_rev"],"task.reopen":["ref","expected_rev"],"task.start":["ref","keep","session_id","transcript_path","client","actor","holder","expected_rev"],"task.stop":["ref","expected_rev","actor","force_claim"],"task.triage":["ref","action","reason","of","expected_rev","actor","force_claim"],"task.triage_list":["limit","offset"],"token.add":["ref","tool","source","model","input_tokens","output_tokens","cache_read_tokens","cache_creation_tokens","total_tokens","confidence","idempotency_key"],"token.remove":["measurement_id"],"tokens.recompute":["dry_run"]},"store":"~/.local/share/tasqx/tasks.db"},"tasqx":"1"}Reading tasks without the API
Any CLI command with --json prints the raw API result — the same bytes the envelope's result would carry. That is usually the shortest path from a shell script to structured data:
tasqx show 53 --json{
"_rev": 1,
"active_since": null,
"annotations": [],
"annotations_next_offset": null,
"annotations_offset": 0,
"annotations_removed": [],
"annotations_total": 0,
"blocked": false,
"blocks": [],
"budget_tokens": null,
"checks": [],
"claim_expires": null,
"claimed_by": null,
"completed": null,
"created": "2026-08-15T10:00:00Z",
"delivered_annotation": null,
"delivered_annotation_id": null,
"depends_on": [],
"due": null,
"estimate": "PT3H",
"first_annotation": null,
"fresh_tokens": 0,
"git_refs": [],
"id": "ea7f7301-c9b4-43b5-afc3-eec055c2d7f4",
"modified": "2026-08-15T10:00:00Z",
"over": null,
"priority": "M",
"project": "infra",
"recurrence": null,
"remind": null,
"scheduled": null,
"short_id": 53,
"spawned_from": null,
"status": "pending",
"tags": [
"ops"
],
"title": "Move nightly backups to object storage",
"tokens": [],
"tracked": "PT0S",
"tracked_adjustment": "PT0S",
"unmet_blockers": [],
"urgency": 4.2,
"wait": null
}
Every method
All 63, grouped by the object they act on. Every section below carries the method's parameters, the shape of what it answers with, and a real request and response — and every one of those columns is generated from the engine rather than described beside it.
Project methods
project.create
The project, plus default (did it claim the default?) and current_default.
Objects: Project.
Parameters
namestringrequired- The project's name. Dotted names (`work.tasqx`) are free-form: nothing nests, and a filter on `work` does not match `work.tasqx`.
descriptionstringoptional- What the project is for. Shown by `project.list` and nowhere load-bearing.
Response
resultidstringalways- The new project's uuid.
namestringalways- The project's name, as stored.
defaultbooleanalways- Whether this create claimed the default project — true only when the store had none.
current_defaultstringnullable- The default project after the call, whichever it is. Null only in a store with no default at all.
echo '{"tasqx":"1","id":"pc1","method":"project.create","params":{"name":"research","description":"Prototypes and spikes"}}' | tasqx api{
"id": "pc1",
"ok": true,
"result": {
"current_default": "website",
"default": false,
"id": "019f7c0a-3d51-7c42-9a08-1f0c4e5b62d7",
"name": "research"
},
"tasqx": "1"
}Illustrative. a create mints a fresh v7 id, whose low bits are random — the pinned clock fixes the timestamp half and nothing fixes the rest, so a captured answer would differ on every run.
tasqx_create_project — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"description":"Prototypes and spikes","name":"research"},"name":"tasqx_create_project"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx initinit — Create a project — just a name, no folder.
project.list
{projects}; each row carries default.
Objects: Project.
Parameters
include_archivedbooleanoptionaldefaultfalse- Include archived projects in the list. Omitted, an archive is out of sight.
Response
resultcountintegeralways- How many projects came back.
store_emptybooleanalways- Whether the store has ever held a task at all — how "nothing yet" is told apart from "nothing matched".
projectsarrayalways- One row per project, sorted by name.
result.projects[]idstringalways- The project's uuid.
namestringalways- The project's name — the value
project:filters on andtask.addtakes. descriptionstringnullable- The project's description, or null.
archivedbooleanalways- Whether the project is archived. Archived projects are omitted unless
include_archivedasked for them. defaultbooleanalways- Whether this is the project a bare
task.addlands in.
echo '{"tasqx":"1","id":"p1","method":"project.list"}' | tasqx api{
"id": "p1",
"ok": true,
"result": {
"count": 5,
"projects": [
{
"archived": false,
"default": false,
"description": "Public REST API and its SDKs",
"id": "309d6b79-965e-4a32-9ae4-45508201e2bd",
"name": "api"
},
{
"archived": false,
"default": false,
"description": null,
"id": "73f778aa-f6fa-4db8-a56a-bd72fb710734",
"name": "home"
},
{
"archived": false,
"default": false,
"description": "CI, deploys and on-call",
"id": "e3eff9c0-cf44-4d3f-89e7-d15f17362f25",
"name": "infra"
},
{
"archived": false,
"default": false,
"description": "iOS and Android apps",
"id": "2fa91425-cb00-4853-9d2c-67eda13ffe79",
"name": "mobile"
},
{
"archived": false,
"default": true,
"description": "Marketing site and docs portal",
"id": "db5b5fab-8f4d-4e27-9da1-494c73cf256d",
"name": "website"
}
],
"store_empty": false
},
"tasqx": "1"
}tasqx_list_projects — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{},"name":"tasqx_list_projects"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
project.use
{name, default, previous}. Sets the default project.
Objects: Project.
Parameters
namestringrequired- The project that becomes the default — where a `task.add` with no `project` lands.
Response
resultnamestringalways- The project that is now the default.
defaultbooleanalways- Always true on success — the call's whole effect, echoed.
previousstringnullable- The project that was the default before, or null if there was none.
echo '{"tasqx":"1","id":"pu1","method":"project.use","params":{"name":"api"}}' | tasqx api{
"id": "pu1",
"ok": true,
"result": {
"default": true,
"name": "api",
"previous": "website"
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
ruled out by D22: an agent has project on task.add and should name it, rather than silently re-aiming the human's default for every later call, including the human's own.
tasqx useuse — Set the default project for bare adds.
project.archive
The archived project, plus default_cleared.
Objects: Project.
Parameters
namestringrequired- The project to archive. Archiving hides it from the list; it removes nothing.
Response
resultnamestringalways- The project that was archived.
archivedbooleanalways- Always true on success.
default_clearedbooleanalways- Whether archiving it also cleared the store's default project (D22).
open_tasksintegeralways- How much open work the archive left behind, counted in the same transaction (D89).
open_overdueintegeralways- How many of those open tasks are past their due date.
echo '{"tasqx":"1","id":"pa1","method":"project.archive","params":{"name":"mobile"}}' | tasqx api{
"id": "pa1",
"ok": true,
"result": {
"archived": true,
"default_cleared": false,
"name": "mobile",
"open_overdue": 0,
"open_tasks": 3
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
retiring a project is a decision about the human's workspace, not about the work. An agent asked to tidy the project list is being asked to make that decision on their behalf, and the CLI is where it belongs.
tasqx archivearchive — Retire a project — out of rotation, tasks untouched.
project.unarchive
{name, archived, open_tasks}. Puts an archived project back into rotation.
Objects: Project.
Parameters
namestringrequired- The archived project to put back into rotation. A project that is not archived is a `conflict`.
Response
resultnamestringalways- The project that was put back into rotation.
archivedbooleanalways- Always false on success.
open_tasksintegeralways- How much open work it holds, so the line can say what just came back into
next(D215).
echo '{"tasqx":"1","id":"pun1","method":"project.unarchive","params":{"name":"mobile"}}' | tasqx api{
"id": "pun1",
"ok": true,
"result": {
"archived": false,
"name": "mobile",
"open_tasks": 0
},
"tasqx": "1"
}Illustrative. the example store has no archived project to put back, so the response is typed; project_unarchive in the core tests pins its shape.
No MCP tool. An agent reaches this method only through a human running tasqx api.
the other half of project.archive, and for the same reason: whether a project is in rotation is a decision about the human's workspace (D215), and the CLI is where it belongs.
tasqx unarchiveunarchive — Put an archived project back into rotation.
Task methods
task.add
The new task, incl. the project it landed in (the default, if none given). checks (strings) and depends_on (refs) are created in the same transaction as the task (D213): an unknown prerequisite or a blank check refuses the whole add and nothing is created, and the answer then carries checks (with the ids check.set takes), depends_on and blocked. budget_tokens (D139) is a size gauge over FRESH tokens — input, output and cache creation, never cache reads — which stops nothing and is read back as fresh_tokens and over on task.get. The title is stored verbatim: a title carrying the CLI's inline sugar (+tag, due:, !prio…) adds a warnings entry naming it. status: "triage" files a proposal outside @working until task.triage accepts it; discovered_from links the task it was found on with derived_from (D238).
Objects: Task.
Parameters
titlestringrequired- Stored verbatim: CLI sugar (
+tag,due:,!prio) is not parsed; use the fields below. projectstringoptional- The project to file it under. Omitted, the task lands in the store's default project — and the response says which, because that is the only place a caller learns it.
prioritystringoptional- Priority: H (high), M (medium), or L (low).
duestringoptional- The deadline; drives urgency and anchors relative reminders, and never hides the task. Date grammar: "tomorrow", "friday", "2026-07-20", "in 3 days", "eom", "2026-07-20T17:00" (bare clock = UTC; an offset is honoured).
scheduledstringoptional- When you intend to start; a future value holds the task in
backloguntil it arrives. Date grammar: "tomorrow", "friday", "2026-07-20", "in 3 days", "eom", "2026-07-20T17:00" (bare clock = UTC; an offset is honoured). waitstringoptional- Hides the task until then, in
backloglikescheduledbut meaning "not my problem yet". Date grammar: "tomorrow", "friday", "2026-07-20", "in 3 days", "eom", "2026-07-20T17:00" (bare clock = UTC; an offset is honoured). estimatestringoptional- Duration: "4h", "90m", "1h30m", "2d", "1w", or ISO-8601 "PT4H".
tagsarray of stringoptional- Tags to put on the task. Lowercased and deduplicated as they are stored.
recurrencestringoptional- Recurrence rule (D2 subset): "daily", "every 3 days", "weekly on mon,wed", "monthly on day 15", "monthly on the last friday".
remindstringoptional- Reminder: a signed offset from
due("-1h", "-30m", "-2d", "+15m") or an absolute date in theduegrammar. checksarray of stringoptional- Acceptance criteria, created open with the task (D213).
depends_onarrayoptional- Short_ids or UUIDs this task waits on (D213). An unknown one refuses the whole add.
budget_tokensintegeroptional- Token budget (D139): a size gauge over FRESH tokens — input, output, cache creation, never cache reads. It STOPS NOTHING; a blown budget signals a task too big to hand over whole.
tasqx_get_taskreportsoveragainst it. statusstringoptional- triage: a proposal, outside @working (D238).
discovered_frominteger or stringoptional- The task this one was found while working on (short_id or UUID). The new task gets a `derived_from` link to it, and `tasqx triage` names it (D238).
clientstringoptional- The tool proposing the task, kept on its `add` event for `tasqx triage` (D238). The MCP server fills it from the handshake when omitted.
Response
resultidstringalways- The new task's uuid — stable, and what an export and an import agree on.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. statusstringalways- The status the task landed in:
pending, orbacklogwhen a futurewaitparks it. projectstringnullable- The project the task actually landed in — for an inherited default this is the only place the caller learns it.
urgencynumberalways- The urgency score the fixed formula gives it (D1).
recurrencestringnullable- The recurrence rule as stored, or null.
titlestringalways- The title as STORED — the CLI's inline sugar can rewrite what you typed, so this is what to verify against.
duestringnullable- The due date resolved to an instant:
due: "friday"comes back as its ISO timestamp. tagsarrayalways- The tags as stored, lowercased and deduplicated.
scheduledstringnullable- The scheduled date resolved to an instant, the same way
dueis. warningsarrayoptional- Present only when the title carries CLI inline sugar (
+tag,due:,!prio…), which this door stores verbatim and never parses: one line naming those words. checksarrayoptional- Present only when the call named
checks(D213): the criteria created with the task, each{id, body, state, position}— the idscheck.settakes. depends_onarrayoptional- Present only when the call named
depends_on(D213): the short ids the task now waits on, deduplicated. blockedbooleanoptional- Present with
depends_on: whether any of those prerequisites is still open. similararrayalways- Always present: up to three open tasks of the same project that look like this one, best first, each
{id, short_id, title, similarity}; empty when none does. A warning only — the task was added either way (D237). similar_skippedbooleanalways- Always present: true when the comparison could not run, so
similaris empty for that reason rather than because nothing looks alike; the task was added either way (D237). discovered_fromintegeroptional- Present only when the call named
discovered_from(D238): the short id of the task this one was found on, now linked to it withderived_from.
result.checks[]idstringalways- The criterion's id — what
check.settakes ascheck_id. bodystringalways- The criterion as written.
statestringalways- Always
openhere: a new criterion is a claim nobody has marked. positionintegeralways- Its place in the task's list, from 0, in the order given.
result.similar[]idstringalways- The open task's uuid.
short_idintegeralways- The open task's short id.
titlestringalways- The open task's title.
similaritynumberalways- How alike the two read, as the cosine of their embeddings rounded to three decimals; at or above the threshold D237 tuned.
echo '{"tasqx":"1","id":"a1","method":"task.add","params":{"title":"Draft the Q4 roadmap","project":"website","priority":"H","due":"friday","tags":["planning"]}}' | tasqx api{
"id": "a1",
"ok": true,
"result": {
"due": "2026-09-18T00:00:00Z",
"id": "019f7c0a-3d51-7c42-9a08-1f0c4e5b62d7",
"project": "website",
"recurrence": null,
"scheduled": null,
"short_id": 62,
"similar": [],
"similar_skipped": false,
"status": "pending",
"tags": [
"planning"
],
"title": "Draft the Q4 roadmap",
"urgency": 16.6
},
"tasqx": "1"
}Illustrative. a new task's v7 id is half clock and half entropy, so no pin can reproduce it. The CLI's own add echo, which prints the short id, is captured instead.
tasqx_add_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"due":"friday","priority":"H","project":"website","tags":["planning"],"title":"Draft the Q4 roadmap"},"name":"tasqx_add_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx addadd — Capture a task — title plus inline sugar.
task.list
{count, total, next_offset, tasks}. An omitted filter matches everything; count is how many rows came back and total how many matched, so a windowed list is never mistaken for a complete one. next_offset is null once nothing is left. An omitted limit defaults to 100; a named limit is clamped to 10,000. fields may include depends_on, which no other projection emits. With an actor AND @working in the filter the list chooses for that caller and leaves out tasks under another holder's live claim; any other list is a view. Rows carry claimed_by and claim_expires (D236).
Objects: Task · Dependency.
Parameters
filterstringoptional- Filter DSL, e.g. "status:pending +api". "@working" is the actionable set. Bare words match the title ("memory explorer" as a phrase); "status:any" is every status. Omit it (or send "") for every task.
sortarray of stringoptional- Sort keys, e.g. ["-urgency", "due"]; "-" descends. Valid: urgency, short_id, priority, due, created, modified, title, tokens, estimate, tracked. An unknown key is rejected.
limitintegeroptional- Rows to return. Omit for this tool's page (100), shrunk to fit the response byte budget; a named limit is honoured up to 10000.
offsetintegeroptional- Matching rows to skip; pass the previous
next_offset. Ordering is stable across pages. fieldsarray of stringoptional- Restrict each row to these fields. Omit for short_id, title, status, priority, urgency, blocked, due, project, tags, with null-valued keys dropped; send [] for the whole row.
actorstringoptional- Who chooses: under @working, others' claims are hidden (D236).
Response
resultcountintegeralways- How many rows came back.
totalintegeralways- How many tasks matched the filter, before
limittruncated. next_offsetintegernullable- The
offsetthat fetches the next page; null once nothing is left. store_emptybooleanalways- Whether the store has ever held a task at all — how "nothing yet" is told apart from "nothing matched".
tasksarrayalways- The matching tasks, one row each.
result.tasks[]idstringalways- The task's uuid, stable across exports and machines.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalwayspending,backlog,active,doneorcancelled— or, on a stored row whose status text this build does not recognize, that text verbatim, withstatus_unrecognized: truebeside it.prioritystringnullableH,M,L, or null for none.projectstringnullable- The project the task belongs to, or null.
duestringnullable- When it is due, as an instant, or null.
scheduledstringnullable- When work is planned to start, or null. While it is in the future a
pendingtask reads asbacklog, out of the working set, as withwait;active,doneandcancelledkeep their status. waitstringnullable- Holds the task out of the working set until this instant, or null. While it is in the future a
pendingtask reads asbacklog;active,doneandcancelledkeep their status. estimatestringnullable- The estimate as an ISO 8601 duration (
PT4H), or null. recurrencestringnullable- The recurrence rule (
every 3 days), or null. remindstringnullable- The reminder spec — an offset from
dueor an absolute instant — or null. urgencynumberalways- The urgency score (D1): priority, due proximity and age, summed.
tagsarrayalways- Every tag on the task, as plain strings.
createdstringalways- When the task was captured.
modifiedstringalways- When it last changed.
completedstringnullable- When
task.donecompleted it, or null.task.reopenclears it, andtask.canceldoes not set it. budget_tokensintegernullable- The size gauge over FRESH tokens (D139), or null for no threshold. It stops nothing.
_revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional. trackedstringalways- Time on the clock as an ISO duration, the interval still running included up to the moment of the read (D166).
active_sincemarks where that one began. tracked_adjustmentstringalways- The net of every
task.adjust_trackedcorrection already insidetracked, as a signed ISO duration (-PT2H25M);PT0Swhen there is none. active_sincestringnullable- When the open interval started, or null when the timer is not running.
blockedbooleanalways- Whether an unmet dependency is holding this task back.
claimed_bystringnullable- Who holds a live claim on this task (D236), or null.
claim_expiresstringnullable- When that claim's lease runs out (UTC); null with
claimed_by. status_unrecognizedbooleanoptional- Present, and true, only on a row whose
statusno writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one.store.importnow refuses such a status.
echo '{"tasqx":"1","id":"e1","method":"task.list","params":{"filter":"+docs","sort":["-urgency"]}}' | tasqx api{
"id": "e1",
"ok": true,
"result": {
"count": 2,
"next_offset": null,
"store_empty": false,
"tasks": [
{
"_rev": 1,
"active_since": null,
"blocked": true,
"budget_tokens": null,
"claim_expires": null,
"claimed_by": null,
"completed": null,
"created": "2026-08-16T10:00:00Z",
"due": "2026-09-21T00:00:00Z",
"estimate": "PT5H",
"id": "46773aad-c4aa-435a-abe1-fcde8ce09658",
"modified": "2026-08-16T10:00:00Z",
"priority": "M",
"project": "api",
"recurrence": null,
"remind": null,
"scheduled": null,
"short_id": 51,
"status": "pending",
"tags": [
"docs"
],
"title": "Write the migration guide for SDK 3.0",
"tracked": "PT0S",
"tracked_adjustment": "PT0S",
"urgency": 12.2,
"wait": null
},
{
"_rev": 1,
"active_since": null,
"blocked": false,
"budget_tokens": null,
"claim_expires": null,
"claimed_by": null,
"completed": null,
"created": "2026-09-05T10:00:00Z",
"due": null,
"estimate": "PT4H",
"id": "94362459-7e19-4ec0-a143-aa65f21c805c",
"modified": "2026-09-05T10:00:00Z",
"priority": "M",
"project": "api",
"recurrence": null,
"remind": null,
"scheduled": null,
"short_id": 55,
"status": "pending",
"tags": [
"docs"
],
"title": "Add OpenAPI examples for every endpoint",
"tracked": "PT0S",
"tracked_adjustment": "PT0S",
"urgency": 4.0,
"wait": null
}
],
"total": 2
},
"tasqx": "1"
}tasqx_list_tasks — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"filter":"+docs","sort":["-urgency"]},"name":"tasqx_list_tasks"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx listlist — List tasks matching a filter.
tasqx agendaagenda — What is coming up, when — list ordered by time.
tasqx dashboarddashboard — Open the overview screen, or ask for its panels as data.
tasqx pickpick — Browse tasks on a full-screen list: read one, search them, start one.
tasqx watchwatch — Live-updating task view.
tasqx boardboard — Live kanban in your browser: drag a card to change the task.
task.get
Full detail incl. annotations, deps, blocked. A limit takes the newest annotations; annotations_total and annotations_next_offset say what was left out. max_body_bytes caps each annotation body IN THE RESPONSE (D148): a longer one is cut on a character boundary and the row gains body_bytes (its real size) and body_truncated, so a single enormous note cannot blow a caller's payload limit; omit it and every body comes back whole, which is what the CLI and tasqx api do. annotations_removed lists the tombstones annotation.remove left — {id, removed} per scrubbed note, never its text — while annotations and annotations_total keep excluding them. explain: true adds urgency_breakdown (priority, due_proximity, age, total) — the terms urgency sums (D1); the plumbing tasqx why --json uses (#150).
Objects: Task · Annotation · Check · Dependency · Git ref · Token measurement.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
annotations_limitintegeroptional- Most-recent annotations to return; omit for this tool's page (20). An over-budget page is cut to what fits and the view names the
annotations_offsetfor the rest;include_json: truespends that budget first. 0 returns none. annotations_offsetintegeroptional- Annotations to skip, counted back from the newest; pass the previous
annotations_next_offset. max_body_bytesintegeroptional- Cap each annotation body at this many BYTES in the response; default 16384. A longer body is cut on a character boundary and marked with its real size and the call that reads it whole; the store is never capped (D148).
explainbooleanoptional- Add
urgency_breakdown(priority,due_proximity,age,total) — the termsurgencysums (D1). Default false.
Response
resultdelivered_annotation_idstringnullable- The note
task.donepinned as the delivery note — the newest one at the instant of completion — or null.task.reopenclears it (D165). first_annotationobjectnullable- The oldest note, whatever page
annotations_limit/annotations_offsetasked for — the card's Description — or null when there is none (D165). delivered_annotationobjectnullable- The pinned delivery note — on a closed task with no pin, the newest note — or null on an open task, or when the pinned note was removed (D165).
fresh_tokensintegeralways- Fresh tokens spent on this task — input, output and cache creation, never cache reads (D139), plus any unsplit
total_tokensin full (D167). overbooleannullable- Whether
fresh_tokensis pastbudget_tokens; null when no budget was set, because there is no verdict to give. checksarrayalways- The acceptance criteria on the task, in position order (D138).
git_refsarrayalways- The branch, PR and merge commit
git.eventrecorded on this task (D239), oldest first. idstringalways- The task's uuid, stable across exports and machines.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalwayspending,backlog,active,doneorcancelled— or, on a stored row whose status text this build does not recognize, that text verbatim, withstatus_unrecognized: truebeside it.prioritystringnullableH,M,L, or null for none.projectstringnullable- The project the task belongs to, or null.
duestringnullable- When it is due, as an instant, or null.
scheduledstringnullable- When work is planned to start, or null. While it is in the future a
pendingtask reads asbacklog, out of the working set, as withwait;active,doneandcancelledkeep their status. waitstringnullable- Holds the task out of the working set until this instant, or null. While it is in the future a
pendingtask reads asbacklog;active,doneandcancelledkeep their status. estimatestringnullable- The estimate as an ISO 8601 duration (
PT4H), or null. recurrencestringnullable- The recurrence rule (
every 3 days), or null. remindstringnullable- The reminder spec — an offset from
dueor an absolute instant — or null. urgencynumberalways- The urgency score (D1): priority, due proximity and age, summed.
tagsarrayalways- Every tag on the task, as plain strings.
createdstringalways- When the task was captured.
modifiedstringalways- When it last changed.
completedstringnullable- When
task.donecompleted it, or null.task.reopenclears it, andtask.canceldoes not set it. budget_tokensintegernullable- The size gauge over FRESH tokens (D139), or null for no threshold. It stops nothing.
_revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional. trackedstringalways- Time on the clock as an ISO duration, the interval still running included up to the moment of the read (D166).
active_sincemarks where that one began. tracked_adjustmentstringalways- The net of every
task.adjust_trackedcorrection already insidetracked, as a signed ISO duration (-PT2H25M);PT0Swhen there is none. active_sincestringnullable- When the open interval started, or null when the timer is not running.
depends_onarrayalways- The short ids this task waits on.
annotationsarrayalways- The notes on this task, oldest first within the page. Pages are counted back from the newest note, so
annotations_offset: 0is the most recent page. blocksarrayalways- The short ids this task is holding back — the reverse edge of
depends_on. spawned_fromintegernullable- The short id of the task whose completion spawned this one (a recurrence, D170); null on any other task.
claimed_bystringnullable- Who holds a live claim on this task (D236): the holder's actor key,
userfor a shell. Null when nobody does, or the lease has expired. claim_expiresstringnullable- When that claim's lease runs out (UTC); null with
claimed_by. Liveness is read against the clock, so an expired lease is simply null here. annotations_totalintegeralways- How many notes the task has, whether or not this page carries them all.
annotations_offsetintegeralways- How many notes this page skipped.
annotations_next_offsetintegernullable- The offset of the next page of notes; null on the last one.
annotations_removedarrayalways- One tombstone per scrubbed note (D113) — id and instant, never the text. Excluded from
annotationsand its total. tokensarrayalways- Every token measurement banked against this task.
blockedbooleanalways- Whether an unmet dependency is holding this task back.
status_unrecognizedbooleanoptional- Present, and true, only on a row whose
statusno writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one.store.importnow refuses such a status. urgency_breakdownobjectoptional- The terms
urgencysums — present only whenexplain: trueasked for them. unmet_blockersarrayalways- The open prerequisites, named — what
blocked: trueis blocked ON.
result.checks[]idstringalways- The criterion's uuid — what
check.setandcheck.removetake. bodystringalways- The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalwaysopen,passedorfailed. A failure is a normal outcome, not an error.evidencestringnullable- The citation for the state, or null — some criteria are met by something nobody can quote.
positionintegeralways- Where it sits in the list, from zero.
createdstringalways- When the criterion was added.
modifiedstringalways- When its state or evidence last changed.
result.git_refs[]idstringalways- The ref row's id.
kindstringalwaysbranch,prorcommit.refstringalways- The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalwaysopen,closedormerged.mergedis final; a PR can move betweenopenandclosed.createdstringalways- When the ref was first recorded.
modifiedstringalways- When
statelast changed. Betweenopenandclosed, the later change wins when two stores merge. notedstringnullable- On a branch row, what
git.eventhas already done for that branch (JSON keyed per PR: the PRs that started the task, the fact its last note stated, the delivery note, the note it completed the task with), so a retry writes a missing note, never a duplicate, and never overrides the user. Null before any.
result.annotations[]idstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
result.first_annotationidstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
result.delivered_annotationidstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
result.annotations_removed[]idstringalways- The scrubbed note's id.
removedstringalways- When it was scrubbed. Never the text — that is the point of the method.
result.tokens[]idstringalways- The measurement's uuid — what
token.removetakes. toolstringalways- Which tool spent the tokens (
claude-code,codex, …). sourcestringalways- How the figure was obtained:
self-report,log-parseorotel. modelstringnullable- The model that did the work, or null.
input_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache — never blended with the others (D48).
cache_creation_tokensintegeralways- Tokens spent writing the cache.
confidencestringalways- How checkable the figure is:
high,mediumorlow. A self-report may not claimhigh. createdstringalways- When the measurement was banked.
total_tokensintegeralways- One unsplit count, from a reporter that could not split it — 0 on a split measurement, and never folded into the four (D167).
result.urgency_breakdownprioritynumberalways- The priority term.
due_proximitynumberalways- The due-date term.
agenumberalways- The age term.
totalnumberalways- The sum — the same number as the row's
urgency, so nobody has to recompute the rounding.
result.unmet_blockers[]short_idintegeralways- The blocking task's short id.
titlestringalways- The blocking task's title, so a refusal can be read without a second call.
echo '{"tasqx":"1","id":"g1","method":"task.get","params":{"ref":"51"}}' | tasqx api{
"id": "g1",
"ok": true,
"result": {
"_rev": 1,
"active_since": null,
"annotations": [
{
"body": "Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week late is a support ticket that already happened.",
"created": "2026-09-11T11:20:00Z",
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e"
},
{
"body": "Blocked until the rate limit lands: the guide has to state the real ceiling, and quoting a number that then changes is worse than waiting.",
"created": "2026-09-15T11:20:00Z",
"id": "ee4a9a3b-10de-465a-aab1-84eeb0e48236"
},
{
"body": "Scope: the 2.x → 3.0 rename table, the auth change, and a worked example per SDK. Not the reference — that generates itself.",
"created": "2026-09-04T11:20:00Z",
"id": "fd496ca3-cd12-4457-8b43-5ef0668cab3c"
}
],
"annotations_next_offset": null,
"annotations_offset": 0,
"annotations_removed": [],
"annotations_total": 3,
"blocked": true,
"blocks": [],
"budget_tokens": null,
"checks": [
{
"body": "Every renamed symbol has a row in the table",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "ebb86ee2-69ed-4938-b57c-c12a89e9414e",
"modified": "2026-09-14T09:10:00Z",
"position": 0,
"state": "passed"
},
{
"body": "One worked example per SDK (js, py, go)",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "6776fd34-ec65-4b9e-8ce6-a106f4f51c13",
"modified": "2026-09-14T09:10:00Z",
"position": 1,
"state": "passed"
},
{
"body": "The rate-limit ceiling is stated with its real number",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "7e37a508-7921-4cb2-bf0c-0a2944eb31e4",
"modified": "2026-09-14T09:10:00Z",
"position": 2,
"state": "open"
},
{
"body": "Reviewed by whoever ships 3.0",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "e6a1096b-6f05-4e95-96f5-52452080f2ac",
"modified": "2026-09-14T09:10:00Z",
"position": 3,
"state": "open"
}
],
"claim_expires": null,
"claimed_by": null,
"completed": null,
"created": "2026-08-16T10:00:00Z",
"delivered_annotation": null,
"delivered_annotation_id": null,
"depends_on": [
50
],
"due": "2026-09-21T00:00:00Z",
"estimate": "PT5H",
"first_annotation": {
"body": "Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week late is a support ticket that already happened.",
"created": "2026-09-11T11:20:00Z",
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e"
},
"fresh_tokens": 0,
"git_refs": [
{
"created": "2026-09-13T14:05:00Z",
"id": "019f5a10-0000-7000-8000-00000000a001",
"kind": "branch",
"modified": "2026-09-13T14:05:00Z",
"noted": null,
"ref": "task/51-sdk-migration-guide",
"state": "open"
},
{
"created": "2026-09-13T14:05:00Z",
"id": "019f5a10-0000-7000-8000-00000000a002",
"kind": "pr",
"modified": "2026-09-13T14:05:00Z",
"noted": null,
"ref": "https://github.com/acme/api/pull/412",
"state": "open"
}
],
"id": "46773aad-c4aa-435a-abe1-fcde8ce09658",
"modified": "2026-08-16T10:00:00Z",
"over": null,
"priority": "M",
"project": "api",
"recurrence": null,
"remind": null,
"scheduled": null,
"short_id": 51,
"spawned_from": null,
"status": "pending",
"tags": [
"docs"
],
"title": "Write the migration guide for SDK 3.0",
"tokens": [],
"tracked": "PT0S",
"tracked_adjustment": "PT0S",
"unmet_blockers": [
{
"short_id": 50,
"title": "Rate-limit the /search endpoint"
}
],
"urgency": 12.2,
"wait": null
},
"tasqx": "1"
}tasqx_get_task — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"51"},"name":"tasqx_get_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
task.brief
{task, neighbourhood, memory}. Everything needed before starting one task (D136): task is task.get's own result verbatim; neighbourhood.depends_on names each prerequisite with its NEWEST annotation — what that task concluded — and neighbourhood.blocks names what this one releases, title and status only; on a recurrence spawn, last_time names the previous occurrence and the first paragraph of its newest note (D170); memory is a memory.search result under an expression tasqx DERIVES from the task's title, tags and project, echoed in matched, scoped to that project PLUS unscoped docs (the same include_unscoped shape memory.search has) or, for a task with no project, store-wide — reported either way in project — and excluding the task's OWN annotations, which the task half already carries in full (#657, D168). The derived expression is a disjunction: a caller's query states what they want and is ANDed, a derived one is a bag of the task's own words and would answer nothing if it were. Half the page (rounded up) is reserved for knowledge docs and annotations fill the rest, either kind taking the other's unused slots, docs listed first (D147); reserved_docs, docs_total and annotations_total say what was done. memory_limit defaults to 5 (D154). annotations_limit, annotations_offset and max_body_bytes page and cap the TASK half exactly as on task.get (D148, D220): absent, it carries the whole history with every body whole, and annotations_total and annotations_next_offset say what a page left out.
Objects: Task · Annotation · Check · Dependency · Memory document · Git ref · Token measurement.
Parameters
refinteger or stringrequired- Task short_id or UUID.
memory_limitintegeroptional- Memory hits to return; default 5. Half the slots (rounded up) are reserved for knowledge docs, annotations fill the rest, and either kind takes the other's unused slots.
annotations_limitintegeroptional- The task's most-recent annotations, as on tasqx_get_task; default 20. Over budget, memory is cut first, then this page (D220).
annotations_offsetintegeroptional- Annotations to skip from the newest, as on tasqx_get_task.
max_body_bytesintegeroptional- Cap each annotation body at this many BYTES in the response; default 16384. A longer body is cut on a character boundary and marked with its real size and the call that reads it whole; the store is never capped (D148).
Response
resulttaskobjectalwaystask.get's own result for this task, withouturgency_breakdown— the brief does not takeexplain.neighbourhoodobjectalways- What this task waits on and what it releases.
memoryobjectalways- A
memory.searchresult under an expression derived from the task's own words, fused with a meaning query made of the same words (D196). last_timeobjectoptional- Present only on a recurrence spawn: the previous occurrence and what it delivered (D170).
result.taskdelivered_annotation_idstringnullable- The note
task.donepinned as the delivery note — the newest one at the instant of completion — or null.task.reopenclears it (D165). first_annotationobjectnullable- The oldest note, whatever page
annotations_limit/annotations_offsetasked for — the card's Description — or null when there is none (D165). delivered_annotationobjectnullable- The pinned delivery note — on a closed task with no pin, the newest note — or null on an open task, or when the pinned note was removed (D165).
fresh_tokensintegeralways- Fresh tokens spent on this task — input, output and cache creation, never cache reads (D139), plus any unsplit
total_tokensin full (D167). overbooleannullable- Whether
fresh_tokensis pastbudget_tokens; null when no budget was set, because there is no verdict to give. checksarrayalways- The acceptance criteria on the task, in position order (D138).
git_refsarrayalways- The branch, PR and merge commit
git.eventrecorded on this task (D239), oldest first. idstringalways- The task's uuid, stable across exports and machines.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalwayspending,backlog,active,doneorcancelled— or, on a stored row whose status text this build does not recognize, that text verbatim, withstatus_unrecognized: truebeside it.prioritystringnullableH,M,L, or null for none.projectstringnullable- The project the task belongs to, or null.
duestringnullable- When it is due, as an instant, or null.
scheduledstringnullable- When work is planned to start, or null. While it is in the future a
pendingtask reads asbacklog, out of the working set, as withwait;active,doneandcancelledkeep their status. waitstringnullable- Holds the task out of the working set until this instant, or null. While it is in the future a
pendingtask reads asbacklog;active,doneandcancelledkeep their status. estimatestringnullable- The estimate as an ISO 8601 duration (
PT4H), or null. recurrencestringnullable- The recurrence rule (
every 3 days), or null. remindstringnullable- The reminder spec — an offset from
dueor an absolute instant — or null. urgencynumberalways- The urgency score (D1): priority, due proximity and age, summed.
tagsarrayalways- Every tag on the task, as plain strings.
createdstringalways- When the task was captured.
modifiedstringalways- When it last changed.
completedstringnullable- When
task.donecompleted it, or null.task.reopenclears it, andtask.canceldoes not set it. budget_tokensintegernullable- The size gauge over FRESH tokens (D139), or null for no threshold. It stops nothing.
_revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional. trackedstringalways- Time on the clock as an ISO duration, the interval still running included up to the moment of the read (D166).
active_sincemarks where that one began. tracked_adjustmentstringalways- The net of every
task.adjust_trackedcorrection already insidetracked, as a signed ISO duration (-PT2H25M);PT0Swhen there is none. active_sincestringnullable- When the open interval started, or null when the timer is not running.
depends_onarrayalways- The short ids this task waits on.
annotationsarrayalways- The notes on this task, oldest first within the page. Pages are counted back from the newest note, so
annotations_offset: 0is the most recent page. blocksarrayalways- The short ids this task is holding back — the reverse edge of
depends_on. spawned_fromintegernullable- The short id of the task whose completion spawned this one (a recurrence, D170); null on any other task.
claimed_bystringnullable- Who holds a live claim on this task (D236): the holder's actor key,
userfor a shell. Null when nobody does, or the lease has expired. claim_expiresstringnullable- When that claim's lease runs out (UTC); null with
claimed_by. Liveness is read against the clock, so an expired lease is simply null here. annotations_totalintegeralways- How many notes the task has, whether or not this page carries them all.
annotations_offsetintegeralways- How many notes this page skipped.
annotations_next_offsetintegernullable- The offset of the next page of notes; null on the last one.
annotations_removedarrayalways- One tombstone per scrubbed note (D113) — id and instant, never the text. Excluded from
annotationsand its total. tokensarrayalways- Every token measurement banked against this task.
blockedbooleanalways- Whether an unmet dependency is holding this task back.
status_unrecognizedbooleanoptional- Present, and true, only on a row whose
statusno writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one.store.importnow refuses such a status. unmet_blockersarrayalways- The open prerequisites, named — what
blocked: trueis blocked ON.
result.task.checks[]idstringalways- The criterion's uuid — what
check.setandcheck.removetake. bodystringalways- The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalwaysopen,passedorfailed. A failure is a normal outcome, not an error.evidencestringnullable- The citation for the state, or null — some criteria are met by something nobody can quote.
positionintegeralways- Where it sits in the list, from zero.
createdstringalways- When the criterion was added.
modifiedstringalways- When its state or evidence last changed.
result.task.git_refs[]idstringalways- The ref row's id.
kindstringalwaysbranch,prorcommit.refstringalways- The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalwaysopen,closedormerged.mergedis final; a PR can move betweenopenandclosed.createdstringalways- When the ref was first recorded.
modifiedstringalways- When
statelast changed. Betweenopenandclosed, the later change wins when two stores merge. notedstringnullable- On a branch row, what
git.eventhas already done for that branch (JSON keyed per PR: the PRs that started the task, the fact its last note stated, the delivery note, the note it completed the task with), so a retry writes a missing note, never a duplicate, and never overrides the user. Null before any.
result.task.annotations[]idstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
result.task.first_annotationidstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
result.task.delivered_annotationidstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
result.task.annotations_removed[]idstringalways- The scrubbed note's id.
removedstringalways- When it was scrubbed. Never the text — that is the point of the method.
result.task.tokens[]idstringalways- The measurement's uuid — what
token.removetakes. toolstringalways- Which tool spent the tokens (
claude-code,codex, …). sourcestringalways- How the figure was obtained:
self-report,log-parseorotel. modelstringnullable- The model that did the work, or null.
input_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache — never blended with the others (D48).
cache_creation_tokensintegeralways- Tokens spent writing the cache.
confidencestringalways- How checkable the figure is:
high,mediumorlow. A self-report may not claimhigh. createdstringalways- When the measurement was banked.
total_tokensintegeralways- One unsplit count, from a reporter that could not split it — 0 on a split measurement, and never folded into the four (D167).
result.task.unmet_blockers[]short_idintegeralways- The blocking task's short id.
titlestringalways- The blocking task's title, so a refusal can be read without a second call.
result.neighbourhooddepends_onarrayalways- Each prerequisite, with its newest annotation — what that task concluded.
blocksarrayalways- What this task releases when it completes, title and status only.
result.neighbourhood.depends_on[]short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalways- The prerequisite's status.
annotationobjectnullable- The prerequisite's newest note, or null when nobody wrote one.
result.neighbourhood.blocks[]short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalways- The dependent's status.
result.memorycountintegeralways- How many hits came back.
totalintegeralways- How many entries matched, by words or by meaning, before the page was cut.
has_morebooleanalways- Whether anything was left behind.
hitsarrayalways- The hits themselves — the same rows
memory.searchreturns, each with itsvia. matchedstringnullable- The FTS expression tasqx derived from the task; null when the task had no searchable word.
projectstringnullable- The project the search was scoped to, or null.
reserved_docsintegeralways- How many of the page's slots were held for knowledge docs (D147).
docs_totalintegeralways- How many docs matched.
annotations_totalintegeralways- How many annotations matched.
semanticobjectnullable- What the meaning side ran:
{model, min_similarity, query},querybeing the text the brief derived from the task's title, tags and project; null when it compared nothing (D196).
result.last_timeshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
completedstringnullable- When the previous occurrence was completed.
deliveredstringnullable- The first paragraph of its newest note — the delivery note — or null when nobody wrote one.
result.memory.hits[]idstringalways- The doc's id, or the annotation's.
kindstringalwaysdocorannotation— which store the hit came from.titlestringalways- The doc's title; for an annotation, the task it is written on.
sourcestringnullable- Where the doc came from (a file path,
task:#51), or null. snippetstringalways- The matching passage, with the hit in context — for a hit found by meaning alone, the start of its best chunk. Never the whole body.
ranknumberalways- The negated fused
score(D196). Lower is a better match and the hits arrive in ascendingrank; the number is not comparable between searches and is not a threshold (D154). standingbooleannullable- For a doc hit, whether it is a standing ruling (D156); null on an annotation hit, which has no such flag.
projectstringnullable- Which project this hit is scoped to — a doc's own column, or the annotation's task's — or null for global knowledge (#657).
stalebooleannullable- true when the doc's origin file no longer matches what was imported; null for annotations and docs with no origin (D180).
stale_refsarraynullable- For a doc hit, the paths and
path::symbolrefs it cites that are gone from the git toplevel of the caller'sworkdir; empty when none are, or none can be checked. Null on an annotation hit and when noworkdirwas given (D235). modifiedstringalways- When the entry last changed: a doc's
modified, an annotation'screated(annotations carry no modified time, so an edit does not move it, the same rulegraph.queryuses for a node) — what a date window filters hits on (D219). viastringalways- Which side found it:
lexical(words — all of them, whichmatchedexplains, or only some, whichpartialsays andmatched_anyexplains),semantic(meaning;similarityexplains it) orboth(D196). similaritynumbernullable- Its best chunk's cosine to the query, to three decimals — also on a hit the words found, when the entry has a vector; null when it has none or the semantic side did not run.
scorenumberalways- The fused score: Σ weight/(20 + rank) over the lists it is on — every word 1.0, any word 0.35, meaning 1.3 (D196, #838). Higher is better.
partialbooleanalways- true when the only words that found it are D193's any-word list: it lacks some of the query's words.
relaxedis true when any hit on the page is.
result.memory.semanticmodelstringalways- The embedding model's id.
min_similaritynumberalways- The floor a match by meaning had to reach.
querystringalways- The text embedded: the task's title, tags and project leaf.
echo '{"tasqx":"1","id":"b1","method":"task.brief","params":{"ref":"51","memory_limit":3}}' | tasqx api{
"id": "b1",
"ok": true,
"result": {
"memory": {
"annotations_total": 0,
"count": 1,
"docs_total": 1,
"has_more": false,
"hits": [
{
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"kind": "doc",
"modified": "2026-09-18T09:30:12.481Z",
"partial": false,
"project": "api",
"rank": -0.047619047619047616,
"score": 0.047619047619047616,
"similarity": 0.269,
"snippet": "description How an SDK release is cut, tagged and announced\n\nCut the…",
"source": "docs/release.md",
"stale": null,
"stale_refs": null,
"standing": false,
"title": "release-process",
"via": "lexical"
}
],
"matched": "\"write\" OR \"migration\" OR \"guide\" OR \"sdk\" OR \"3.0\" OR \"docs\" OR \"api\"",
"project": "api",
"reserved_docs": 2,
"semantic": {
"min_similarity": 0.3,
"model": "potion-base-8M@bf8b056651a2c21b8d2565580b8569da283cab23/int8/tok2/chunk4",
"query": "Write the migration guide for SDK 3.0 docs api"
},
"total": 1
},
"neighbourhood": {
"blocks": [],
"depends_on": [
{
"annotation": null,
"short_id": 50,
"status": "pending",
"title": "Rate-limit the /search endpoint"
}
]
},
"task": {
"…": "task.get's own result, verbatim"
}
},
"tasqx": "1"
}Illustrative. every hit carries a rank. Since D196 it is the negated fused reciprocal rank, not bm25, but the words list is still in bm25 order, computed through the platform's log(): two near-equal hits can trade places between macOS and Linux and move every number after them, and the drift job goes red for everyone but whoever captured last. The tasqx brief SCREEN is captured instead; it prints snippets and never the number.
tasqx_brief_task — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"memory_limit":3,"ref":"51"},"name":"tasqx_brief_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx briefbrief — Everything needed before starting a task, in one read.
task.next
{task, summary, store_empty, total} (D213). task is the row tasqx next picks — @working, your scope ANDed on, highest urgency first — or null; summary is the first paragraph of its oldest annotation. With resume: true the answer also carries active (each active task in scope and the actor holding its clock, D140) and recent (the five open tasks annotated most recently, each with the first 200 characters of its newest note and when it was written). A task under another holder's live claim is skipped; actor names whose claims are yours, user when omitted (D236).
Objects: Task · Dependency.
Parameters
projectstringoptional- Limit the pick to this project.
filterstringoptional- Filter DSL ANDed onto
@working, e.g.+mcp. resumebooleanoptional- Also answer "where did I leave off": active tasks and recently annotated open tasks in the same scope. Default false.
actorstringoptional- Who chooses: others' claims are skipped (D236).
Response
resulttaskobjectnullable- The most urgent unblocked task — the first row
task.listgives for@working, scoped, sorted-urgency— or null when nothing is workable. summarystringnullable- The first paragraph of that task's OLDEST annotation, whitespace collapsed and cut at 400 characters; null when the task has no notes or there is no task.
store_emptybooleanalways- Whether the store has ever held a task, which tells "nothing yet" from "nothing matched".
totalintegeralways- How many unblocked tasks matched the scope, of which
taskis the most urgent. activearrayoptional- Present with
resume: true: every open-and-active task in scope,{short_id, title, status, note, active_since, held_by}.held_byis the actor on the task's latest start (D140), or null. recentarrayoptional- Present with
resume: true: up to five open, non-active tasks in scope whose newest annotation is the most recent, newest first,{short_id, title, status, note}.noteis{body, created}with the body cut at 200 characters.
result.taskidstringalways- The task's uuid, stable across exports and machines.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalwayspending,backlog,active,doneorcancelled— or, on a stored row whose status text this build does not recognize, that text verbatim, withstatus_unrecognized: truebeside it.prioritystringnullableH,M,L, or null for none.projectstringnullable- The project the task belongs to, or null.
duestringnullable- When it is due, as an instant, or null.
scheduledstringnullable- When work is planned to start, or null. While it is in the future a
pendingtask reads asbacklog, out of the working set, as withwait;active,doneandcancelledkeep their status. waitstringnullable- Holds the task out of the working set until this instant, or null. While it is in the future a
pendingtask reads asbacklog;active,doneandcancelledkeep their status. estimatestringnullable- The estimate as an ISO 8601 duration (
PT4H), or null. recurrencestringnullable- The recurrence rule (
every 3 days), or null. remindstringnullable- The reminder spec — an offset from
dueor an absolute instant — or null. urgencynumberalways- The urgency score (D1): priority, due proximity and age, summed.
tagsarrayalways- Every tag on the task, as plain strings.
createdstringalways- When the task was captured.
modifiedstringalways- When it last changed.
completedstringnullable- When
task.donecompleted it, or null.task.reopenclears it, andtask.canceldoes not set it. budget_tokensintegernullable- The size gauge over FRESH tokens (D139), or null for no threshold. It stops nothing.
_revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional. trackedstringalways- Time on the clock as an ISO duration, the interval still running included up to the moment of the read (D166).
active_sincemarks where that one began. tracked_adjustmentstringalways- The net of every
task.adjust_trackedcorrection already insidetracked, as a signed ISO duration (-PT2H25M);PT0Swhen there is none. active_sincestringnullable- When the open interval started, or null when the timer is not running.
blockedbooleanalways- Whether an unmet dependency is holding this task back.
claimed_bystringnullable- Who holds a live claim on this task (D236), or null.
claim_expiresstringnullable- When that claim's lease runs out (UTC); null with
claimed_by. status_unrecognizedbooleanoptional- Present, and true, only on a row whose
statusno writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one.store.importnow refuses such a status.
result.active[]short_idintegeralways- The task's short id.
titlestringalways- The task's title.
statusstringalways- The task's stored status text.
noteobjectnullable- The task's newest annotation, or null when it has none.
active_sincestringnullable- When the running interval began.
held_bystringnullable- The actor on the task's latest start (D140); null when that start named none.
result.active[].notebodystringalways- The note, whitespace collapsed and cut at 200 characters with
…. createdstringalways- When the note was written.
result.recent[]short_idintegeralways- The task's short id.
titlestringalways- The task's title.
statusstringalways- The task's stored status text.
noteobjectnullable- The task's newest annotation, or null when it has none.
result.recent[].notebodystringalways- The note, whitespace collapsed and cut at 200 characters with
…. createdstringalways- When the note was written.
echo '{"tasqx":"1","id":"n1","method":"task.next","params":{"project":"api","resume":true}}' | tasqx api{
"id": "n1",
"ok": true,
"result": {
"active": [],
"recent": [
{
"note": {
"body": "Scope: the 2.x → 3.0 rename table, the auth change, and a worked example per SDK. Not the reference — that generates itself.",
"created": "2026-09-04T11:20:00Z"
},
"short_id": 51,
"status": "pending",
"title": "Write the migration guide for SDK 3.0"
}
],
"store_empty": false,
"summary": null,
"task": {
"_rev": 1,
"active_since": null,
"blocked": false,
"budget_tokens": null,
"claim_expires": null,
"claimed_by": null,
"completed": null,
"created": "2026-08-31T10:00:00Z",
"due": "2026-09-19T00:00:00Z",
"estimate": "PT3H",
"id": "813547e2-5937-41f0-8401-82fcdb14a009",
"modified": "2026-08-31T10:00:00Z",
"priority": "H",
"project": "api",
"recurrence": null,
"remind": null,
"scheduled": null,
"short_id": 50,
"status": "pending",
"tags": [
"perf"
],
"title": "Rate-limit the /search endpoint",
"tracked": "PT0S",
"tracked_adjustment": "PT0S",
"urgency": 15.9,
"wait": null
},
"total": 2
},
"tasqx": "1"
}tasqx_next — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"project":"api","resume":true},"name":"tasqx_next"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx nextnext — The one highest-urgency unblocked task.
task.start
The task, timer running. Correlation params land in the start event. auto_stopped lists whichever other task D6's single-active rule just stopped to make room for this one — empty unless keep was omitted and something else was running. actor (D140) says who is asking for the clock: a start against a timer a DIFFERENT actor holds is refused conflict rather than silently stopping it, since that would leave the other party's work untracked. Absent on both sides — a shell — keeps D6's behaviour exactly; the MCP server fills it in with its own connection id.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
keepbooleanoptional- Keep other active tasks running (opt out of single-active).
session_idstringoptional- Your agent-session id, recorded on this task's event for token attribution.
transcript_pathstringoptional- Absolute path to your session transcript, recorded on the same event.
clientstringoptional- Calling tool as "<name> <version>". Filled in from the MCP handshake when omitted.
actorstringoptional- Who is asking for the clock (D140); filled in with this connection's id when omitted. Starting while ANOTHER actor holds an active clock or a claim (D236) is a
conflict, not a silent stop of their timer; passkeep: trueto run both. Reconnecting? Pass a stable id. holderstringoptional- The claim holder this start acts as, when it is not `actor` (D236): the CLI's `start --holder`, which leaves the clock's D140 actor anonymous. Absent, the holder is `actor`, else `user`. Not on the MCP tool, where `actor` is both.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
Response
resultidstringalways- The task's uuid.
statusstringalwaysactiveonce the clock is running.interval_startedstringnullable- When the running interval opened. On the idempotent re-start of a task already running, that interval's existing start. Null only if an active row has lost its start, which no tasqx write produces.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
already_runningbooleanalways- True when the call opened no new interval because the timer was already on.
auto_stoppedarrayalways- Whatever D6's single-active rule stopped to make room. Empty unless
keepwas omitted and something else was running.
result.auto_stopped[]idstringalways- The stopped task's uuid.
short_idintegeralways- The stopped task's short id.
trackedstringalways- That task's running total after the stop.
echo '{"tasqx":"1","id":"s1","method":"task.start","params":{"ref":"50"}}' | tasqx api{
"id": "s1",
"ok": true,
"result": {
"already_running": false,
"auto_stopped": [
{
"id": "63e5a05b-e665-459b-be06-d750369a9ad7",
"short_id": 48,
"tracked": "PT0S"
}
],
"id": "813547e2-5937-41f0-8401-82fcdb14a009",
"interval_started": "2026-09-16T09:00:00Z",
"short_id": 50,
"status": "active",
"title": "Rate-limit the /search endpoint"
},
"tasqx": "1"
}tasqx_start_timer — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"50"},"name":"tasqx_start_timer"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
task.stop
{status, interval, tracked}. interval is the duration just closed; tracked is the running total, the same word task.get uses for it.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
actorstringoptional- Holder (D236).
force_claimbooleanoptional- Override another holder's claim.
Response
resultstatusstringalways- The status the task fell back to.
intervalstringalways- The interval just closed, as an ISO duration.
trackedstringalways- The running total after the stop — the same word
task.getuses. short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
echo '{"tasqx":"1","id":"st1","method":"task.stop","params":{"ref":"48"}}' | tasqx api{
"id": "st1",
"ok": true,
"result": {
"interval": "PT0S",
"short_id": 48,
"status": "pending",
"title": "Ship the v2 pricing page",
"tracked": "PT0S"
},
"tasqx": "1"
}tasqx_stop_timer — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"48"},"name":"tasqx_stop_timer"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx stopstop — Pause an active task.
task.claim
{short_id, title, op, claimed_by, claim_expires, _rev} (D236). op is claim, renew or takeover. Another holder's live lease is a conflict naming the holder and the expiry.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
leasestringoptional- Default 30m, max 24h.
actorstringoptional- The holder; default this connection. Reconnecting? Pass a stable id.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (D236).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
opstringalwaysclaim,renew(the holder claimed again) ortakeover(another holder's lease had expired) — the event it wrote.claimed_bystringalways- The holder: the call's
actor, elseuser. claim_expiresstringalways- When the lease runs out (UTC).
_revintegeralways- The task's revision counter after the claim.
echo '{"tasqx":"1","id":"cl1","method":"task.claim","params":{"ref":"47","lease":"1h","actor":"builder-2"}}' | tasqx api{
"id": "cl1",
"ok": true,
"result": {
"_rev": 2,
"claim_expires": "2026-09-16T10:00:00Z",
"claimed_by": "builder-2",
"op": "claim",
"short_id": 47,
"title": "Renew the TLS certificate for api.example.dev"
},
"tasqx": "1"
}tasqx_claim_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"actor":"builder-2","lease":"1h","ref":"47"},"name":"tasqx_claim_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx claimclaim — Claim a task so other agents skip it.
task.release
{short_id, title, released_by, forced, _rev} (D236). Only the holder releases, unless force_claim takes a claim another holder left behind.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
actorstringoptional- The holder; default this connection. Reconnecting? Pass a stable id.
force_claimbooleanoptional- Override another holder's claim.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (D236).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
released_bystringalways- The holder that gave the claim up — with
force, the caller that took it from another holder. forcedbooleanalways- True when
forcereleased a claim someone else held; thereleaseevent says so too. _revintegeralways- The task's revision counter after the release.
echo '{"tasqx":"1","id":"rl1","method":"task.release","params":{"ref":"47","actor":"builder-2"}}' | tasqx api{
"id": "rl1",
"ok": true,
"result": {
"_rev": 3,
"forced": false,
"released_by": "builder-2",
"short_id": 47,
"title": "Renew the TLS certificate for api.example.dev"
},
"tasqx": "1"
}Illustrative. the demo store holds no claim to give up, and the manifest runs each row against a fresh copy of it; task.release in the conformance suite pins its shape.
tasqx_release_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"actor":"builder-2","ref":"47"},"name":"tasqx_release_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx releaserelease — Give a claim up.
task.adjust_tracked
{short_id, title, delta, tracked, tracked_adjustment, _rev}. A signed correction (-2h25m, +30m) to tracked time, in any status; a total below zero or an empty reason is bad_request. Recorded as its own event, which event.revert takes back (D166).
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
deltastringrequired- Signed duration:
-2h25m,+30m. reasonstringrequired- Why the time is corrected.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
deltastringalways- The correction this call applied, as a signed ISO duration.
trackedstringalways- The total after it — the same word
task.getuses. tracked_adjustmentstringalways- The net of every correction on the task, this one included.
_revintegeralways- The task's revision after the correction.
echo '{"tasqx":"1","id":"at1","method":"task.adjust_tracked","params":{"ref":"42","delta":"-2h25m","reason":"idle gap"}}' | tasqx api{
"id": "at1",
"ok": true,
"result": {
"_rev": 2,
"delta": "-PT2H25M",
"short_id": 42,
"title": "Paginate the audit log",
"tracked": "PT6H25M",
"tracked_adjustment": "-PT2H25M"
},
"tasqx": "1"
}tasqx_adjust_tracked — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"delta":"-2h25m","reason":"idle gap","ref":"42"},"name":"tasqx_adjust_tracked"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx adjustadjust — Correct a task's tracked time.
task.done
The task; plus the spawned next instance if recurring. Correlation params land in the done event, and so do tool and model on their own (D65) — a caller that cannot count its tokens still records who did the work. Any present token count additionally records a self-report measurement — total_tokens alone is one unsplit count, never beside the four (D167) — the primary channel: only the caller knows which task a turn's spend served, and the log-parse fallback refuses samples claimed by more than one task's window. A task with open blockers is refused conflict naming them; force: true completes it anyway, and the response then carries forced: true and blocked_by (D150).
Objects: Task · Dependency.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
session_idstringoptional- Your agent-session id, recorded on this task's event for token attribution.
transcript_pathstringoptional- Absolute path to your session transcript, recorded on the same event.
clientstringoptional- Calling tool as "<name> <version>". Filled in from the MCP handshake when omitted.
toolstringoptional- The AI tool doing the work, free-form (e.g. "claude-code"). Also names a token measurement, defaulting to
client. modelstringoptional- The model doing the work, e.g. "claude-opus-5". Recorded with or without token counts.
checks_passedarray of stringoptional- Ids of the acceptance criteria this work proved (D138). Completing with criteria open or failed is not refused, but
checks_hintsays so andtasqx_outcomescounts it unproven. evidencestringoptional- One citation covering
checks_passed: a test name, an excerpt of output, a commit sha. forcebooleanoptional- Past open dependencies (D150).
input_tokensintegeroptional- Self-reported input tokens (0 or more).
output_tokensintegeroptional- Self-reported output tokens (0 or more).
cache_read_tokensintegeroptional- Self-reported cache-read tokens (0 or more).
cache_creation_tokensintegeroptional- Self-reported cache-creation tokens (0 or more).
total_tokensintegeroptional- One unsplit count, instead of the four (D167).
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
actorstringoptional- Who is completing, as a claim holder (D236): another holder's live claim is a `conflict` unless `force_claim`. Filled in with the MCP connection's id when omitted there; `user` elsewhere.
force_claimbooleanoptionaldefaultfalse- Complete past another holder's live claim; the `done` event records `claim_forced` with that holder (D236). `force` is only for open dependencies (D150).
Response
resultstatusstringalwaysdone— the status the task is now in.completedstringalways- The instant recorded as the completion.
unblockedarrayalways- The short ids this completion released.
spawnedobjectoptional- The next instance, present only when the completed task carried a recurrence rule (D2) and no task already holds that next slot, as after a reopen and a second completion (D191).
tokens_hintstringoptional- Present only when the completion self-reported no token counts — the nudge, not an error.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
trackedstringalways- Time on the clock when it closed.
estimatestringnullable- What it was estimated at, or null — beside
tracked, the calibration in one line. forcedbooleanoptional- Present and true only on a completion that overrode still-open blockers (D150).
blocked_byarrayoptional- The blockers that override left open. Present only beside
forced.
result.blocked_by[]short_idintegeralways- The blocking task's short id.
titlestringalways- The blocking task's title, so a refusal can be read without a second call.
echo '{"tasqx":"1","id":"d1","method":"task.done","params":{"ref":"47"}}' | tasqx api{
"id": "d1",
"ok": true,
"result": {
"completed": "2026-09-16T09:00:00Z",
"estimate": "PT1H",
"short_id": 47,
"status": "done",
"title": "Renew the TLS certificate for api.example.dev",
"tokens_hint": "no token counts were self-reported; log-parse attribution is a best-effort fallback — pass input_tokens/output_tokens/cache_read_tokens/cache_creation_tokens on completion for a reliable measurement",
"tracked": "PT0S",
"unblocked": []
},
"tasqx": "1"
}tasqx_complete_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"47"},"name":"tasqx_complete_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx donedone — Complete a task.
task.modify
{short_id, _rev, set}; set echoes the RESOLVED value actually stored for each field this call named (e.g. due:"friday" comes back as its ISO instant). null in the request's own set clears a field. A stale expected_rev is a conflict carrying the current rev; a set.title carrying inline sugar adds warnings, as on task.add.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
setobjectrequired- Field → new value, e.g. {"priority":"M","due":"2026-07-22T17:00:00+02:00"}.
expected_revintegeroptional- Optimistic-concurrency guard. Supplied by the server from the task's current
_revwhen omitted; pass it to pin a rev you read earlier. There is no last-writer-wins mode. actorstringoptional- Holder (D236).
force_claimbooleanoptional- Override another holder's claim.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. _revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional. setobjectalways- The RESOLVED value stored for each field this call named —
due: "friday"comes back as its instant. warningsarrayoptional- Present only when
set.titlecarries CLI inline sugar, stored verbatim: one line naming those words, as ontask.add.
echo '{"tasqx":"1","id":"m1","method":"task.modify","params":{"ref":"52","set":{"priority":"H","due":"friday"}}}' | tasqx api{
"id": "m1",
"ok": true,
"result": {
"_rev": 2,
"set": {
"due": "2026-09-18T00:00:00Z",
"priority": "H"
},
"short_id": 52
},
"tasqx": "1"
}tasqx_modify_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"52","set":{"due":"friday","priority":"H"}},"name":"tasqx_modify_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx modifymodify — Change a task — set fields or --clear them.
task.cancel
{short_id, status}. Like task.start, stop, done and reopen, it takes task.modify's expected_rev: a stale one is a conflict carrying the current rev, and nothing is written.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
actorstringoptional- Holder (D236).
force_claimbooleanoptional- Override another holder's claim.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. statusstringalwayscancelled.unblockedarrayalways- The short ids the cancellation released — a cancelled blocker stops blocking (D114).
echo '{"tasqx":"1","id":"c1","method":"task.cancel","params":{"ref":"60"}}' | tasqx api{
"id": "c1",
"ok": true,
"result": {
"short_id": 60,
"status": "cancelled",
"unblocked": []
},
"tasqx": "1"
}tasqx_cancel_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"60"},"name":"tasqx_cancel_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx cancelcancel — Cancel a task.
task.reopen
{short_id, status, blocked}. blocked names the open dependents this reopen put back — the mirror of unblocked on task.done.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. statusstringalways- The status the task went back to.
blockedarrayalways- The open dependents this reopen put back into
blocked— the mirror ofunblocked.
echo '{"tasqx":"1","id":"ro1","method":"task.reopen","params":{"ref":"12"}}' | tasqx api{
"id": "ro1",
"ok": true,
"result": {
"blocked": [],
"short_id": 12,
"status": "pending"
},
"tasqx": "1"
}tasqx_reopen_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"12"},"name":"tasqx_reopen_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx reopenreopen — Reopen a completed or cancelled task.
task.triage
{short_id, title, action, status, _rev, unblocked} (D238). Settles a task in triage: accept makes it pending (or backlog, while a future date holds it); decline cancels it, keeping reason as a note; duplicate cancels it, moves its notes and dependents to of and links the two with duplicates, adding of and moved_annotations. A task not in triage is a conflict; a decline or duplicate of a task another holder claims is one too, unless force_claim (D236). event.revert takes back an accept or a decline.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
actionstringrequired- `accept` (to pending), `decline` (cancelled, `reason` kept as a note) or `duplicate` (cancelled, notes moved to `of`, linked with `duplicates`). D238.
reasonstringoptional- Why it was declined.
ofinteger or stringoptional- The task it duplicates (action
duplicate). expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written.
actorstringoptional- Holder (D236).
force_claimbooleanoptional- Override another holder's claim.
Response
resultshort_idintegeralways- The proposal's short id.
titlestringalways- Its title.
actionstringalwaysaccept,declineorduplicate, as asked.statusstringalways- Its status now:
pending(orbacklogwhile a futurewaitorscheduledholds it) afteraccept,cancelledotherwise (D238). _revintegeralways- The proposal's revision after the call.
unblockedarrayalways- Short ids of open tasks the decline or duplicate released, as
task.cancelreports them (D11). Empty afteraccept. ofobjectoptional- Present on
duplicate:{short_id, title}of the task it repeats, now linked to it withduplicates. moved_annotationsintegeroptional- Present on
duplicate: how many notes moved toof(D211).
echo '{"tasqx":"1","id":"tr1","method":"task.triage","params":{"ref":"62","action":"duplicate","of":"60"}}' | tasqx api{
"id": "tr1",
"ok": true,
"result": {
"_rev": 2,
"action": "duplicate",
"moved_annotations": 0,
"of": {
"short_id": 60,
"title": "Investigate flaky checkout test"
},
"short_id": 62,
"status": "cancelled",
"title": "Retry the flaky checkout e2e test",
"unblocked": []
},
"tasqx": "1"
}tasqx_triage_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"action":"duplicate","of":"60","ref":"62"},"name":"tasqx_triage_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
task.triage_list
{tasks, total, next_offset} (D238). The triage inbox in one read, oldest first: each proposal with the client its add named and the task it was discovered_from. next_offset is null on the last page.
Objects: Task.
Parameters
limitintegeroptionaldefault100- Proposals per page, at most 10,000 — `task.list`'s bounds.
offsetintegeroptionaldefault0- Proposals to skip; pass the previous answer's `next_offset`.
Response
resulttasksarrayalways- The page of proposals, oldest first, each
{short_id, title, created, client, discovered_from}. totalintegeralways- How many tasks are in triage, the page aside.
next_offsetintegernullable- The
offsetthat reads the next page; null on the last.
result.tasks[]short_idintegeralways- The proposal's short id.
titlestringalways- Its title.
createdstringalways- When it was filed.
clientstringnullable- The tool its
addevent names, or null when none did. discovered_fromobjectnullable{short_id, title}of the task itsderived_fromlink points at, or null.
echo '{"tasqx":"1","id":"tl1","method":"task.triage_list","params":{}}' | tasqx api{
"id": "tl1",
"ok": true,
"result": {
"next_offset": null,
"tasks": [
{
"client": "claude-code 2.1",
"created": "2026-09-14T15:40:00Z",
"discovered_from": {
"short_id": 48,
"title": "Ship the v2 pricing page"
},
"short_id": 62,
"title": "Retry the flaky checkout e2e test"
},
{
"client": "claude-code 2.1",
"created": "2026-09-15T15:40:00Z",
"discovered_from": {
"short_id": 48,
"title": "Ship the v2 pricing page"
},
"short_id": 63,
"title": "Cache the pricing API response"
}
],
"total": 2
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
tasqx_list_tasks with status:triage already lists the inbox for an agent; the filing tool and origin task this adds are for the person settling it (D238).
tasqx triagetriage — List the proposals waiting in triage, oldest first.
Tag methods
tag.add
The task's tags.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
tagsarray of stringrequired- The tags to add. Already-present tags are a no-op rather than an error.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. tagsarrayalways- Every tag on the task after the call.
echo '{"tasqx":"1","id":"t1","method":"tag.add","params":{"ref":"53","tags":["q4"]}}' | tasqx api{
"id": "t1",
"ok": true,
"result": {
"short_id": 53,
"tags": [
"ops",
"q4"
]
},
"tasqx": "1"
}tasqx_tag_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"53","tags":["q4"]},"name":"tasqx_tag_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx tagtag — Attach tags to a task.
tag.remove
The task's remaining tags, plus removed. A tag the task does not have is not_found and removes nothing — all or nothing, so a typo can never answer ok.
Objects: Task.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
tagsarray of stringrequired- The tags to take off. All or nothing: a tag the task does not have is `not_found` and removes none of them, so a typo cannot answer ok.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. tagsarrayalways- The tags left on the task.
removedarrayalways- The tags this call took off. A tag the task does not have is
not_foundand removes nothing.
echo '{"tasqx":"1","id":"t2","method":"tag.remove","params":{"ref":"53","tags":["ops"]}}' | tasqx api{
"id": "t2",
"ok": true,
"result": {
"removed": [
"ops"
],
"short_id": 53,
"tags": []
},
"tasqx": "1"
}tasqx_untag_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"53","tags":["ops"]},"name":"tasqx_untag_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx untaguntag — Remove tags from a task.
Annotation methods
annotation.add
The annotation. Takes task.modify's expected_rev: a stale one is a conflict and nothing is written.
Objects: Annotation.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
bodystringrequired- Note text, stored verbatim. Multi-line markdown is fine.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. annotationobjectalways- The note as stored, echoed back so a caller can see its bytes survived.
result.annotationidstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
echo '{"tasqx":"1","id":"an1","method":"annotation.add","params":{"ref":"51","body":"Ruling: the guide ships WITH the 3.0 release."}}' | tasqx api{
"id": "an1",
"ok": true,
"result": {
"annotation": {
"body": "Ruling: the guide ships WITH the 3.0 release.",
"created": "2026-09-16T09:00:00Z",
"id": "019f7c0a-3d51-7401-a1e6-c040f1ddbe51"
},
"short_id": 51
},
"tasqx": "1"
}Illustrative. the note's id is minted fresh, v7, half of it random. The tasqx annotate screen is captured instead.
tasqx_annotate_task — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"Ruling: the guide ships WITH the 3.0 release.","ref":"51"},"name":"tasqx_annotate_task"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx annotateannotate — Attach a timestamped note to a task, correct one, or move it.
annotation.remove
{short_id, removed}. annotation_id is the full id, a unique prefix of 8+ characters (ambiguous is conflict, listing the candidates) or the note's 1-based position on the task, as tasqx show numbers it (D211). Scrubs the annotation's body in the store (D113) — a hard delete, not a hide — and removed names only the id and the instant, never the text. An unknown or already-removed id is not_found; event.revert does not cover this op.
Objects: Annotation.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
annotation_idstringrequired- Full id, unique prefix of 8+ characters, or the 1-based position
tasqx showprints (D211).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. removedobjectalways- The tombstone: the id and the instant, never the text (D113).
result.removedidstringalways- The scrubbed note's id.
removedstringalways- When it was scrubbed. Never the text — that is the point of the method.
echo '{"tasqx":"1","id":"ar1","method":"annotation.remove","params":{"ref":"51","annotation_id":"17dd6621-7db4-43b5-9f36-ddf89018081e"}}' | tasqx api{
"id": "ar1",
"ok": true,
"result": {
"removed": {
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e",
"removed": "2026-09-16T09:00:00Z"
},
"short_id": 51
},
"tasqx": "1"
}tasqx_remove_annotation — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"annotation_id":"17dd6621-7db4-43b5-9f36-ddf89018081e","ref":"51"},"name":"tasqx_remove_annotation"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx unannotateunannotate — Permanently scrub one annotation's text.
annotation.move
{short_id, annotation, to}. Puts one note on another task (D211): its id, body and created are kept, only the task changes, and search follows. to is the destination task ref; moving to the task the note is already on is conflict, a missing task or note not_found. event.revert puts it back.
Objects: Annotation.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
annotation_idstringrequired- Full id, unique prefix of 8+ characters, or 1-based position on
ref. tointeger or stringrequired- The task to move it to: short_id (integer) or full UUID.
Response
resultshort_idintegeralways- The short id of the task the note was on — the
refyou sent. annotationobjectalways- The note as it now stands: the same id, body and
created(D211). toobjectalways- The task it is on now:
{short_id, title}.
result.annotationidstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
result.toshort_idintegeralways- The destination task's short id.
titlestringalways- The destination task's title.
echo '{"tasqx":"1","id":"am1","method":"annotation.move","params":{"ref":"51","annotation_id":"1","to":"52"}}' | tasqx api{
"id": "am1",
"ok": true,
"result": {
"annotation": {
"body": "Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week late is a support ticket that already happened.",
"created": "2026-09-11T11:20:00Z",
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e"
},
"short_id": 51,
"to": {
"short_id": 52,
"title": "Dark mode for the settings screen"
}
},
"tasqx": "1"
}tasqx_move_annotation — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"annotation_id":"1","ref":"51","to":"52"},"name":"tasqx_move_annotation"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx annotateannotate — Attach a timestamped note to a task, correct one, or move it.
annotation.update
{short_id, annotation, _rev}. Replaces one note's body IN PLACE (D165): its id, created and position are kept, so a corrected first note stays the card's Description, and search re-indexes the new text. expected_rev is the TASK's _rev, as on task.modify. A removed note is not_found; event.revert puts the previous body back.
Objects: Annotation.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
annotation_idstringrequired- Full id, unique prefix of 8+ characters, or the 1-based position
tasqx showprints (D211). bodystringrequired- The corrected text; replaces the old body whole.
expected_revintegeroptional- Guard on the task's
_rev; supplied by the server when omitted.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. annotationobjectalways- The note as it now stands: the same id and
created, the new body (D165). _revintegeralways- The task's revision counter after the edit. Send it back as
expected_revto make the next change conditional.
result.annotationidstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
echo '{"tasqx":"1","id":"au1","method":"annotation.update","params":{"ref":"51","annotation_id":"019f7c0a-3d51-7401-a1e6-c040f1ddbe51","body":"Ruling: the guide ships AFTER the 3.0 release."}}' | tasqx api{
"id": "au1",
"ok": true,
"result": {
"_rev": 7,
"annotation": {
"body": "Ruling: the guide ships AFTER the 3.0 release.",
"created": "2026-09-16T09:00:00Z",
"id": "019f7c0a-3d51-7401-a1e6-c040f1ddbe51"
},
"short_id": 51
},
"tasqx": "1"
}Illustrative. the note it edits has an id minted fresh, v7, half of it random, so no fixed request can name it.
tasqx_update_annotation — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"annotation_id":"019f7c0a-3d51-7401-a1e6-c040f1ddbe51","body":"Ruling: the guide ships AFTER the 3.0 release.","ref":"51"},"name":"tasqx_update_annotation"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx annotateannotate — Attach a timestamped note to a task, correct one, or move it.
Check methods
check.add
{short_id, check{id, body, state, evidence, position, created, modified}}. One acceptance criterion (D138), appended at the end and starting open. tasqx NEVER RUNS a check: the body is a claim and evidence is a citation, both stored verbatim and neither interpreted. What runs commands is a hook the operator installed, calling check.set like any other client.
Objects: Check.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
bodystringrequired- The criterion, in your own words. Stored verbatim.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. checkobjectalways- The criterion as stored, appended at the end and starting
open.
result.checkidstringalways- The criterion's uuid — what
check.setandcheck.removetake. bodystringalways- The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalwaysopen,passedorfailed. A failure is a normal outcome, not an error.evidencestringnullable- The citation for the state, or null — some criteria are met by something nobody can quote.
positionintegeralways- Where it sits in the list, from zero.
createdstringalways- When the criterion was added.
modifiedstringalways- When its state or evidence last changed.
echo '{"tasqx":"1","id":"ca1","method":"check.add","params":{"ref":"51","body":"Every renamed symbol has a row in the table"}}' | tasqx api{
"id": "ca1",
"ok": true,
"result": {
"check": {
"body": "Every renamed symbol has a row in the table",
"created": "2026-09-16T09:00:00Z",
"evidence": null,
"id": "019f7c0a-3d51-7a90-b0c1-52f0a7d3c611",
"modified": "2026-09-16T09:00:00Z",
"position": 4,
"state": "open"
},
"short_id": 51
},
"tasqx": "1"
}Illustrative. the criterion's id is minted fresh, v7, half of it random.
tasqx_add_check — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"Every renamed symbol has a row in the table","ref":"51"},"name":"tasqx_add_check"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx checkcheck — Acceptance criteria on a task: add, mark, drop.
check.set
{short_id, check_id, state}. state is open|passed|failed; failed is a normal outcome, not an error. evidence is optional — some criteria are met by something nobody can quote, and inventing a citation is worse than an unproven pass. Name the check by exactly one of check_id or position (1-based, in the order task.get lists them); an unknown one is not_found listing the task's checks. A stale expected_rev is a conflict.
Objects: Check.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
check_idstringrequired- The check's id, from
tasqx_get_taskor the add. positionintegerrequired- Or its 1-based place in tasqx_get_task's list.
statestringrequired- open | passed | failed.
evidencestringoptional- Your proof: a test name, an excerpt of output, a commit sha. Optional — an invented citation is worse than an unproven
passed. expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. check_idstringalways- The criterion that moved.
statestringalways- Its new state.
echo '{"tasqx":"1","id":"cs1","method":"check.set","params":{"ref":"51","check_id":"7e37a508-7921-4cb2-bf0c-0a2944eb31e4","state":"passed","evidence":"docs/guides/sdk-3.0.md#rate-limits"}}' | tasqx api{
"id": "cs1",
"ok": true,
"result": {
"check_id": "7e37a508-7921-4cb2-bf0c-0a2944eb31e4",
"short_id": 51,
"state": "passed"
},
"tasqx": "1"
}tasqx_set_check — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"check_id":"7e37a508-7921-4cb2-bf0c-0a2944eb31e4","evidence":"docs/guides/sdk-3.0.md#rate-limits","ref":"51","state":"passed"},"name":"tasqx_set_check"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx checkcheck — Acceptance criteria on a task: add, mark, drop.
check.remove
{short_id, check_id, removed}. For a criterion that was the wrong thing to ask; use check.set failed when the criterion was right and the work did not meet it. The check is named as on check.set.
Objects: Check.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
check_idstringrequired- The check's id.
positionintegerrequired- Or its 1-based place in tasqx_get_task's list.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. check_idstringalways- The criterion that was dropped.
removedbooleanalways- Always true on success; an unknown id is
not_foundinstead.
echo '{"tasqx":"1","id":"cr1","method":"check.remove","params":{"ref":"51","check_id":"e6a1096b-6f05-4e95-96f5-52452080f2ac"}}' | tasqx api{
"id": "cr1",
"ok": true,
"result": {
"check_id": "e6a1096b-6f05-4e95-96f5-52452080f2ac",
"removed": true,
"short_id": 51
},
"tasqx": "1"
}tasqx_remove_check — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"check_id":"e6a1096b-6f05-4e95-96f5-52452080f2ac","ref":"51"},"name":"tasqx_remove_check"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx checkcheck — Acceptance criteria on a task: add, mark, drop.
Dependency methods
dependency.add
Dep state + blocked. Takes task.modify's expected_rev: a stale one is a conflict and nothing is written.
Objects: Dependency.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
depends_oninteger or stringrequired- The task
refmust wait for: short_id or UUID. expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. depends_onarrayalways- Every short id this task now waits on.
blockedbooleanalways- Whether the task is blocked after the call.
insertedbooleanalways- False when the edge was already there — a re-run told apart from a new edge.
echo '{"tasqx":"1","id":"da1","method":"dependency.add","params":{"ref":"55","depends_on":"50"}}' | tasqx api{
"id": "da1",
"ok": true,
"result": {
"blocked": true,
"depends_on": [
50
],
"inserted": true,
"short_id": 55
},
"tasqx": "1"
}tasqx_add_dependency — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"depends_on":"50","ref":"55"},"name":"tasqx_add_dependency"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx depdep — Make one task depend on another.
dependency.remove
Dep state + blocked. Takes task.modify's expected_rev: a stale one is a conflict and nothing is written.
Objects: Dependency.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
depends_oninteger or stringrequired- The blocker to stop waiting for: short_id or UUID.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. depends_onarrayalways- Every short id this task still waits on.
blockedbooleanalways- Whether the task is still blocked — whether it is actionable now.
echo '{"tasqx":"1","id":"dr1","method":"dependency.remove","params":{"ref":"51","depends_on":"50"}}' | tasqx api{
"id": "dr1",
"ok": true,
"result": {
"blocked": false,
"depends_on": [],
"short_id": 51
},
"tasqx": "1"
}tasqx_remove_dependency — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"depends_on":"50","ref":"51"},"name":"tasqx_remove_dependency"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx undepundep — Remove a dependency edge.
Link methods
link.add
{id, from, to, relation, metadata, created_at, created} — one explicit edge of the knowledge graph (D160). from and to are NODE references: a task short id or uuid, a bare uuid, or task:/memory:/annotation:/project: plus an id (a project also by name), and they come back resolved as <type>:<uuid>. relation is one of references, supersedes, implements_decision, derived_from, contradicts; anything else is refused by name. Adding the same from/to/relation twice is idempotent and answers created: false with the id the first call minted; a self-link is refused and a cycle is not. expected_rev guards the from endpoint when it has one (a task or a doc) and is ignored for an annotation or project, which carry no rev.
Objects: Link.
Parameters
frominteger or stringrequired- The node the link starts at. A task short id (`42`), a bare uuid (tried against tasks, memory docs, annotations and projects in that order), or `task:`/`memory:`/`annotation:`/`project:` plus an id — a project also by name (`project:work`). An unknown prefix is refused; a well-formed reference naming nothing is `not_found`.
tointeger or stringrequired- The node the link points at, in the same grammar as `from`. Linking a node to itself is refused; a cycle between two nodes is not — nothing schedules work off a link.
relationstringrequired- What the link asserts: `references`, `supersedes`, `implements_decision`, `derived_from` or `contradicts`. Anything else is refused with the list, not stored.
metadataobjectoptional- Your own object, stored verbatim and never interpreted. A repeat of an existing link never overwrites it.
expected_revintegeroptional- Optimistic-concurrency guard on the `from` endpoint, mismatched a `conflict` naming both revs. Ignored when that endpoint is an annotation or a project, which carry no rev.
Response
resultidstringalways- The link's uuid. On a duplicate this is the id the first call minted, not a new one.
fromstringalways- The node the link starts at, as
<type>:<uuid>— the resolved form of whatever reference was sent. tostringalways- The node the link points at, spelled the same way.
relationstringalways- The relation as stored, one of the five D160 fixes.
metadataobjectnullable- The metadata this link carries, or null. On a duplicate it is what the FIRST call stored — a repeat never overwrites.
created_atstringalways- When the link was written.
createdbooleanalways- False when the link was already there, so a re-run is told apart from a new edge. Adding the same from/to/relation twice is idempotent, never an error.
echo '{"tasqx":"1","id":"la1","method":"link.add","params":{"from":"51","to":"memory:eb864f1e-e68a-4d96-af89-597bd0d2d52e","relation":"implements_decision"}}' | tasqx api{
"id": "la1",
"ok": true,
"result": {
"created": true,
"created_at": "2026-09-17T09:12:04Z",
"from": "task:019f7c0a-3d51-7c42-9a08-1f0c4e5b62d7",
"id": "019f8b31-77a4-7f10-8c55-2d7e9a13b004",
"metadata": null,
"relation": "implements_decision",
"to": "memory:eb864f1e-e68a-4d96-af89-597bd0d2d52e"
},
"tasqx": "1"
}Illustrative. a new link mints a fresh v7 id whose low bits are random, and the response also carries the task's own uuid — the pinned clock fixes the timestamp half of each and nothing fixes the rest, so a captured answer would differ on every run.
No MCP tool. An agent reaches this method only through a human running tasqx api.
D160's explicit links are a curation surface: an agent proposing edges between docs, tasks and decisions is exactly the noise the graph's inferred half already covers without persisting anything. A link is durable and shows as fact, so it stays a human act until a promotion flow exists for an agent to ask for one.
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
link.remove
{id, removed}. By the link's own id — removing one that is not there is not_found, not a no-op.
Objects: Link.
Parameters
idstringrequired- The link's own uuid, as `link.add` and `link.list` report it.
Response
resultidstringalways- The link that is gone.
removedbooleanalways- Always true on success. Removing a link that is not there is
not_found, not a no-op.
echo '{"tasqx":"1","id":"lr1","method":"link.remove","params":{"id":"019f8b31-77a4-7f10-8c55-2d7e9a13b004"}}' | tasqx api{
"id": "lr1",
"ok": true,
"result": {
"id": "019f8b31-77a4-7f10-8c55-2d7e9a13b004",
"removed": true
},
"tasqx": "1"
}Illustrative. it echoes the link id it was given, and that id came from a link.add whose own answer cannot be captured — so a fixture here would pin a uuid no other example on this page can produce.
No MCP tool. An agent reaches this method only through a human running tasqx api.
the corrective half of link.add, off for the same reason: an agent that cannot write an edge has nothing of its own to retract, and deleting a human's curated link unsupervised is a bigger foot-gun than the gap it closes (token.remove's reasoning).
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
link.list
{count, total, next_offset, links} — the same paging shape as task.list (D70), newest first. ref is a node reference and matches EITHER endpoint, so a node's whole neighbourhood is one call; omit it to list the store's links. relation narrows to one of the five. An omitted limit defaults to 100 and a named one is clamped to 1,000.
Objects: Link.
Parameters
refinteger or stringoptional- Only links with this node at EITHER end, in `link.add`'s reference grammar. Omitted, the whole store's links come back.
relationstringoptional- Only links asserting this relation. An unknown one is refused, not read as a filter matching nothing.
limitintegeroptionaldefault100- How many links to return, newest first. Clamped to 1,000.
offsetintegeroptionaldefault0- Where the page starts. `next_offset` from the previous page keeps the walk going.
Response
resultcountintegeralways- How many links came back in this page.
totalintegeralways- How many matched before
limitandoffsetcut the page. next_offsetintegernullable- The offset that keeps walking, or null once nothing is left.
linksarrayalways- The links themselves, newest first. A
refmatches EITHER endpoint, so a node's neighbourhood is one call.
result.links[]idstringalways- The link's uuid — what
link.removetakes. fromstringalways- The node the link starts at, as
<type>:<uuid>:task:,memory:,annotation:orproject:. tostringalways- The node the link points at, spelled the same way.
relationstringalways- What the link asserts:
references,supersedes,implements_decision,derived_from,contradictsorduplicates(D238). metadataobjectnullable- The caller's own object, stored verbatim and never interpreted, or null.
created_atstringalways- When the link was written. Named
created_atbecauselink.addanswers with a booleancreatedbeside it.
echo '{"tasqx":"1","id":"ll1","method":"link.list","params":{"ref":"51"}}' | tasqx api{
"id": "ll1",
"ok": true,
"result": {
"count": 0,
"links": [],
"next_offset": null,
"total": 0
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
nothing asks for it yet: an agent reaching the graph wants a node's neighbourhood with labels, which is graph.query's job, not a page of endpoint ids it would then have to resolve one by one.
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
Graph methods
graph.query
{root, depth, select, nodes, edges, node_count, edge_count, truncated, omitted_nodes, omitted_edges, include_inferred} — one bounded projection of the knowledge graph around root (D160), which is a node reference in link.add's grammar. With root: null and a select (blocked or orphans) it answers a whole-store question instead, under the same caps, and depth is refused. Nodes come back ordered by depth, kind then id and edges by relation, from then to, so the same call twice is the same JSON. depth is 0–4 (default 2), max_nodes 1–1,000 (default 250) and max_edges 1–5,000 (default 750), each refused by name rather than clamped; truncated and the two omitted counts say when a cap bit. Every edge names its kind and source: structural for one read out of the store, inferred with a confidence for the search_match and shared_tag edges include_inferred computes and nothing stores.
Objects: Graph.
Parameters
rootinteger or stringrequired- The node the projection is anchored at (required unless `select` is given), in `link.add`'s reference grammar: a task short id (`42`), a bare uuid, or `task:`/`memory:`/`annotation:`/`project:` plus an id. It is returned resolved as `
: `, and it is kept whatever the filters below say — a projection with no anchor in it has nothing to project from. selectstringoptional- A whole-store question instead of a walk: send `"root": null` (or leave `root` out) and name the set. `blocked` is every open task with an unmet blocker, those blockers, and the `depends_on` edges between them, with the waiting tasks first; `orphans` is every task, memory document and project with no edge of a relation the walk crosses (a note is never one), and draws no edges. Naming a `root` as well, or a `depth`, is refused, and so is any other value, with the list. `node_types`, `relation_types`, `project`, `status`, `tags`, the dates, `include_inferred` and the two caps apply as they do to a walk, with one exception: `search_match` needs a root and is never added. The answer is capped like any other (`truncated`, `omitted_nodes`), but the scan reads every blocked task or every id first, so on a very large store narrow it with a filter.
depthintegeroptionaldefault2- How many hops to walk, 0 to 4. Out of range is refused, never clamped: a caller who asked for 5 cannot tell a clamped answer from the graph really ending there.
node_typesarray of stringoptional- Keep only these kinds of node: `task`, `memory`, `annotation`, `project`. A dropped node is not expanded either, so this bounds the cost as well as the answer. An empty array filters nothing.
relation_typesarray of stringoptional- Only cross these relations: `depends_on`, `has_annotation`, `belongs_to_project`, the five link relations, and `search_match`/`shared_tag` for the inferred ones. An unknown name is refused with the list. An empty array crosses every relation.
projectstringoptional- Keep only the nodes filed under this project — tasks and memory documents. A project node and the notes on a kept task pass through, because neither is filed anywhere of its own.
statusstringoptional- Keep only tasks in this status, read the way every other surface reads it (a future `wait` shows `backlog`). Nodes that are not tasks are unaffected.
tagsarray of stringoptional- Keep only tasks carrying at least one of these tags. Nodes that are not tasks are unaffected.
modified_afterstringoptional- An RFC 3339 lower bound on a task's or document's `modified` and a note's `created`. A project carries no such date and is never excluded by the window.
modified_beforestringoptional- The upper bound of the same window, also RFC 3339.
include_inferredbooleanoptionaldefaultfalse- Add the computed edges: up to ten `search_match` hits for the root's own title, and a `shared_tag` edge between every pair of returned tasks sharing a tag. Each carries a `confidence` and the `source` that produced it, and none of them is ever stored.
max_nodesintegeroptionaldefault250- How many nodes the answer may carry, 1 to 1,000. Applied to the deterministic order, so the cut is the same every time; `omitted_nodes` says how many went.
max_edgesintegeroptionaldefault750- How many edges the answer may carry, 1 to 5,000. Applied after the edges whose endpoints were cut have already gone; `omitted_edges` says how many were left out.
Response
resultrootstringnullable- The node the projection is anchored at, resolved to its stable
<type>:<uuid>id. Null on a whole-storeselect, which has no anchor. depthintegeralways- How many hops were walked — the
depththat was asked for, or 2. Always 0 on a whole-storeselect, which walks nothing. selectstringnullable- The whole-store set that was asked for (
blockedororphans), or null on a walk from aroot. nodesarrayalways- The nodes, ordered by depth, then kind, then id. Deterministic: the same call twice is the same list.
edgesarrayalways- The edges, ordered by relation, then
from, thento. Only edges with both endpoints among the nodes above. node_countintegeralways- How many nodes came back — the length of
nodes, always present so a caller need not count. edge_countintegeralways- How many edges came back.
truncatedbooleanalways- Whether a cap bit, i.e. whether either omitted figure below is non-zero. A client that cannot tell a graph that ends from one that was cut draws the wrong picture.
omitted_nodesintegeralways- How many nodes the walk found and
max_nodescut. omitted_edgesintegeralways- How many edges
max_edgescut, after the ones whose endpoints were cut had already gone. include_inferredbooleanalways- Whether the computed edges were asked for, echoed so a cached answer says which kind of graph it is.
result.nodes[]idstringalways- The node's stable id,
<type>:<uuid>— the same spelling a link's endpoints use, and what a follow-upgraph.querytakes as itsroot. typestringalways- Which kind of node:
task,memory,annotationorproject. labelstringalways- The one line to draw in the box: a task's title, a document's title, a note's first line (cut to 80 characters) or a project's name.
summarystringnullable- The second line:
#<short_id> · <status> · <priority>for a task, the source for a document, the owning task for a note, the description for a project. Null when a project has no description. projectstringnullable- The project this node is filed under, or null — including on a project node, which is not filed under itself.
statusstringnullable- A task's status, read the way every other surface reads it (a future
waitshowsbacklog). Null on every other kind of node. modifiedstringnullable- When the node last changed:
modifiedfor a task or a document,createdfor a note, which carries no modified time, so an edit does not move it. Null for a project, which carries no such column — so the date window cannot exclude one. short_idintegernullable- The small number a human types for a task. Null on every other kind of node.
taskstringnullable- The node id of the task a note is written on. Null on every other kind of node.
result.edges[]idstringalways- The edge's id, stable for the same edge across calls:
dep:,ann:,member:orlink:for a stored one,search:ortag:for a computed one. fromstringalways- The node id the edge starts at. A
depends_onedge always points from the task that waits to the one it waits on, whichever end the walk reached it from. tostringalways- The node id the edge points at.
relationstringalways- What the edge asserts:
depends_on,has_annotation,belongs_to_project, one of the five link relations, orsearch_match/shared_tagwheninclude_inferredasked for them. kindstringalwaysstructuralfor an edge read out of a table,inferredfor one computed for this call and never stored.confidencenumbernullable- How strong a computed edge is, 0 to 1 within this answer. Null on a structural edge: a stored fact has no confidence to report.
sourcestringalways- Where the edge came from: the table (
dependencies,annotations,tasks.project,docs.project,links) or what computed it (memory.search: <words>,tags: <shared>).
echo '{"tasqx":"1","id":"gq1","method":"graph.query","params":{"root":"51","depth":1}}' | tasqx api{
"id": "gq1",
"ok": true,
"result": {
"depth": 1,
"edge_count": 6,
"edges": [
{
"confidence": null,
"from": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"id": "member:task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"kind": "structural",
"relation": "belongs_to_project",
"source": "tasks.project",
"to": "project:309d6b79-965e-4a32-9ae4-45508201e2bd"
},
{
"confidence": null,
"from": "task:813547e2-5937-41f0-8401-82fcdb14a009",
"id": "member:task:813547e2-5937-41f0-8401-82fcdb14a009",
"kind": "structural",
"relation": "belongs_to_project",
"source": "tasks.project",
"to": "project:309d6b79-965e-4a32-9ae4-45508201e2bd"
},
{
"confidence": null,
"from": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"id": "dep:task:46773aad-c4aa-435a-abe1-fcde8ce09658:task:813547e2-5937-41f0-8401-82fcdb14a009",
"kind": "structural",
"relation": "depends_on",
"source": "dependencies",
"to": "task:813547e2-5937-41f0-8401-82fcdb14a009"
},
{
"confidence": null,
"from": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"id": "ann:17dd6621-7db4-43b5-9f36-ddf89018081e",
"kind": "structural",
"relation": "has_annotation",
"source": "annotations",
"to": "annotation:17dd6621-7db4-43b5-9f36-ddf89018081e"
},
{
"confidence": null,
"from": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"id": "ann:ee4a9a3b-10de-465a-aab1-84eeb0e48236",
"kind": "structural",
"relation": "has_annotation",
"source": "annotations",
"to": "annotation:ee4a9a3b-10de-465a-aab1-84eeb0e48236"
},
{
"confidence": null,
"from": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"id": "ann:fd496ca3-cd12-4457-8b43-5ef0668cab3c",
"kind": "structural",
"relation": "has_annotation",
"source": "annotations",
"to": "annotation:fd496ca3-cd12-4457-8b43-5ef0668cab3c"
}
],
"include_inferred": false,
"node_count": 6,
"nodes": [
{
"id": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"label": "Write the migration guide for SDK 3.0",
"modified": "2026-08-16T10:00:00Z",
"project": "api",
"short_id": 51,
"status": "pending",
"summary": "#51 · pending · M",
"task": null,
"type": "task"
},
{
"id": "task:813547e2-5937-41f0-8401-82fcdb14a009",
"label": "Rate-limit the /search endpoint",
"modified": "2026-08-31T10:00:00Z",
"project": "api",
"short_id": 50,
"status": "pending",
"summary": "#50 · pending · H",
"task": null,
"type": "task"
},
{
"id": "annotation:17dd6621-7db4-43b5-9f36-ddf89018081e",
"label": "Ruling: the guide ships WITH the 3.0 release, not after it. A migration note th…",
"modified": "2026-09-11T11:20:00Z",
"project": null,
"short_id": null,
"status": null,
"summary": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"task": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"type": "annotation"
},
{
"id": "annotation:ee4a9a3b-10de-465a-aab1-84eeb0e48236",
"label": "Blocked until the rate limit lands: the guide has to state the real ceiling, an…",
"modified": "2026-09-15T11:20:00Z",
"project": null,
"short_id": null,
"status": null,
"summary": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"task": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"type": "annotation"
},
{
"id": "annotation:fd496ca3-cd12-4457-8b43-5ef0668cab3c",
"label": "Scope: the 2.x → 3.0 rename table, the auth change, and a worked example per SD…",
"modified": "2026-09-04T11:20:00Z",
"project": null,
"short_id": null,
"status": null,
"summary": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"task": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"type": "annotation"
},
{
"id": "project:309d6b79-965e-4a32-9ae4-45508201e2bd",
"label": "api",
"modified": null,
"project": null,
"short_id": null,
"status": null,
"summary": "Public REST API and its SDKs",
"task": null,
"type": "project"
}
],
"omitted_edges": 0,
"omitted_nodes": 0,
"root": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"select": null,
"truncated": false
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
an agent already reaches the graph's ends: tasqx_search_memory returns the docs and annotations the inferred half would match, and tasqx_get_task returns a task's notes, checks and dependencies. What graph.query adds over those is a LAYOUT — nodes, edges and provenance to draw — and that is a desktop client's question (D160), not something a model gains by reading two hundred node objects into its context.
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
Memory methods
memory.add
{id, title, project, standing, created}. Body stored verbatim (D41). project is optional free-standing scoping (#134); an unset doc stays global rather than defaulting onto whatever project is current. standing (default false) marks a ruling meant for every session of its scope, and above fifteen such docs in one scope the result also carries a hint to merge or retract (D156).
Objects: Memory document.
Parameters
titlestringrequired- The doc's title. Indexed with the body, so the words a future search will use belong in it.
bodystringrequired- Stored verbatim; multi-line markdown is fine.
sourcestringoptional- Where this came from: a path, URL, or ticket. One doc per source: one another doc holds is a
conflictnaming it (D174). projectstringoptional- Optional project scope. Omit to leave the doc unscoped; it is never defaulted onto a current project.
standingbooleanoptional- True marks a ruling that belongs in every session of its scope until retracted; default false (D156).
Response
resultidstringalways- The new doc's uuid — what a search hit carries and
memory.gettakes. titlestringalways- The doc's title, as stored.
projectstringnullable- The project the doc is scoped to, or null for a global one (#134).
standingbooleanalways- Whether the doc was stored as a standing ruling — one meant for every session of its scope (D156).
createdstringalways- When it was stored.
hintstringoptional- Present only when this add pushed its scope past the soft cap on standing docs: a nudge to merge or retract one. Test for the key, not its value (D156).
similararrayalways- Always present: up to three docs of the same project or none, with a vector in the search index, that look like this one, best first, each
{id, title, similarity}; empty when none does. A warning only — the doc was stored either way (D237). similar_skippedbooleanalways- Always present: true when the comparison could not run, so
similaris empty for that reason rather than because nothing looks alike; the doc was stored either way (D237).
result.similar[]idstringalways- The existing doc's uuid — what
memory.gettakes. titlestringalways- The existing doc's title.
similaritynumberalways- How alike the two read: the new doc's embedding against the existing doc's closest chunk, rounded to three decimals; at or above the threshold D237 tuned.
echo '{"tasqx":"1","id":"ma1","method":"memory.add","params":{"title":"rate-limit-ceiling","body":"60 requests a minute per key, burst 120.","source":"notes/limits.md","project":"api"}}' | tasqx api{
"id": "ma1",
"ok": true,
"result": {
"created": "2026-09-16T09:00:00Z",
"id": "019f7c0a-3d51-7b18-8d44-6e1a92c07f35",
"project": "api",
"similar": [
{
"id": "019f6e21-8c04-7a93-b5d2-41f0c8e7a1b6",
"similarity": 0.842,
"title": "api-rate-limits"
}
],
"similar_skipped": false,
"standing": false,
"title": "rate-limit-ceiling"
},
"tasqx": "1"
}Illustrative. the doc's id is minted fresh, v7, half of it random.
tasqx_add_memory — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"60 requests a minute per key, burst 120.","project":"api","source":"notes/limits.md","title":"rate-limit-ceiling"},"name":"tasqx_add_memory"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
memory.search
{count, total, has_more, hits, matched, matched_any, relaxed, semantic} — docs + annotations ranked by meaning and by words (D196): an embedding model built into the binary lists entries whose best passage is at least min_similarity (0 to 1, default 0.30) close to the query, FTS5 lists them by bm25, stemmed (porter tokenizer, #128) so "reviewing" matches a doc that only says "review", and the lists are fused by weighted reciprocal rank. Each hit says via (lexical, semantic, both), its similarity and fused score; rank is the negated score, lower is better. mode is hybrid (default), lexical (words only, as raw always is) or semantic; a query with no word of three letters (an id such as D41) is matched by words alone, and semantic is null. matched is the FTS5 expression actually run, which is how count: 0 is told apart from a store holding nothing on the subject. In hybrid mode the same words joined with OR run as a third list, weighted below the others (matched_any); a hit only it found says partial, and relaxed is true when one is on the page (D193). Under mode: lexical the OR runs only when nothing holds every word, as before. total is every entry either side matched before limit truncates (#132), and project scopes to one project's docs plus its tasks' annotations (#134), and source_prefix keeps only docs whose stored source starts with it, literally and case-sensitively, before the page and total (D240). Each hit carries its own project too (null for global knowledge, #657), so a store-wide search says which project it came from.
Objects: Memory document.
Parameters
querystringrequired- Plain-text search words. Matched as quoted phrases, so hyphens and dots are safe.
limitintegeroptional- Max hits (0 or more); default 10. The response's
total/has_moresay what this left out. scopestringoptional- What to search. Optional; defaults to all.
rawbooleanoptional- Pass the query through as FTS5 syntax (words only: needs mode lexical or none). Invalid syntax is bad_request.
projectstringoptional- Scope to one project: docs stored with it, and annotations whose task carries it. Omit to search every project.
include_unscopedbooleanoptional- Widen a
projectscope to docs and annotations with NO project — whattasqx memory importproduces. Never admits another project's. Refused withoutproject. source_prefixstringoptional- Docs whose source starts with this, literally (D240).
modestringoptional- Default hybrid: meaning and words. lexical: words only.
min_similaritynumberoptional- Meaning floor; default 0.30.
workdirstringoptional- The caller's working directory, absolute. Each doc hit's `stale_refs` is checked against its git toplevel; omitted, `stale_refs` is null (D235). The MCP server passes its own.
Response
resultcountintegeralways- How many hits came back.
totalintegeralways- How many entries matched before
limittruncated: every all-words match and every semantic one at or above the floor, counted once. An entry only the any-word list found is not counted unless those two found nothing (#132, D196). has_morebooleanalways- Whether a counted match (see
total) was left behind. hitsarrayalways- The hits, best first: the all-words, any-word and semantic lists fused by weighted reciprocal rank, docs and annotations together (D196).
matchedstringnullable- The FTS5 expression actually run — how
count: 0is told apart from a store holding nothing on the subject. Null undermode: semantic, which runs none. relaxedbooleanalways- Whether no entry held every word of a plain query and the any-word list ran (D193). In hybrid mode that list always runs for two content words or more and its hits say
partial; undermode: lexicalit runs only when no entry has every word, and thenmatchedis that OR. Always false forrawandmode: semantic. matched_anystringnullable- The any-word OR expression, when it ran (#838): always in hybrid mode for a query of two content words or more, only as D193's fallback under
mode: lexical. semanticobjectnullable- The meaning side's
{model, min_similarity}, or null when it did not run:mode: lexicalorraw, a query with no word of three letters or more that is not a stopword (an id such asD41), fewer than half its content words known to the model whole, or nothing to compare against —semantic_skippedsays which (D196). semantic_skippedobjectnullable- Why the meaning side did not run when the mode asked for it:
{code, reason, content_words, known_words}; null when it ran, and undermode: lexicalorraw(#838).
result.hits[]idstringalways- The doc's id, or the annotation's.
kindstringalwaysdocorannotation— which store the hit came from.titlestringalways- The doc's title; for an annotation, the task it is written on.
sourcestringnullable- Where the doc came from (a file path,
task:#51), or null. snippetstringalways- The matching passage, with the hit in context — for a hit found by meaning alone, the start of its best chunk. Never the whole body.
ranknumberalways- The negated fused
score(D196). Lower is a better match and the hits arrive in ascendingrank; the number is not comparable between searches and is not a threshold (D154). standingbooleannullable- For a doc hit, whether it is a standing ruling (D156); null on an annotation hit, which has no such flag.
projectstringnullable- Which project this hit is scoped to — a doc's own column, or the annotation's task's — or null for global knowledge (#657).
stalebooleannullable- true when the doc's origin file no longer matches what was imported; null for annotations and docs with no origin (D180).
stale_refsarraynullable- For a doc hit, the paths and
path::symbolrefs it cites that are gone from the git toplevel of the caller'sworkdir; empty when none are, or none can be checked. Null on an annotation hit and when noworkdirwas given (D235). modifiedstringalways- When the entry last changed: a doc's
modified, an annotation'screated(annotations carry no modified time, so an edit does not move it, the same rulegraph.queryuses for a node) — what a date window filters hits on (D219). viastringalways- Which side found it:
lexical(words — all of them, whichmatchedexplains, or only some, whichpartialsays andmatched_anyexplains),semantic(meaning;similarityexplains it) orboth(D196). similaritynumbernullable- Its best chunk's cosine to the query, to three decimals — also on a hit the words found, when the entry has a vector; null when it has none or the semantic side did not run.
scorenumberalways- The fused score: Σ weight/(20 + rank) over the lists it is on — every word 1.0, any word 0.35, meaning 1.3 (D196, #838). Higher is better.
partialbooleanalways- true when the only words that found it are D193's any-word list: it lacks some of the query's words.
relaxedis true when any hit on the page is.
result.semanticmodelstringalways- The embedding model's id: the table, its revision, the quantisation, and the tokenizer and chunker versions.
min_similaritynumberalways- The floor a semantic hit's best chunk reached, rounded to three decimals (default 0.30).
result.semantic_skippedcodestringalways- Why, as a stable code to branch on:
no_content_word(no word of three letters or more — an id such asD41),unknown_words(fewer than half the content words are one whole vocabulary token, or none is known),no_vectors(nothing indexed in scope to compare against),unavailable(the meaning index could not be read this time; try again). reasonstringalways- The same, in words for a person; its text may change.
content_wordsintegeralways- The query's content words: whitespace-separated words, trimmed of punctuation, with three letters or more and not a stopword — an identifier such as
D41is not one. known_wordsintegeralways- How many of them the model knows whole: every piece the tokenizer splits the word into, punctuation left out, is one vocabulary token, not
##pieces and not[UNK].
echo '{"tasqx":"1","id":"ms1","method":"memory.search","params":{"query":"release canary","limit":2}}' | tasqx api{
"id": "ms1",
"ok": true,
"result": {
"count": 2,
"has_more": false,
"hits": [
{
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"kind": "doc",
"modified": "2026-09-18T09:30:12.481Z",
"partial": false,
"project": "api",
"rank": -0.12543290043290042,
"score": 0.12543290043290042,
"similarity": 0.538,
"snippet": "…Cut the release branch on Monday, tag after the canary has run…",
"source": "docs/release.md",
"stale": null,
"stale_refs": null,
"standing": false,
"title": "release-process",
"via": "both"
},
{
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e",
"kind": "annotation",
"modified": "2026-09-18T09:30:12.481Z",
"partial": true,
"project": "api",
"rank": -0.016666666666666666,
"score": 0.016666666666666666,
"similarity": 0.233,
"snippet": "…the guide ships WITH the 3.0 release, not after it. A…",
"source": "task:#51",
"stale": null,
"stale_refs": null,
"standing": null,
"title": "Write the migration guide for SDK 3.0",
"via": "lexical"
}
],
"matched": "\"release\" \"canary\"",
"matched_any": "\"release\" OR \"canary\"",
"relaxed": false,
"semantic": {
"min_similarity": 0.3,
"model": "potion-base-8M@bf8b056651a2c21b8d2565580b8569da283cab23/int8/tok2/chunk4"
},
"semantic_skipped": null,
"total": 1
},
"tasqx": "1"
}Illustrative. every hit carries a rank. Since D196 it is the negated fused reciprocal rank, not bm25, but the words list is still in bm25 order, computed through the platform's log(), so two near-equal hits can trade places between machines and a capture is reproducible only on the one that took it. The tasqx memory search screen is captured instead — it prints the snippet, and a similarity only on a hit found by meaning alone.
tasqx_search_memory — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"limit":2,"query":"release canary"},"name":"tasqx_search_memory"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
memory.get
{id, title, body, source, project, rev, created, modified} — one doc whole, by the id a search hit carries. An annotation id is refused, naming the task to read it from.
Objects: Memory document.
Parameters
idstringrequired- The doc UUID, as a search hit or
tasqx_add_memoryprints it. workdirstringoptional- The caller's working directory, absolute. `stale_refs` is checked against its git toplevel; omitted, it is null (D235). The MCP server passes its own.
Response
resultidstringalways- The doc's uuid.
sourcestringnullable- Where it came from — the path
memory.importkeyed on, or null. titlestringalways- The doc's title.
bodystringalways- The whole body, stored and returned verbatim (D41).
createdstringalways- When it was first stored.
modifiedstringalways- When it last changed.
projectstringnullable- The project it is scoped to, or null.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. standingbooleanalways- Whether it is a standing ruling, meant for every session of its scope (D156). Stated on every doc, so a restore carries it.
origin_pathstringnullable- The absolute path of the file
memory.importread this doc from, or null when no import set one (D180). origin_mtimeintegernullable- That file's modification time in unix seconds when it was read, or null (D180).
origin_sizeintegernullable- That file's size in bytes when it was read, or null (D180).
stale_refsarraynullable- The paths and
path::symbolrefs the doc cites that are gone from the git toplevel of the caller'sworkdir; empty when none are, or none can be checked; null when noworkdirwas given (D235).
echo '{"tasqx":"1","id":"mg1","method":"memory.get","params":{"id":"eb864f1e-e68a-4d96-af89-597bd0d2d52e"}}' | tasqx api{
"id": "mg1",
"ok": true,
"result": {
"_rev": 0,
"body": "---\ndescription: How an SDK release is cut, tagged and announced\n---\nCut the release branch on Monday, tag after the canary has run for a day, and announce in the changelog feed once the packages are live.",
"created": "2026-09-05T09:15:00Z",
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"modified": "2026-09-10T09:15:00Z",
"origin_mtime": null,
"origin_path": null,
"origin_size": null,
"project": "api",
"source": "docs/release.md",
"stale_refs": null,
"standing": false,
"title": "release-process"
},
"tasqx": "1"
}tasqx_get_memory — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":"eb864f1e-e68a-4d96-af89-597bd0d2d52e"},"name":"tasqx_get_memory"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
memory.remove
{id, removed}.
Objects: Memory document.
Parameters
idstringrequired- The document's id, as tasqx_search_memory reports it on a
dochit.
Response
resultidstringalways- The doc that is gone.
removedbooleanalways- Always true on success. Removal is permanent and outside
undo.
echo '{"tasqx":"1","id":"mr1","method":"memory.remove","params":{"id":"091eb5ff-05d5-4cb2-ba2f-0afdc77f7935"}}' | tasqx api{
"id": "mr1",
"ok": true,
"result": {
"id": "091eb5ff-05d5-4cb2-ba2f-0afdc77f7935",
"removed": true
},
"tasqx": "1"
}tasqx_remove_memory — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":"091eb5ff-05d5-4cb2-ba2f-0afdc77f7935"},"name":"tasqx_remove_memory"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
memory.import
{imported, replaced, docs}, each doc {id, title, source, project, replaced, _rev}. One transaction; same source replaces IN PLACE (id and creation date kept), bumps that doc's _rev (D143) and is counted in replaced; a batch naming one source twice is refused whole (D174). project scopes every doc in the batch (#657): omitted, an existing doc keeps its prior scope (like standing) and a new one lands global; named, it moves an existing doc's scope on re-import, the same way memory.update --project would.
Objects: Memory document.
Parameters
docsarray of objectrequired- The documents to store, each `{title, body, source?}`. One transaction: same `source` replaces in place, keeping the doc's id and creation date (D143); a batch naming one `source` twice is refused whole (D174). A doc may also carry `origin_path` (the absolute path of the file it was read from), `origin_mtime` (that file's modification time in unix seconds) and `origin_size` (its size in bytes) — all optional, stored as given and overwritten on every re-import, never used as identity (D180). A replace whose stored doc recorded a different `origin_path` that still exists is refused whole with `conflict`, naming the doc and both files (D203); every doc a replace did overwrite is named under `replaced_docs`. A doc whose title and body already match the stored one is left alone (no new `rev`, no event) and counted under `unchanged` (#86).
projectstringoptional- Scopes every doc in the batch (#657) — one value for the whole import, not per-doc. Omitted, a new doc lands global and an existing one (a re-import) keeps whatever scope it already had, like `standing`; named, it MOVES an existing doc's scope on re-import.
Response
resultimportedintegeralways- How many docs the batch carried, unchanged ones included.
replacedintegeralways- How many of those replaced a doc with the same
source, in place, because its title, body or named project differed. unchangedintegeralways- How many matched a stored doc's title and body exactly and were left alone: no new
rev, no event, not inreplaced_docs(#86). docsarrayalways- One row per document in the batch.
replaced_docsarrayalways- One row per existing doc the batch replaced in place, naming the title it had before — always present, empty when nothing was replaced (#972).
result.docs[]idstringalways- The doc's id — the one it already had when it was replaced in place.
titlestringalways- Its title.
sourcestringnullable- The source it was keyed on.
projectstringnullable- The batch's
project, echoed per doc — null when the import named none (#657). replacedbooleanalways- Whether this row replaced an existing doc rather than creating one (D143).
unchangedbooleanalways- Whether this row matched the stored doc exactly and was left alone,
_revas it was (#86). _revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional.
result.replaced_docs[]idstringalways- The replaced doc's id, which it keeps.
sourcestringalways- The source it is keyed on.
previous_titlestringalways- The title it had before this import replaced it.
echo '{"tasqx":"1","id":"mi1","method":"memory.import","params":{"docs":[{"title":"release-process","body":"Cut the release branch on Monday.","source":"docs/release.md"}]}}' | tasqx api{
"id": "mi1",
"ok": true,
"result": {
"docs": [
{
"_rev": 1,
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"project": null,
"replaced": true,
"source": "docs/release.md",
"title": "release-process",
"unchanged": false
}
],
"imported": 1,
"replaced": 1,
"replaced_docs": [
{
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"previous_title": "release-process",
"source": "docs/release.md"
}
],
"unchanged": 0
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
it takes a batch of documents read off a filesystem, and the filesystem the CLI reads is not the one an MCP client is on. memory.add is the per-document tool that does reach across the wire.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
memory.refresh
{checked, refreshed, missing, unchanged} — re-reads every doc that recorded an origin file (D180) whose file has changed since, through the same upsert a re-import uses: id and creation date kept, rev bumped (D143), project and standing left alone. Stored mtime and size are the fast filter and the bytes are the truth, so a touch counts as unchanged. A doc whose file cannot be read is listed under missing and left exactly as it is — nothing is ever deleted. dry_run reports the same answer and writes nothing.
Objects: Memory document.
Parameters
dry_runbooleanoptionaldefaultfalse- Check every doc and report, without writing anything. The same answer the real sweep would give — which docs would be re-read, which files are gone — so it can be looked at before a store full of documents is rewritten.
Response
resultcheckedintegeralways- How many docs named an origin file and were examined.
refreshed,missingandunchangedaccount for all of them. refreshedarrayalways- The docs whose file had changed and were re-read from it.
missingarrayalways- The docs whose file could not be read — gone, renamed, or on a volume that is not mounted. Reported, never removed.
unchangedintegeralways- How many docs still matched their file, byte for byte, and were left alone — their
revdid not move.
result.refreshed[]idstringalways- The doc's id, unchanged — a refresh replaces the text in place.
sourcestringnullable- The source it is keyed on, or null for a doc that has an origin file and no source.
result.missing[]idstringalways- The doc, still stored with the text it already had.
sourcestringnullable- The source it is keyed on, or null.
origin_pathstringalways- The file that could not be read, as the import recorded it (D180).
echo '{"tasqx":"1","id":"mf1","method":"memory.refresh","params":{"dry_run":true}}' | tasqx api{
"id": "mf1",
"ok": true,
"result": {
"checked": 3,
"missing": [
{
"id": "091eb5ff-05d5-4cb2-ba2f-0afdc77f7935",
"origin_path": "/home/you/repo/docs/retired.md",
"source": "docs/retired.md"
}
],
"refreshed": [
{
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"source": "docs/release.md"
}
],
"unchanged": 1
},
"tasqx": "1"
}Illustrative. the answer is whatever the files under the store's own imports look like right now, so a captured one would describe the capturing machine's disk rather than the method.
No MCP tool. An agent reaches this method only through a human running tasqx api.
the sweep re-reads the files memory.import recorded on THIS machine (D180), and the filesystem an MCP client is on is not that one — memory.import's own reason, one method over. Keeping a server's own docs fresh is the server's job, reached by calling the engine, not an agent's to trigger across the wire.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
memory.list
{count, total, next_offset, docs} — the same paging shape as task.list (#133). Browses docs newest-modified first, without a query; each row carries a body_preview, not the full body, and a standing flag; standing filters to only standing docs (true) or only the rest (false), omitted for all (D156).
Objects: Memory document.
Parameters
limitintegeroptional- Rows to return; default 20, because an unbounded browse page grows with the store.
offsetintegeroptional- Matching docs to skip; pass the previous response's
next_offset. projectstringoptional- Restrict to docs stored with this
project. Omit to list every doc. standingbooleanoptional- true lists only standing docs, false only the rest; omit for all (D156).
Response
resultcountintegeralways- How many docs came back.
totalintegeralways- How many docs the scope holds.
next_offsetintegernullable- The
offsetthat fetches the next page; null once nothing is left. docsarrayalways- The docs, newest-modified first.
result.docs[]idstringalways- The doc's uuid.
titlestringalways- Its title.
sourcestringnullable- Where it came from, or null.
projectstringnullable- The project it is scoped to, or null.
createdstringalways- When it was stored.
modifiedstringalways- When it last changed — the column this list sorts by.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. body_previewstringalways- The opening of the body, or all of it when the body is short (
body_truncatedsays which) — browsing pays for a preview, not a payload. body_truncatedbooleanalways- Whether the preview is shorter than the body.
standingbooleanalways- Whether it is a standing ruling (D156) — on every row, not only on a page filtered by
standing.
echo '{"tasqx":"1","id":"m1","method":"memory.list"}' | tasqx api{
"id": "m1",
"ok": true,
"result": {
"count": 5,
"docs": [
{
"_rev": 0,
"body_preview": "# Pricing page\nThree tiers, annual billing shown first. The release of v2 waits for legal to sign off the new terms.",
"body_truncated": false,
"created": "2026-09-09T09:15:00Z",
"id": "0787b26d-9e2e-4be5-ab66-ec953102fad3",
"modified": "2026-09-14T09:15:00Z",
"project": "website",
"source": "notes/pricing.md",
"standing": false,
"title": "pricing-page-decisions"
},
{
"_rev": 0,
"body_preview": "---\ndescription: How an SDK release is cut, tagged and announced\n---\nCut the release branch on Monday, tag after the canary has run for a day, and announce in t",
"body_truncated": true,
"created": "2026-09-05T09:15:00Z",
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"modified": "2026-09-10T09:15:00Z",
"project": "api",
"source": "docs/release.md",
"standing": false,
"title": "release-process"
},
{
"_rev": 0,
"body_preview": "The handover happens Friday at 16:00. Walk the open incidents, the error budget and anything paged twice in the week, then rotate the pager.",
"body_truncated": false,
"created": "2026-08-29T09:15:00Z",
"id": "1bce1a9b-5134-4ab7-8665-34cd79fe0c5f",
"modified": "2026-09-03T09:15:00Z",
"project": "infra",
"source": null,
"standing": false,
"title": "on-call-handover"
},
{
"_rev": 0,
"body_preview": "The refresh race happens when two requests see an expired token at once. Serialise refresh behind one lock and retry the losing request.",
"body_truncated": false,
"created": "2026-08-22T09:15:00Z",
"id": "b3bd4390-2124-42ac-829d-f542ecde8a07",
"modified": "2026-08-27T09:15:00Z",
"project": "mobile",
"source": null,
"standing": false,
"title": "android-token-refresh"
},
{
"_rev": 0,
"body_preview": "Dr. Visser, Tuesdays and Thursdays, book two weeks ahead.",
"body_truncated": false,
"created": "2026-08-08T09:15:00Z",
"id": "091eb5ff-05d5-4cb2-ba2f-0afdc77f7935",
"modified": "2026-08-13T09:15:00Z",
"project": null,
"source": null,
"standing": false,
"title": "dentist"
}
],
"next_offset": null,
"total": 5
},
"tasqx": "1"
}tasqx_list_memory — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{},"name":"tasqx_list_memory"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
memory.update
{id, title, source, project, standing, rev, modified} — replaces a doc's fields IN PLACE (#135), the correction path memory.remove's permanence has none of. standing sets or clears the flag and bumps rev like any other edit (D156). expected_rev is task.modify's optimistic-concurrency guard, unchanged: mismatched, it is a conflict naming both revs.
Objects: Memory document.
Parameters
idstringrequired- The doc UUID, as a search hit,
tasqx_add_memoryortasqx_list_memoryprints it. titlestringoptional- New title. Omit to leave unchanged.
bodystringoptional- New body, stored verbatim. Omit to leave unchanged.
sourcestringoptional- New source. Omit to leave unchanged. One another doc holds is a
conflictnaming it (D174). projectstringoptional- New project scope. Omit to leave unchanged.
standingbooleanoptional- Set or clear the standing flag; omit to leave unchanged (D156).
expected_revintegeroptional- Optimistic-concurrency guard. Supplied by the server from the doc's current
revwhen omitted; pass it to pin a rev you read earlier.
Response
resultidstringalways- The doc that changed.
titlestringalways- Its title after the update.
sourcestringnullable- Its source after the update, or null.
projectstringnullable- Its project after the update, or null.
standingbooleanalways- The standing flag as it now stands, whether this call set it, cleared it or left it alone (D156).
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. modifiedstringalways- When the update landed.
echo '{"tasqx":"1","id":"mu1","method":"memory.update","params":{"id":"1bce1a9b-5134-4ab7-8665-34cd79fe0c5f","body":"The handover happens Friday at 16:00, in the on-call channel."}}' | tasqx api{
"id": "mu1",
"ok": true,
"result": {
"_rev": 1,
"id": "1bce1a9b-5134-4ab7-8665-34cd79fe0c5f",
"modified": "2026-09-16T09:00:00Z",
"project": "infra",
"source": null,
"standing": false,
"title": "on-call-handover"
},
"tasqx": "1"
}tasqx_update_memory — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"The handover happens Friday at 16:00, in the on-call channel.","id":"1bce1a9b-5134-4ab7-8665-34cd79fe0c5f"},"name":"tasqx_update_memory"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx memorymemory — Store and search knowledge: docs, patterns, and your task annotations (D41).
Note methods
note.add
The note: {id, short_id, body, project, status, promoted_to, created, modified, _rev, has_links, promoted_task_short_id} (D241). A quick-capture note is neither a task nor a memory doc: it scores no urgency, stays out of @working and reports, and memory.search never sees it. Its short_id comes from its own counter. project must name a live project.
Objects: Note.
Parameters
bodystringrequired- Stored verbatim.
projectstringoptional- An existing project to file it under. Optional.
Response
resultidstringalways- The note's uuid.
short_idintegeralways- The note's own small number, from a counter tasks do not share — what
note.gettakes beside the uuid. bodystringalways- The whole body, stored and returned verbatim.
projectstringnullable- The project it is filed under, or null.
statusstringalwaysopenorarchived.promoted_tostringnullable- Where
note.promotefiled it —task:<uuid>ormemory:<uuid>— or null while it has not been promoted. Terminal: a promoted note stays archived, and a merge keeps the promotion from either side. createdstringalways- When it was captured.
modifiedstringalways- When it last changed.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. has_linksbooleanalways- Whether the body holds an
http://orhttps://link — derived on every read, the same testnote.list {links: true}filters on. promoted_task_short_idintegernullable- The short id the task
promoted_tonames holds in this store, read at the call so a renumbered task reads right; null when the note was not promoted to a task or the task is gone.
echo '{"tasqx":"1","id":"n1","method":"note.add","params":{"body":"try the new tracing exporter","project":"infra"}}' | tasqx api{
"id": "n1",
"ok": true,
"result": {
"_rev": 0,
"body": "try the new tracing exporter",
"created": "2026-09-16T09:00:00Z",
"has_links": false,
"id": "019f5a10-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
"modified": "2026-09-16T09:00:00Z",
"project": "infra",
"promoted_task_short_id": null,
"promoted_to": null,
"short_id": 3,
"status": "open"
},
"tasqx": "1"
}Illustrative. the answer carries the new note's uuid, a v7 id whose random half differs on every run, so a capture could never be reproduced (D149).
tasqx_add_note — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"try the new tracing exporter","project":"infra"},"name":"tasqx_add_note"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx notenote — Jot down a quick note — an idea, a URL to read later — outside the backlog (D241).
note.list
{count, total, next_offset, notes} — the D70 page, newest first, bodies whole unless max_body_bytes cuts one (D148's body_truncated and body_bytes). status is open (default), archived or all; links keeps only bodies holding an http:// or https:// link; query is a case-insensitive substring, not a ranked search (D241).
Objects: Annotation · Note.
Parameters
statusstringoptional- Default open.
linksbooleanoptional- Only notes holding an
http://orhttps://link. querystringoptional- A substring of the body, any case.
projectstringoptional- Only notes filed under this project.
limitintegeroptional- Default 20.
offsetintegeroptional- Pass the previous response's
next_offset. max_body_bytesintegeroptional- Default 640.
Response
resultcountintegeralways- How many notes came back.
totalintegeralways- How many notes match before the page.
next_offsetintegernullable- The
offsetthat fetches the next page; null once nothing is left. notesarrayalways- The notes, newest first, bodies whole.
result.notes[]idstringalways- The note's uuid.
short_idintegeralways- The note's own small number, from a counter tasks do not share — what
note.gettakes beside the uuid. bodystringalways- The whole body, stored and returned verbatim.
projectstringnullable- The project it is filed under, or null.
statusstringalwaysopenorarchived.promoted_tostringnullable- Where
note.promotefiled it —task:<uuid>ormemory:<uuid>— or null while it has not been promoted. Terminal: a promoted note stays archived, and a merge keeps the promotion from either side. createdstringalways- When it was captured.
modifiedstringalways- When it last changed.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. has_linksbooleanalways- Whether the body holds an
http://orhttps://link — derived on every read, the same testnote.list {links: true}filters on. promoted_task_short_idintegernullable- The short id the task
promoted_tonames holds in this store, read at the call so a renumbered task reads right; null when the note was not promoted to a task or the task is gone. body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
echo '{"tasqx":"1","id":"n2","method":"note.list","params":{"links":true}}' | tasqx api{
"id": "n2",
"ok": true,
"result": {
"count": 1,
"next_offset": null,
"notes": [
{
"_rev": 0,
"body": "read https://www.rfc-editor.org/rfc/rfc9457 before the error-format change",
"created": "2026-09-14T11:20:00Z",
"has_links": true,
"id": "019f5a10-0000-7000-8000-00000000b001",
"modified": "2026-09-14T11:20:00Z",
"project": "api",
"promoted_task_short_id": null,
"promoted_to": null,
"short_id": 1,
"status": "open"
}
],
"total": 1
},
"tasqx": "1"
}tasqx_list_notes — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"links":true},"name":"tasqx_list_notes"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx notesnotes — List your notes, newest first (D241).
note.get
One note, by uuid or short id, cut like note.list's rows when max_body_bytes is given (D241).
Objects: Annotation · Note.
Parameters
idstringrequired- The note's uuid or short id.
max_body_bytesintegeroptional- Default 16384.
Response
resultidstringalways- The note's uuid.
short_idintegeralways- The note's own small number, from a counter tasks do not share — what
note.gettakes beside the uuid. bodystringalways- The whole body, stored and returned verbatim.
projectstringnullable- The project it is filed under, or null.
statusstringalwaysopenorarchived.promoted_tostringnullable- Where
note.promotefiled it —task:<uuid>ormemory:<uuid>— or null while it has not been promoted. Terminal: a promoted note stays archived, and a merge keeps the promotion from either side. createdstringalways- When it was captured.
modifiedstringalways- When it last changed.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. has_linksbooleanalways- Whether the body holds an
http://orhttps://link — derived on every read, the same testnote.list {links: true}filters on. promoted_task_short_idintegernullable- The short id the task
promoted_tonames holds in this store, read at the call so a renumbered task reads right; null when the note was not promoted to a task or the task is gone. body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
echo '{"tasqx":"1","id":"n3","method":"note.get","params":{"id":2}}' | tasqx api{
"id": "n3",
"ok": true,
"result": {
"_rev": 0,
"body": "idea: a dark-mode toggle in the docs header",
"created": "2026-09-15T16:05:00Z",
"has_links": false,
"id": "019f5a10-0000-7000-8000-00000000b002",
"modified": "2026-09-15T16:05:00Z",
"project": null,
"promoted_task_short_id": null,
"promoted_to": null,
"short_id": 2,
"status": "open"
},
"tasqx": "1"
}tasqx_get_note — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":2},"name":"tasqx_get_note"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx notenote — Jot down a quick note — an idea, a URL to read later — outside the backlog (D241).
note.update
The note after the change. status: archived archives it and open brings it back; expected_rev mismatched is a conflict naming both revs (D241).
Objects: Note.
Parameters
idstringrequired- The note's uuid or short id.
bodystringoptional- New body. Omit to leave unchanged.
projectstringoptional- New project. Omit to leave unchanged.
statusstringoptional- `archived` archives the note, `open` brings it back (D241).
expected_revintegeroptional- Optimistic-concurrency guard. Supplied by the server from the note's current
_revwhen omitted.
Response
resultidstringalways- The note's uuid.
short_idintegeralways- The note's own small number, from a counter tasks do not share — what
note.gettakes beside the uuid. bodystringalways- The whole body, stored and returned verbatim.
projectstringnullable- The project it is filed under, or null.
statusstringalwaysopenorarchived.promoted_tostringnullable- Where
note.promotefiled it —task:<uuid>ormemory:<uuid>— or null while it has not been promoted. Terminal: a promoted note stays archived, and a merge keeps the promotion from either side. createdstringalways- When it was captured.
modifiedstringalways- When it last changed.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. has_linksbooleanalways- Whether the body holds an
http://orhttps://link — derived on every read, the same testnote.list {links: true}filters on. promoted_task_short_idintegernullable- The short id the task
promoted_tonames holds in this store, read at the call so a renumbered task reads right; null when the note was not promoted to a task or the task is gone.
echo '{"tasqx":"1","id":"n4","method":"note.update","params":{"id":2,"status":"archived","expected_rev":0}}' | tasqx api{
"id": "n4",
"ok": true,
"result": {
"_rev": 1,
"body": "idea: a dark-mode toggle in the docs header",
"created": "2026-09-15T16:05:00Z",
"has_links": false,
"id": "019f5a10-0000-7000-8000-00000000b002",
"modified": "2026-09-16T09:00:00Z",
"project": null,
"promoted_task_short_id": null,
"promoted_to": null,
"short_id": 2,
"status": "archived"
},
"tasqx": "1"
}tasqx_update_note — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"expected_rev":0,"id":2,"status":"archived"},"name":"tasqx_update_note"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx notenote — Jot down a quick note — an idea, a URL to read later — outside the backlog (D241).
note.remove
{id, short_id, removed} — deletes the note for good; event.revert refuses it (D241).
Objects: Note.
Parameters
idstringrequired- The note's uuid or short id.
Response
resultidstringalways- The note that is gone.
short_idintegeralways- Its short id, which is never reused.
removedbooleanalways- Always true on success. Removal is permanent and outside
undo.
echo '{"tasqx":"1","id":"n5","method":"note.remove","params":{"id":2}}' | tasqx api{
"id": "n5",
"ok": true,
"result": {
"id": "019f5a10-0000-7000-8000-00000000b002",
"removed": true,
"short_id": 2
},
"tasqx": "1"
}tasqx_remove_note — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":2},"name":"tasqx_remove_note"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx notenote — Jot down a quick note — an idea, a URL to read later — outside the backlog (D241).
note.promote
{note, task|doc} — in one transaction, to: task files a task through task.add's own write (the note's project, else the default), with the full body as its first annotation when the title does not say all of it; to: memory files a doc through memory.add's. The title is title or the body's first line cut at 80 characters. The note is archived with promoted_to naming what it became by uuid; promotion is terminal, so a promoted note is refused and cannot be reopened (D241).
Objects: Note.
Parameters
idstringrequired- The note's uuid or short id.
tostringrequired- `task` or `memory`: what the note becomes (D241).
titlestringoptional- Optional; defaults to the body's first line.
expected_revintegeroptional- Optional guard on the note's
_rev.
Response
resultnoteobjectalways- The note after the promote: archived, with
promoted_tonaming what it became. taskobjectoptional- The task it became, on
to: task; absent onto: memory. docobjectoptional- The memory doc it became, on
to: memory; absent onto: task.
result.noteidstringalways- The note's uuid.
short_idintegeralways- The note's own small number, from a counter tasks do not share — what
note.gettakes beside the uuid. bodystringalways- The whole body, stored and returned verbatim.
projectstringnullable- The project it is filed under, or null.
statusstringalwaysopenorarchived.promoted_tostringnullable- Where
note.promotefiled it —task:<uuid>ormemory:<uuid>— or null while it has not been promoted. Terminal: a promoted note stays archived, and a merge keeps the promotion from either side. createdstringalways- When it was captured.
modifiedstringalways- When it last changed.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. has_linksbooleanalways- Whether the body holds an
http://orhttps://link — derived on every read, the same testnote.list {links: true}filters on. promoted_task_short_idintegernullable- The short id the task
promoted_tonames holds in this store, read at the call so a renumbered task reads right; null when the note was not promoted to a task or the task is gone.
result.taskidstringalways- The task's uuid.
short_idintegeralways- The task's short id — the small number every
refaccepts. titlestringalways- Its title: the one given, or the body's first line cut at 80 characters.
statusstringalways- Its status, as
task.addanswers it. projectstringnullable- The project it landed in — the note's, else the store's default — or null.
result.docidstringalways- The doc's uuid — what
memory.gettakes. titlestringalways- Its title: the one given, or the body's first line cut at 80 characters.
projectstringnullable- The note's project, which scopes the doc, or null.
standingbooleanalways- Always false: a promoted note is ordinary memory (D156).
createdstringalways- When it was stored.
echo '{"tasqx":"1","id":"n6","method":"note.promote","params":{"id":2,"to":"task"}}' | tasqx api{
"id": "n6",
"ok": true,
"result": {
"note": {
"_rev": 1,
"body": "idea: a dark-mode toggle in the docs header",
"created": "2026-09-15T16:05:00Z",
"has_links": false,
"id": "019f5a10-0000-7000-8000-00000000b002",
"modified": "2026-09-16T09:00:00Z",
"project": null,
"promoted_task_short_id": 63,
"promoted_to": "task:019f5a10-2d3e-7f40-9b5c-6d7e8f9a0b1c",
"short_id": 2,
"status": "archived"
},
"task": {
"id": "019f5a10-2d3e-7f40-9b5c-6d7e8f9a0b1c",
"project": "website",
"short_id": 63,
"status": "pending",
"title": "idea: a dark-mode toggle in the docs header"
}
},
"tasqx": "1"
}Illustrative. the answer carries the new task's uuid, a v7 id whose random half differs on every run, so a capture could never be reproduced (D149).
tasqx_promote_note — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":2,"to":"task"},"name":"tasqx_promote_note"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx notenote — Jot down a quick note — an idea, a URL to read later — outside the backlog (D241).
Token methods
token.add
{short_id, measurement}. Records AI token spend; never bumps _rev. total_tokens is one unsplit count, refused beside the split four (D167); a missing required field is refused together with every other one missing. A repeated idempotency_key on the same task returns the measurement it already banked, unchanged.
Objects: Token measurement.
Parameters
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
toolstringrequired- The AI tool that spent them; defaults to the handshake's client.
sourcestringrequired- How the figure was obtained: `self-report`, `log-parse` or `otel`. It is what `confidence` is graded against.
modelstringoptional- The model that did the work. Recorded, never interpreted.
input_tokensintegeroptionaldefault0- Fresh input tokens.
output_tokensintegeroptionaldefault0- Output tokens.
cache_read_tokensintegeroptionaldefault0- Tokens read from cache. Kept in its own bucket and never blended with the others (D48).
cache_creation_tokensintegeroptionaldefault0- Tokens spent writing the cache.
total_tokensintegeroptional- One unsplit count, instead of the four.
confidencestringrequired- How checkable the figure is: `medium` or `low` for a self-report — `high` is refused, because confidence describes verifiability and not preference (D50).
idempotency_keystringoptional- The same key twice on one task banks once.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. measurementobjectalways- The measurement as banked. A repeated
idempotency_keyreturns the one already stored, unchanged.
result.measurementidstringalways- The measurement's uuid — what
token.removetakes. toolstringalways- Which tool spent the tokens (
claude-code,codex, …). sourcestringalways- How the figure was obtained:
self-report,log-parseorotel. modelstringnullable- The model that did the work, or null.
input_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache — never blended with the others (D48).
cache_creation_tokensintegeralways- Tokens spent writing the cache.
confidencestringalways- How checkable the figure is:
high,mediumorlow. A self-report may not claimhigh. createdstringalways- When the measurement was banked.
total_tokensintegeralways- One unsplit count, from a reporter that could not split it — 0 on a split measurement, and never folded into the four (D167).
echo '{"tasqx":"1","id":"tk1","method":"token.add","params":{"ref":"51","tool":"claude-code","source":"self-report","model":"opus","input_tokens":18400,"output_tokens":2600,"cache_read_tokens":91000,"cache_creation_tokens":12000,"confidence":"medium"}}' | tasqx api{
"id": "tk1",
"ok": true,
"result": {
"measurement": {
"cache_creation_tokens": 12000,
"cache_read_tokens": 91000,
"confidence": "medium",
"created": "2026-09-16T09:00:00Z",
"id": "019f7c0a-3d51-7d22-b73c-9353de0a3360",
"input_tokens": 18400,
"model": "opus",
"output_tokens": 2600,
"source": "self-report",
"tool": "claude-code",
"total_tokens": 0
},
"short_id": 51
},
"tasqx": "1"
}Illustrative. the measurement's id is minted fresh, v7, half of it random.
tasqx_add_tokens — write scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"cache_creation_tokens":12000,"cache_read_tokens":91000,"confidence":"medium","input_tokens":18400,"model":"opus","output_tokens":2600,"ref":"51","source":"self-report","tool":"claude-code"},"name":"tasqx_add_tokens"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx tokenstokens — Record token spend after the fact, or repair stored attribution (D50, D167).
token.remove
{short_id, removed}. Deletes one measurement by id — the correction path a wrong self-report otherwise has none of (D50, D67; #210). removed is the measurement object that is now gone.
Objects: Token measurement.
Parameters
measurement_idstringrequired- The measurement to delete, by the id `token.add` handed back. There is no `ref`: a measurement id is globally unique.
Response
resultshort_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. removedobjectalways- The measurement that is now gone — the same object
token.addhands back (#210).
result.removedidstringalways- The measurement's uuid — what
token.removetakes. toolstringalways- Which tool spent the tokens (
claude-code,codex, …). sourcestringalways- How the figure was obtained:
self-report,log-parseorotel. modelstringnullable- The model that did the work, or null.
input_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache — never blended with the others (D48).
cache_creation_tokensintegeralways- Tokens spent writing the cache.
confidencestringalways- How checkable the figure is:
high,mediumorlow. A self-report may not claimhigh. createdstringalways- When the measurement was banked.
total_tokensintegeralways- One unsplit count, from a reporter that could not split it — 0 on a split measurement, and never folded into the four (D167).
echo '{"tasqx":"1","id":"tr1","method":"token.remove","params":{"measurement_id":"019f7c0a-3d51-7d22-b73c-9353de0a3360"}}' | tasqx api{
"id": "tr1",
"ok": true,
"result": {
"removed": {
"cache_creation_tokens": 12000,
"cache_read_tokens": 91000,
"confidence": "medium",
"created": "2026-09-16T09:00:00Z",
"id": "019f7c0a-3d51-7d22-b73c-9353de0a3360",
"input_tokens": 18400,
"model": "opus",
"output_tokens": 2600,
"source": "self-report",
"tool": "claude-code",
"total_tokens": 0
},
"short_id": 51
},
"tasqx": "1"
}Illustrative. it deletes a measurement by id, and the only measurement to delete is one a previous call just minted — so the request itself quotes an id no fixture can pin.
No MCP tool. An agent reaches this method only through a human running tasqx api.
the corrective half of token.add (#210), kept off although its other half is tasqx_add_tokens (D167): an agent that reports a wrong count fixes it by reporting the right one, and a removal an agent could reach unsupervised on the ledger a lead reads for budget decisions is a bigger foot-gun than the gap it closes. A human runs tasqx api token.remove instead.
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
tokens.recompute
{dry_run, tasks, totals}. Re-runs log-parse attribution over stored windows under the refusal rule (D50). dry_run defaults to true — report the per-task delta, write nothing; send false to apply. In-process only: a daemon refuses this method over the socket, so stop any daemon on the store first.
Objects: Token measurement.
Parameters
dry_runbooleanoptionaldefaulttrue- Report the per-task delta and write nothing. Send `false` to apply it — this is the one method that deletes measurement rows.
Response
resultdry_runbooleanalways- Whether this run wrote anything. Defaults to true: report the delta, change nothing.
tasksarrayalways- One row per task whose attribution would change.
totalsobjectalways- The whole-store totals, before and after.
result.tasks[]taskintegeralways- The task's short id.
actionstringalways- What the pass would do to it:
insert,updateordelete. beforeobjectalways- The four buckets as they stand.
afterobjectalways- The four buckets the pass computed.
result.tasks[].beforeinput_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache.
cache_creation_tokensintegeralways- Tokens spent writing the cache.
result.tasks[].afterinput_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache.
cache_creation_tokensintegeralways- Tokens spent writing the cache.
result.totalsbeforeintegeralways- Total attributed tokens before the pass.
afterintegeralways- Total attributed tokens after it.
echo '{"tasqx":"1","id":"rc1","method":"tokens.recompute"}' | tasqx api{
"id": "rc1",
"ok": true,
"result": {
"dry_run": true,
"tasks": [],
"totals": {
"after": 0,
"before": 0
}
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
a maintenance pass over the whole store's attribution. It is an operator action with a runtime proportional to history, not a step in anybody's task.
tasqx tokenstokens — Record token spend after the fact, or repair stored attribution (D50, D167).
Report methods
report.summary
{groups, generated, filter, all, since, until}. group_by defaults to project; the result echoes the scope it applied, so a total cannot be read against the wrong period. since/until (D97) window tracked_total and the token buckets by WHEN the time or spend happened — a different axis from filter's completed.after:/completed.before:, which selects tasks by completion date; both are null unless the caller sets them.
Parameters
group_bystringoptional- Grouping axis. Optional; defaults to project.
filterstringoptional- Optional filter DSL to scope the report.
metricsarray of stringoptional- Extra columns per group. Omit and each group carries only
count: every other metric is opt-in here, unliketasqx report. allbooleanoptional- Count cancelled tasks too. By default a report with no status term skips cancelled only — done work always counts (D24).
sincestringoptional- Window
tracked_totaland the token buckets to spend at or after this instant, not the task's lifetime total — a different axis fromfilter'scompleted.*terms (D97). untilstringoptional- The other end of
since: excludes anything at or after this instant. Together they window WHEN the spend happened.
Response
resultgroupsarrayalways- One row per group, on the axis
group_bynamed. generatedstringalways- When the report was computed — the time of the call, not the window.
filterstringalways- The filter this total was taken over, echoed so it cannot be read against the wrong scope (D69).
allbooleanalways- Whether cancelled work was counted (D24).
tokens_excluded_cancelled_tasksintegeralways- How many cancelled tasks carrying token spend the default just excluded.
sincestringnullable- The start of the time window, or null when the caller set none (D97).
untilstringnullable- The end of that window, or null.
store_emptybooleanalways- Whether the store has ever held a task at all — how "nothing yet" is told apart from "nothing matched".
result.groups[]projectstringalways- The group key — named by
group_by, so this column isproject,statusorpriority. countintegeralways- How many tasks fell in this group.
est_totalstringalways- Their estimates summed, as an ISO duration. Only when
metricsnames it — the default iscountalone. overdueintegeralways- How many of them are past due. Only when
metricsnames it.
echo '{"tasqx":"1","id":"r1","method":"report.summary","params":{"group_by":"project"}}' | tasqx api{
"id": "r1",
"ok": true,
"result": {
"all": false,
"filter": "",
"generated": "2026-09-16T09:00:00Z",
"groups": [
{
"count": 10,
"project": "api"
},
{
"count": 2,
"project": "home"
},
{
"count": 11,
"project": "infra"
},
{
"count": 16,
"project": "mobile"
},
{
"count": 18,
"project": "website"
}
],
"since": null,
"store_empty": false,
"tokens_excluded_cancelled_tasks": 0,
"until": null
},
"tasqx": "1"
}tasqx_summary — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"group_by":"project"},"name":"tasqx_summary"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
report.outcomes
{groups, group_by, metrics, generated, filter, since, until, store_empty}. What the work DID, as against what it cost (D137): rework (completions that were reopened), calibration (median tracked-over-estimate), cost (the four token buckets, never blended), silent (completions carrying no annotation), abandonment (started, then cancelled) forced (completions that overrode open blockers, D150) and unbriefed (completions of a task that no task.brief or memory-carrying task.start was ever read for, D227). Every rate comes back beside the n it was computed over. Scope is tasks that CLOSED, by the instant they closed — so a completion that was reopened still counts, which is the whole point. Omitting metrics emits all of them; there is no all, because here a cancellation is a measured outcome rather than noise D24 excludes.
Parameters
group_bystringoptional- Grouping axis. Optional; defaults to project.
filterstringoptional- Optional filter DSL to scope the report —
tasqx_list_tasks' grammar. metricsarray of stringoptional- Which metrics to emit. Omit for ALL of them; naming none does not mean a bare count here.
sincestringoptional- Only count tasks that closed at or after this instant.
since/untilwindow WHEN THE TASK CLOSED, not when spend happened. untilstringoptional- The other end of
since: excludes tasks that closed at or after this instant.
Response
resultgroupsarrayalways- One row per group, each metric beside the
nit was computed over. group_bystringalways- The axis the rows are grouped on, echoed.
metricsarrayalways- The metrics computed. Omitting
metricson the request emits all of them. generatedstringalways- When the report was computed.
filterstringalways- The filter applied, echoed.
sincestringnullable- The start of the window over when tasks CLOSED, or null.
untilstringnullable- The end of that window, or null.
store_emptybooleanalways- Whether the store has ever held a task at all — how "nothing yet" is told apart from "nothing matched".
result.groups[]projectstringalways- The group key, named by
group_by. completionsintegeralways- How many tasks completed in the window.
closedintegeralways- How many closed — completions plus cancellations.
reworkobjectalways- Completions that were later reopened.
calibrationobjectalways- Tracked time over estimate, as a median.
costobjectalways- The four token buckets, never blended.
silentobjectalways- Completions carrying no annotation — work that left no record of what it decided.
abandonmentobjectalways- Work that was started and then cancelled, with the time inside it.
overrunobjectalways- Completions that went past their
budget_tokens(D139). unprovenobjectalways- Completions whose acceptance criteria were not all passed — open or failed (D138, D205).
forcedobjectalways- Completions that overrode still-open blockers (D150).
unbriefedobjectalways- Completions of a task that was never the subject of
task.briefand whose start carried no memory (D227).
result.groups[].reworkcountintegeralways- How many occurrences.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
ratenumbernullablecountovern; null rather than zero when there was nothing to divide by.refsarrayalways- The short ids behind the count, so a number can be opened.
result.groups[].calibrationmedian_rationumbernullable- Median tracked-over-estimate. Null when nothing in the group carried both.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
result.groups[].costtokens_inintegeralways- Fresh input tokens across the group.
tokens_outintegeralways- Output tokens.
tokens_cache_readintegeralways- Cache reads.
tokens_cache_creationintegeralways- Cache writes.
tokens_unsplitintegeralways- Unsplit
total_tokenscounts, summed apart from the four (D167). nintegeralways- The denominator this rate was computed over. A rate never travels without it.
confidencestringoptional- How checkable the figures are. Absent — not null — when nothing was measured, because there is nothing to grade.
result.groups[].silentcountintegeralways- How many occurrences.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
ratenumbernullablecountovern; null rather than zero when there was nothing to divide by.refsarrayalways- The short ids behind the count, so a number can be opened.
result.groups[].abandonmentcountintegeralways- How many started tasks were then cancelled.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
ratenumbernullablecountovern, or null.tracked_totalstringalways- The time spent inside the abandoned work — how much, not only how many.
refsarrayalways- The short ids behind the count.
result.groups[].overruncountintegeralways- How many occurrences.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
ratenumbernullablecountovern; null rather than zero when there was nothing to divide by.refsarrayalways- The short ids behind the count, so a number can be opened.
result.groups[].unprovencountintegeralways- How many occurrences.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
ratenumbernullablecountovern; null rather than zero when there was nothing to divide by.refsarrayalways- The short ids behind the count, so a number can be opened.
failedintegeralways- How many of
counthad a check marked failed rather than left open (D205).
result.groups[].forcedcountintegeralways- How many occurrences.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
ratenumbernullablecountovern; null rather than zero when there was nothing to divide by.refsarrayalways- The short ids behind the count, so a number can be opened.
result.groups[].unbriefedcountintegeralways- How many occurrences.
nintegeralways- The denominator this rate was computed over. A rate never travels without it.
ratenumbernullablecountovern; null rather than zero when there was nothing to divide by.refsarrayalways- The short ids behind the count, so a number can be opened.
echo '{"tasqx":"1","id":"ro1","method":"report.outcomes","params":{"group_by":"project","metrics":["rework","calibration"]}}' | tasqx api{
"id": "ro1",
"ok": true,
"result": {
"filter": "",
"generated": "2026-09-16T09:00:00Z",
"group_by": "project",
"groups": [
{
"calibration": {
"median_ratio": 1.1666666666666667,
"n": 3
},
"closed": 7,
"completions": 6,
"project": "api",
"rework": {
"count": 0,
"n": 6,
"rate": 0.0,
"refs": []
}
},
{
"calibration": {
"median_ratio": 1.0,
"n": 4
},
"closed": 9,
"completions": 8,
"project": "infra",
"rework": {
"count": 0,
"n": 8,
"rate": 0.0,
"refs": []
}
},
{
"calibration": {
"median_ratio": 1.3541666666666667,
"n": 2
},
"closed": 15,
"completions": 13,
"project": "mobile",
"rework": {
"count": 2,
"n": 13,
"rate": 0.15384615384615383,
"refs": [
17,
19
]
}
},
{
"calibration": {
"median_ratio": 1.5833333333333333,
"n": 9
},
"closed": 15,
"completions": 13,
"project": "website",
"rework": {
"count": 0,
"n": 13,
"rate": 0.0,
"refs": []
}
}
],
"metrics": [
"rework",
"calibration"
],
"since": null,
"store_empty": false,
"until": null
},
"tasqx": "1"
}tasqx_outcomes — read scope.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"group_by":"project","metrics":["rework","calibration"]},"name":"tasqx_outcomes"}}The tool hands back this method's result in a text block — or, where its own description says so, less of it by default: a rendered view, a compact row, no rank. What it sends and how to ask for the rest is documented with the tool.
tasqx reportreport — Summary counts, or outcomes, optionally grouped, as text or HTML.
Store methods
store.export
{tasks, projects, dropped_projects, docs, dropped_docs, notes, dropped_notes, links, dropped_links, events, dropped_events, default_project, dropped_dependencies}. Notes are scoped like docs (D241). events is the whole audit log except the bookkeeping rows a `store.import` itself writes. An unfiltered export carries every project/doc/link/event (D12) but a removed doc's history before its removal (D197), counted in dropped_events; any other filter scopes those four to what the exported tasks need and reports what it dropped (D171, D181) — include_unscoped widens that scope back to docs with no project. A link travels only when BOTH its ends are nodes the document carries. out_path (absolute, never an existing file) writes the document there instead and answers {out_path, bytes, tasks} with the dropped_* counts, so a store past a daemon's 1 MiB frame can leave it (D201), plus what the document leaves out that would change it if counted inside: skipped_events (that bookkeeping) and removed_annotations (removed notes' tombstones) (D207).
Objects: Task · Project · Annotation · Check · Dependency · Memory document · Note · Link · Event · Git ref · Token measurement.
Parameters
filterstringoptional- A filter-DSL query narrowing which tasks are exported. Omitted, the whole store comes back — and so do every project, every doc and the whole event log (D12). Any other filter also scopes `projects`/`docs`/`events` to what the exported tasks need (D171).
include_unscopedbooleanoptionaldefaultfalse- Widen a filtered export's `docs`/`projects` to ones that carry no project at all. Refused with no `filter` — an unfiltered export already carries everything, so there is nothing to widen from (D171).
out_pathstringoptional- An absolute path to write the document to instead of returning it — for a store too big for a daemon's 1 MiB frame (D201). Never overwrites: a file already there is refused. The result is then `{out_path, bytes, tasks}` plus the `dropped_*` counts, `default_project`, and two counts the document cannot carry without changing on every import (D207): `skipped_events`, the bookkeeping a past import wrote about itself, and `removed_annotations`, the removed notes' tombstones left behind.
Response
resulttasksarrayalways- Every task in scope, whole: fields, tags, notes, criteria, edges and measurements.
dropped_dependenciesintegeralways- How many dependency edges pointed outside the filtered slice and were dropped.
projectsarrayalways- Every project this document needs: all of them on an unfiltered export, or the ones named by the filter or by an exported task on a filtered one.
dropped_projectsintegeralways- How many project rows a filtered export left out because nothing exported named them.
docsarrayalways- Every memory doc this document needs, scoped like
projects; an unscoped doc ships only withinclude_unscoped. dropped_docsintegeralways- How many memory docs a filtered export left out, scoped or unscoped.
notesarrayalways- Every quick-capture note (D241), scoped like
docs: a note's project in scope, an unscoped one only withinclude_unscoped. The stored row, without the derivedhas_links. dropped_notesintegeralways- How many notes a filtered export left out, scoped or unscoped.
linksarrayalways- Every explicit link (D160) whose BOTH ends are nodes this document carries — the same row
link.listpages. dropped_linksintegeralways- How many links a filtered export left out because one of their ends is a node this document does not carry.
eventsarrayalways- The whole audit log for what this document carries, minus the bookkeeping rows an import itself writes, plus the
memory.removeorlink.removeof a doc or link that is gone from this store and within the document's scope, so a merge elsewhere can apply it (D197). dropped_eventsintegeralways- How many events the export left out because they belong to a task, doc, project or link this document does not carry — on an unfiltered export, the history of a removed doc other than its removal, since its
memory.addnames the title the removal took away (D197). default_projectstringnullable- The store's default project, or null — also null when a filtered export did not carry it among
projects.
result.tasks[]idstringalways- The task's uuid, stable across exports and machines.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalwayspending,backlog,active,doneorcancelled— or, on a stored row whose status text this build does not recognize, that text verbatim, withstatus_unrecognized: truebeside it.prioritystringnullableH,M,L, or null for none.projectstringnullable- The project the task belongs to, or null.
duestringnullable- When it is due, as an instant, or null.
scheduledstringnullable- When work is planned to start, or null. While it is in the future a
pendingtask reads asbacklog, out of the working set, as withwait;active,doneandcancelledkeep their status. waitstringnullable- Holds the task out of the working set until this instant, or null. While it is in the future a
pendingtask reads asbacklog;active,doneandcancelledkeep their status. estimatestringnullable- The estimate as an ISO 8601 duration (
PT4H), or null. recurrencestringnullable- The recurrence rule (
every 3 days), or null. remindstringnullable- The reminder spec — an offset from
dueor an absolute instant — or null. urgencynumberalways- The urgency score (D1): priority, due proximity and age, summed.
tagsarrayalways- Every tag on the task, as plain strings.
createdstringalways- When the task was captured.
modifiedstringalways- When it last changed.
completedstringnullable- When
task.donecompleted it, or null.task.reopenclears it, andtask.canceldoes not set it. budget_tokensintegernullable- The size gauge over FRESH tokens (D139), or null for no threshold. It stops nothing.
_revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional. tracked_secondsintegeroptional- Tracked time in raw seconds (D42). Omitted when it would be zero.
tracked_adjustment_secondsintegeroptional- The signed net of the
task.adjust_trackedcorrections insidetracked_seconds(D166). Omitted when it would be zero. active_sincestringoptional- The open interval's anchor, omitted when no timer is running.
delivered_annotation_idstringoptional- The note
task.donepinned as the delivery note (D165). Present only on a task a completion pinned one on. tokensarrayoptional- Every token measurement against this task. Omitted entirely for a task that has none.
git_refsarrayoptional- The branch, PR and merge commit recorded on this task (D239). Omitted entirely for a task that has none.
depends_onarrayalways- The short ids this task waits on.
annotationsarrayalways- Every note on the task — an export carries the whole history, unpaged.
status_unrecognizedbooleanoptional- Present, and true, only on a row whose
statusno writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one.store.importnow refuses such a status. checksarrayalways- The acceptance criteria on the task, in position order (D138).
spawned_fromstringoptional- The uuid of the task whose completion spawned this one (D170). Omitted on any other task.
claimed_bystringoptional- The claim's holder (D236), expired or not. Omitted on an unclaimed task.
claim_expiresstringoptional- When that claim's lease runs out (D236). Omitted with
claimed_by.
result.tasks[].tokens[]idstringalways- The measurement's uuid — what
token.removetakes. toolstringalways- Which tool spent the tokens (
claude-code,codex, …). sourcestringalways- How the figure was obtained:
self-report,log-parseorotel. modelstringnullable- The model that did the work, or null.
input_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache — never blended with the others (D48).
cache_creation_tokensintegeralways- Tokens spent writing the cache.
confidencestringalways- How checkable the figure is:
high,mediumorlow. A self-report may not claimhigh. createdstringalways- When the measurement was banked.
total_tokensintegeralways- One unsplit count, from a reporter that could not split it — 0 on a split measurement, and never folded into the four (D167).
result.tasks[].annotations[]idstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
result.tasks[].checks[]idstringalways- The criterion's uuid — what
check.setandcheck.removetake. bodystringalways- The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalwaysopen,passedorfailed. A failure is a normal outcome, not an error.evidencestringnullable- The citation for the state, or null — some criteria are met by something nobody can quote.
positionintegeralways- Where it sits in the list, from zero.
createdstringalways- When the criterion was added.
modifiedstringalways- When its state or evidence last changed.
result.tasks[].git_refs[]idstringalways- The ref row's id.
kindstringalwaysbranch,prorcommit.refstringalways- The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalwaysopen,closedormerged.mergedis final; a PR can move betweenopenandclosed.createdstringalways- When the ref was first recorded.
modifiedstringalways- When
statelast changed. Betweenopenandclosed, the later change wins when two stores merge. notedstringnullable- On a branch row, what
git.eventhas already done for that branch (JSON keyed per PR: the PRs that started the task, the fact its last note stated, the delivery note, the note it completed the task with), so a retry writes a missing note, never a duplicate, and never overrides the user. Null before any.
result.projects[]idstringalways- The project's uuid.
namestringalways- Its name — what a task's
projectnames. descriptionstringnullable- Its description, or null.
archivedbooleanalways- Whether it is archived.
createdstringalways- When it was created.
result.docs[]idstringalways- The doc's uuid.
sourcestringnullable- Where it came from — the path
memory.importkeyed on, or null. titlestringalways- The doc's title.
bodystringalways- The whole body, stored and returned verbatim (D41).
createdstringalways- When it was first stored.
modifiedstringalways- When it last changed.
projectstringnullable- The project it is scoped to, or null.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. standingbooleanalways- Whether it is a standing ruling, meant for every session of its scope (D156). Stated on every doc, so a restore carries it.
origin_pathstringnullable- The absolute path of the file
memory.importread this doc from, or null when no import set one (D180). origin_mtimeintegernullable- That file's modification time in unix seconds when it was read, or null (D180).
origin_sizeintegernullable- That file's size in bytes when it was read, or null (D180).
result.notes[]idstringalways- The note's uuid.
short_idintegeralways- The note's own small number, from a counter tasks do not share — what
note.gettakes beside the uuid. bodystringalways- The whole body, stored and returned verbatim.
projectstringnullable- The project it is filed under, or null.
statusstringalwaysopenorarchived.promoted_tostringnullable- Where
note.promotefiled it —task:<uuid>ormemory:<uuid>— or null while it has not been promoted. Terminal: a promoted note stays archived, and a merge keeps the promotion from either side. createdstringalways- When it was captured.
modifiedstringalways- When it last changed.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional.
result.links[]idstringalways- The link's uuid — what
link.removetakes. fromstringalways- The node the link starts at, as
<type>:<uuid>:task:,memory:,annotation:orproject:. tostringalways- The node the link points at, spelled the same way.
relationstringalways- What the link asserts:
references,supersedes,implements_decision,derived_from,contradictsorduplicates(D238). metadataobjectnullable- The caller's own object, stored verbatim and never interpreted, or null.
created_atstringalways- When the link was written. Named
created_atbecauselink.addanswers with a booleancreatedbeside it.
result.events[]idstringalways- The event's uuid.
entitystringalways- What kind of thing it happened to:
task,project,doc,linkornote. entity_idstringalways- That thing's uuid.
opstringalways- What happened:
add,done,start,stop,tag.remove, … payloadobjectnullable- The op's own vocabulary. Null only for a row whose payload will not parse — a corrupt store.
tsstringalways- When it happened.
actorstringnullable- Who did it, when the caller said, or null.
echo '{"tasqx":"1","id":"x1","method":"store.export","params":{"filter":"+docs"}}' | tasqx api{
"id": "x1",
"ok": true,
"result": {
"default_project": "website",
"docs": [
{
"_rev": 0,
"body": "# Pricing page…",
"created": "2026-09-09T09:15:00Z",
"id": "0787b26d-9e2e-4be5-ab66-ec953102fad3",
"modified": "2026-09-11T09:15:00Z",
"origin_mtime": null,
"origin_path": null,
"origin_size": null,
"project": "website",
"source": "notes/pricing.md",
"standing": false,
"title": "pricing-page-decisions"
}
],
"dropped_dependencies": 0,
"dropped_docs": 1,
"dropped_events": 2,
"dropped_links": 0,
"dropped_notes": 1,
"dropped_projects": 0,
"events": [
{
"actor": "user",
"entity": "task",
"entity_id": "46773aad-c4aa-435a-abe1-fcde8ce09658",
"id": "37e2265e-0745-46cf-ab75-44127cc95bc2",
"op": "add",
"payload": {
"title": "Write the migration guide for SDK 3.0"
},
"ts": "2026-08-26T10:00:00Z"
}
],
"links": [
{
"created_at": "2026-09-11T09:20:00Z",
"from": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"id": "5f0a7c21-3d64-4a0e-9b8f-2c1d7e4a6b90",
"metadata": null,
"relation": "references",
"to": "memory:0787b26d-9e2e-4be5-ab66-ec953102fad3"
}
],
"notes": [
{
"_rev": 0,
"body": "read https://www.rfc-editor.org/rfc/rfc9457 before the error-format change",
"created": "2026-09-14T11:20:00Z",
"id": "019f5a10-0000-7000-8000-00000000b001",
"modified": "2026-09-14T11:20:00Z",
"project": "api",
"promoted_to": null,
"short_id": 1,
"status": "open"
}
],
"projects": [
{
"archived": false,
"created": "2026-06-18T09:00:00Z",
"description": "Public REST API and its SDKs",
"id": "309d6b79-965e-4a32-9ae4-45508201e2bd",
"name": "api"
}
],
"tasks": [
{
"…": "one whole task per row"
}
]
},
"tasqx": "1"
}Illustrative. the real answer is the entire store — every task, project, doc, link and audit row, in one payload. What matters here is its shape, so this example is a single row of each with the bodies elided.
No MCP tool. An agent reaches this method only through a human running tasqx api.
an agent cannot snapshot what it just wrote, and that is a real gap — but the payload is the whole store, which is exactly the size problem D63 and D66 spent two rounds on. It needs a filter and a budget before it is a tool rather than a way to blow a client's limit in one call.
store.import
{imported, projects_imported, projects_created, docs_imported, docs_declared, events_imported, events_logged, links_imported, default_project, renumbered, docs_merged, notes_imported, notes_renumbered, dry_run, merged, project_description_conflicts, deduplicated}. A task already in the store at a higher _rev than the payload's refuses the whole import (conflict) rather than silently discarding the annotations, tags and edges added since. A task whose short_id a DIFFERENT task here already holds keeps its id and takes the next free number, listed in renumbered as {id, from, to} (D177). A memory doc whose source a DIFFERENT doc here already holds merges onto that doc — the later modified wins the text — listed in docs_merged as {source, kept_id, dropped_id, took} (D183). Links are restored last, both ends resolved against what this store now holds; an end it does not have refuses the import by name (D181). dry_run (default false) runs the entire import — every renumbering, merge and refusal — and rolls it back instead of committing, so the same answer previews what a real import would do (D184). merge (default false) unions a KNOWN task's annotations, checks, tags and dependency edges with what this store already holds instead of replacing them, skips its _rev guard (D185), takes each field from the side whose event last wrote it and counts tracked time from the union of both event logs — listed in merged as {id, took, from_payload, from_store, tracked_delta_seconds} (D189). A merge also applies every removal either event log holds — of a tag, check, annotation, dependency edge, link or memory doc — that is later than the row's last write, so a row removed on one machine stays removed on both; a removed annotation takes D113's scrub (D197). A project this store already knows keeps its own non-empty description over the payload's — a project carries no modified stamp, so there is no later-wins call to make — listed in project_description_conflicts as {name, dropped} (D190); archived is still taken from the payload unconditionally. Two copies of one recurrence occurrence (same spawned_from, due and scheduled, anywhere in the store) fold into the lowest id, listed in deduplicated (D191). path (absolute) reads the document from a file instead of tasks and the rest, for one past a daemon's 1 MiB frame; merge and dry_run still come from the params (D201).
Parameters
tasksarray of objectrequired- The task documents to import, in the shape `store.export` emits. Required unless `path` is given, so a misspelled key at the top level is still refused by absence.
projectsarray of objectoptional- The project records. A task naming a project this list does not carry mints one, reported in `projects_created`.
default_projectstringoptional- The default project to set after the import.
docsarray of objectoptional- The knowledge docs to import.
eventsarray of objectoptional- The audit rows to import, so history survives a round trip.
linksarray of objectoptional- The explicit links to restore, in the shape `store.export` emits. Written after everything they point at; an end neither the document nor this store holds refuses the import by name (D181).
notesarray of objectoptional- The document's quick-capture notes (D241), as `store.export` writes them. Optional; a document without the key imports no notes.
dry_runbooleanoptionaldefaultfalse- Run the whole import — every renumbering, merge and refusal — and roll it back instead of committing, so the result previews what a real import would do without writing anything (D184).
mergebooleanoptionaldefaultfalse- Merge a task this store ALREADY holds instead of replacing it (D185): its annotations, checks, token measurements, tags and dependency edges are unioned with what is here, its `_rev` guard is skipped, each scalar field group is taken from the side whose latest event touched it (D189; the later `modified` decides a group neither log touched, or a copy that carries no log), and its tracked total is counted from the union of both event logs, plus the larger of the two sides' totals the logs do not explain, so both directions of a merge agree. Reported per task in `merged`. Default false keeps the wholesale replace a restore wants (D138).
pathstringoptional- An absolute path to read the whole document from instead of sending it inline — for a document too big for a daemon's 1 MiB frame (D201). Refused beside `tasks`; `merge` and `dry_run` still come from these params, never from the file.
Response
resultimportedintegeralways- How many tasks the document brought in.
projects_importedintegeralways- How many projects it carried.
projects_createdarrayalways- The NAMES of projects minted because a task named one the document did not carry — a list, so nothing appears out of nowhere unseen.
docs_importedintegeralways- How many knowledge docs came in.
docs_declaredbooleanalways- Whether the document had a
docssection at all — an empty one told apart from a missing one (#179). events_importedintegeralways- How many audit rows came in.
events_loggedintegeralways- How many audit rows the import wrote about itself — an
importper task, project and note, amemory.addper doc — on top ofevents_imported; an export never carries them (#1116). links_importedintegeralways- How many explicit links came in (D181) — an edge this store already held under a different id counts, because that is the same edge stated twice. Zero for a document with no
linkssection. default_projectstringnullable- The default project after the import, or null.
renumberedarrayalways- Every task whose
short_ida DIFFERENT task in this store already held, as{id, from, to}— it kept its id and took the next free number (D177). Empty when nothing moved. notes_renumberedarrayalways- Every note whose
short_ida DIFFERENT note in this store already held, as{id, from, to}— it kept its id and took a number above the whole payload (D177's rule, D241). Empty when nothing moved. notes_importedintegeralways- How many notes the document's
notessection carried in (D241): a new note keeps its short id unless one here holds it, a known one is replaced, or onmergetakes the latermodified. Zero for a document with none. docs_mergedarrayalways- Every memory doc that landed on a doc this store already held under the same
source, as{source, kept_id, dropped_id, took}— the stored id is kept, the payload's is dropped, andtookispayloadwhen the payload'smodifiedwas later (its title, body, project and standing were written) orstorewhen it was not (nothing was written).dropped_idis null when the payload doc carried noidof its own, rather than a minted one a dry run would then roll back. D183. Empty when nothing merged. dry_runbooleanalways- Echoes the
dry_runparam (D184). True means every row above was written and then rolled back — the answer describes what WOULD have happened, not what is now on disk. mergedarrayalways- Every task this store ALREADY held that
mergeunioned rather than replaced, as{id, took, from_payload, from_store, tracked_delta_seconds}. Each scalar field group follows the side whose latest event touched it (D189);from_payloadandfrom_storename the fields that differed and the side each was taken from, andtookispayloadorstorewhen one side supplied all of them,mixedwhen both did, and D185's later-modifiedside when none differed.tracked_delta_secondsis how far the tracked total moved: the total is counted from the union of both event logs, plus the larger of the two sides' totals the logs do not explain. Its annotations, checks, tokens, tags and edges are unioned either way, less every row whose removal, in either event log, is later than its last write (D197). D185. Empty whenmergewas not asked for, or when the document carried nothing this store had seen. project_description_conflictsarrayalways- Every project this store already held whose OWN non-empty description was kept over the payload's, as
{name, dropped}— a project carries nomodifiedstamp, so there is no later-wins call to make and the destination's text always wins (D190).archivedis still taken from the payload unconditionally. Empty when nothing was dropped. deduplicatedarrayalways- Every recurrence occurrence folded into an older copy of itself, as
{kept, dropped, spawned_from, kept_id, dropped_id, dropped_status, status, dropped_dependencies}(short ids, then uuids). Every group in the store sharingspawned_from,dueandscheduledkeeps the lowest id and takes the other's notes, checks, tags, edges, links, tokens and events.statusis the survivor's afterwards: the copy with the latest status event wins it (D189's rule), and tracked time is counted from both copies' event logs, each event once.dropped_dependenciesnames the survivor's edges dropped because the fold would have closed a cycle. D191. Empty when nothing was folded.
echo '{"tasqx":"1","id":"si1","method":"store.import","params":{"tasks":[{"id":"9a1f3c52-0b44-4a2e-9d6f-7c5b1e08a4d1","short_id":900,"title":"Restore the incident timeline","status":"pending","priority":"M","project":"infra","created":"2026-09-10T08:00:00Z","modified":"2026-09-10T08:00:00Z","_rev":1,"tags":["ops"],"depends_on":[],"annotations":[]}]}}' | tasqx api{
"id": "si1",
"ok": true,
"result": {
"deduplicated": [],
"default_project": "website",
"docs_declared": false,
"docs_imported": 0,
"docs_merged": [],
"dry_run": false,
"events_imported": 0,
"events_logged": 1,
"imported": 1,
"links_imported": 0,
"merged": [],
"notes_imported": 0,
"notes_renumbered": [],
"project_description_conflicts": [],
"projects_created": [],
"projects_imported": 0,
"renumbered": []
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
it overwrites, in bulk, from a document nobody has reviewed. The confirmation model (§7) defers to the host's gate, and a host gate on a call whose diff nobody can see is not a safeguard.
Event methods
event.list
{count, events} — the append-only log. from takes the usual date grammar and is a lower bound, not an exact filter: it promises no events older than roughly that instant.
Objects: Event.
Parameters
limitintegeroptionaldefault50- How many events to return, newest first.
refinteger or stringoptional- Only events about this task.
entitystringoptional- Only events about this kind of thing: `task`, `project`, `doc`, `link` or `note`. An unknown name is refused, not read as a filter matching nothing.
fromstringoptional- A lower bound in the usual date grammar (`yesterday`, `2026-09-01`). It promises no events older than roughly that instant, not an exact cut.
Response
resultcountintegeralways- How many events came back.
eventsarrayalways- The events, newest first for everything this engine wrote —
idis a UUIDv7, time-ordered — but a row brought in bystore.importkeeps its document's id and can sort anywhere.
result.events[]idstringalways- The event's uuid.
entitystringalways- What kind of thing it happened to:
task,project,doc,linkornote. entity_idstringalways- That thing's uuid.
opstringalways- What happened:
add,done,start,stop,tag.remove, … payloadobjectnullable- The op's own vocabulary. Null only for a row whose payload will not parse — a corrupt store.
tsstringalways- When it happened.
actorstringnullable- Who did it, when the caller said, or null.
echo '{"tasqx":"1","id":"ev1","method":"event.list","params":{"entity":"project","limit":2}}' | tasqx api{
"id": "ev1",
"ok": true,
"result": {
"count": 2,
"events": [
{
"actor": "user",
"entity": "project",
"entity_id": "73f778aa-f6fa-4db8-a56a-bd72fb710734",
"id": "a66b0d38-9d95-447e-bd29-9753a7677796",
"op": "create",
"payload": {
"default": false,
"description": null,
"name": "home"
},
"ts": "2026-06-18T00:00:00Z"
},
{
"actor": "user",
"entity": "project",
"entity_id": "e3eff9c0-cf44-4d3f-89e7-d15f17362f25",
"id": "986e86cb-0ab8-4b67-a26b-7f62b1852f27",
"op": "create",
"payload": {
"default": false,
"description": "CI, deploys and on-call",
"name": "infra"
},
"ts": "2026-06-18T00:00:00Z"
}
]
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
the audit log is unbounded and has no paging, so exposing it would repeat the task.get mistake D63 fixed. It needs the same limit/offset treatment before it can be a tool; no client has asked for it yet.
event.revert
{reverted, short_id, title, restored} — reverted carries the event id, its op and its timestamp. Undoes the newest event by APPENDING a compensating one, so the reversed event stays in the log. Seven ops are undoable (stop, tag.remove, dependency.remove, annotation.add, annotation.update, annotation.move, adjust_tracked); every other one is conflict naming itself and what does take it back. The newest event, not the last call you made: a call that changed nothing writes no event, so undo reaches past it — which is why the answer names what it undid instead of saying ok. With ref the newest event must be that task's, and with expected_rev the task must still be at that rev; otherwise it is conflict and nothing is undone, so a client undoing its own last write cannot take back a later one.
Objects: Event.
Parameters
refinteger or stringoptional- Undo only if the newest event is this task's; otherwise `conflict`, and nothing is undone (#638).
expected_revintegeroptional- With `ref`: undo only if the task is still at this rev, so undoing your own last write cannot take back a later one. Needs `ref`.
Response
resultrevertedobjectalways- The event that was undone.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The title of the task the undo touched.
restoredobjectalways- What the inverse put back — per-op:
doneandcancelanswer{status, tracked?, interval_started?, checks?, removed_spawn?},modifyanswers{fields}(each restored column and its value),tag.addanswers{removed}. _revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional.
result.revertedeventstringalways- The undone event's id.
opstringalways- Its op — undo names what it undid instead of answering ok.
tsstringalways- When the undone event happened.
echo '{"tasqx":"1","id":"u1","method":"event.revert"}' | tasqx api{
"id": "u1",
"ok": true,
"result": {
"_rev": 5,
"restored": {
"tags": [
"ops"
]
},
"reverted": {
"event": "9c2b4d61-1f77-4a0c-9c1e-2b8e5d3a7f40",
"op": "tag.remove",
"ts": "2026-09-16T09:00:00Z"
},
"short_id": 53,
"title": "Move nightly backups to object storage"
},
"tasqx": "1"
}Illustrative. it undoes whatever is NEWEST in the log, so its answer depends on store state a fixture cannot pin — and on the demo store the newest event is a start, which undo refuses by name rather than reversing.
No MCP tool. An agent reaches this method only through a human running tasqx api.
tasqx has an undo (D54) and an agent cannot reach it, which is the sharpest single omission on this list. It stays off until the tool can show what it is about to undo: event.revert acts on the last matching event, and its blast radius depends on store state the calling agent has not read. A destructive one-shot whose effect the caller cannot see is not a tool, it is a coin flip.
tasqx undoundo — Take back the last thing this store recorded.
Git methods
git.event
{changed, event, short_id, title, status, transition, note, reason}. Records the branch, PR and merge commit on the task the branch task/<id>-<slug> (or task) names, and moves it: opened starts a pending task, merged completes it only when every check passed and nothing blocks it, closed adds a note. Every event re-evaluates the outcome, so a repeated merged completes a task whose checks were fixed since; one whose outcome already holds, or that a recorded merge already settled, answers changed: false with a reason. tasqx never calls GitHub (D239).
Objects: Git ref.
Parameters
eventstringrequired- `opened`, `merged` or `closed`: what happened to the PR.
branchstringrequired- The PR's branch. `task/
- ` names the task, whatever follows the id. prstringoptional- The PR's URL, recorded as a ref and named in the note the event writes.
shastringoptional- The merge commit's sha. Recorded on `merged` only.
taskinteger or stringoptional- The task, when the branch does not name it; it overrides the branch when both do.
Response
resultchangedbooleanalways- False when the event wrote nothing: a replay whose outcome already holds, or an event a recorded merge already settled.
eventstringalways- The event, as given:
opened,mergedorclosed. short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title.
statusstringalways- The task's status after the event.
transitionstringnullablestarted,completedorannotated; null when the event moved nothing beyond the refs.notestringnullable- The note the event wrote (on
completed, the delivery note); null when it wrote none. reasonstringnullable- Why nothing was written when
changedis false:already recorded, or the state that kept the event from moving its ref (mergedis final). Null otherwise.
echo '{"tasqx":"1","id":"ge1","method":"git.event","params":{"event":"merged","branch":"task/51-sdk-migration-guide","pr":"https://github.com/acme/api/pull/412","sha":"4f9c2e1"}}' | tasqx api{
"id": "ge1",
"ok": true,
"result": {
"changed": true,
"event": "merged",
"note": "Merged https://github.com/acme/api/pull/412 (4f9c2e1), but not completed: 2 checks not passed: open \"The rate-limit ceiling is stated with its real number\"; open \"Reviewed by whoever ships 3.0\"; blocked by #50",
"reason": null,
"short_id": 51,
"status": "pending",
"title": "Write the migration guide for SDK 3.0",
"transition": "annotated"
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
a PR event is reported by whatever watched the PR — the land skill reading gh pr view, a CI step, a git hook — through tasqx git event (D239). An agent that claimed a merge would be reporting a fact it has not seen; it completes a task with tasqx_complete_task and cites the merge as evidence.
tasqx gitgit — Let a pull request start or complete its task.
Reminder methods
reminder.fire
{fired, short_id, at}. Idempotent.
Objects: Task.
Parameters
refinteger or stringrequired- The task whose reminder is being fired. Daemon-internal — the `reminded` event is a dedupe key, not a notification anyone reads here.
atstringrequired- The instant the reminder is recorded against. Firing the same instant twice is a no-op.
Response
resultfiredbooleanalways- Whether this call fired the reminder. False on a repeat — the method is idempotent.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. atstringalways- The instant the reminder is recorded against.
echo '{"tasqx":"1","id":"rf1","method":"reminder.fire","params":{"ref":"49","at":"2026-09-17T16:00:00Z"}}' | tasqx api{
"id": "rf1",
"ok": true,
"result": {
"at": "2026-09-17T16:00:00Z",
"fired": true,
"short_id": 49
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
daemon-internal. Its reminded event is a dedupe key and a push surface, not a thing a client asks for.
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
Core methods
core.capabilities
{api, methods, params, features, default_project, store}.
Parameters
None. Send params as {}, or leave it out.
Response
resultapistringalways- The API major version this build speaks.
methodsarrayalways- Every method name it dispatches.
paramsobjectalways- The accepted params key set, per method — a key outside it is refused, never ignored (D33).
featuresarrayalways- The optional features this build carries.
default_projectstringnullable- The store's default project, or null.
storestringnullable- The store file this engine answers from (D74) — the caller's own in-process, the daemon's over a socket. Null in memory.
echo '{"tasqx":"1","id":"c1","method":"core.capabilities"}' | tasqx api{
"id": "c1",
"ok": true,
"result": {
"api": "1",
"default_project": "work.tasqx",
"features": [
"dependencies",
"filter.boolean",
"reminders"
],
"methods": [
"project.create",
"project.list",
"project.use",
"project.archive",
"project.unarchive",
"task.add",
"task.list",
"task.get",
"task.brief",
"task.next",
"task.start",
"task.stop",
"task.claim",
"task.release",
"task.adjust_tracked",
"task.done",
"task.modify",
"task.cancel",
"task.reopen",
"task.triage",
"task.triage_list",
"tag.add",
"tag.remove",
"annotation.add",
"check.add",
"check.set",
"check.remove",
"annotation.remove",
"annotation.move",
"annotation.update",
"token.add",
"token.remove",
"dependency.add",
"dependency.remove",
"link.add",
"link.remove",
"link.list",
"graph.query",
"memory.add",
"memory.search",
"memory.get",
"memory.remove",
"memory.import",
"memory.refresh",
"memory.list",
"memory.update",
"note.add",
"note.list",
"note.get",
"note.update",
"note.remove",
"note.promote",
"tokens.recompute",
"report.summary",
"report.outcomes",
"store.export",
"store.import",
"git.event",
"event.list",
"event.revert",
"reminder.fire",
"core.capabilities",
"otlp.status"
],
"params": {
"annotation.add": [
"ref",
"body",
"expected_rev"
],
"annotation.move": [
"ref",
"annotation_id",
"to"
],
"annotation.remove": [
"ref",
"annotation_id"
],
"annotation.update": [
"ref",
"annotation_id",
"body",
"expected_rev"
],
"check.add": [
"ref",
"body"
],
"check.remove": [
"ref",
"check_id",
"position"
],
"check.set": [
"ref",
"check_id",
"position",
"state",
"evidence",
"expected_rev"
],
"core.capabilities": [],
"dependency.add": [
"ref",
"depends_on",
"expected_rev"
],
"dependency.remove": [
"ref",
"depends_on",
"expected_rev"
],
"event.list": [
"limit",
"ref",
"entity",
"from"
],
"event.revert": [
"ref",
"expected_rev"
],
"git.event": [
"event",
"branch",
"pr",
"sha",
"task"
],
"graph.query": [
"root",
"select",
"depth",
"node_types",
"relation_types",
"project",
"status",
"tags",
"modified_after",
"modified_before",
"include_inferred",
"max_nodes",
"max_edges"
],
"link.add": [
"from",
"to",
"relation",
"metadata",
"expected_rev"
],
"link.list": [
"ref",
"relation",
"limit",
"offset"
],
"link.remove": [
"id"
],
"memory.add": [
"title",
"body",
"source",
"project",
"standing"
],
"memory.get": [
"id",
"workdir"
],
"memory.import": [
"docs",
"project"
],
"memory.list": [
"limit",
"offset",
"project",
"standing"
],
"memory.refresh": [
"dry_run"
],
"memory.remove": [
"id"
],
"memory.search": [
"query",
"limit",
"scope",
"raw",
"project",
"include_unscoped",
"source_prefix",
"mode",
"min_similarity",
"workdir"
],
"memory.update": [
"id",
"title",
"body",
"source",
"project",
"standing",
"expected_rev"
],
"note.add": [
"body",
"project"
],
"note.get": [
"id",
"max_body_bytes"
],
"note.list": [
"status",
"links",
"query",
"project",
"limit",
"offset",
"max_body_bytes"
],
"note.promote": [
"id",
"to",
"title",
"expected_rev"
],
"note.remove": [
"id"
],
"note.update": [
"id",
"body",
"project",
"status",
"expected_rev"
],
"otlp.status": [],
"project.archive": [
"name"
],
"project.create": [
"name",
"description"
],
"project.list": [
"include_archived"
],
"project.unarchive": [
"name"
],
"project.use": [
"name"
],
"reminder.fire": [
"ref",
"at"
],
"report.outcomes": [
"group_by",
"filter",
"metrics",
"since",
"until"
],
"report.summary": [
"group_by",
"filter",
"metrics",
"all",
"since",
"until"
],
"store.export": [
"filter",
"include_unscoped",
"out_path"
],
"store.import": [
"tasks",
"projects",
"default_project",
"docs",
"events",
"links",
"notes",
"dry_run",
"merge",
"path"
],
"tag.add": [
"ref",
"tags"
],
"tag.remove": [
"ref",
"tags"
],
"task.add": [
"title",
"project",
"priority",
"due",
"scheduled",
"wait",
"estimate",
"tags",
"recurrence",
"remind",
"checks",
"depends_on",
"budget_tokens",
"status",
"discovered_from",
"client"
],
"task.adjust_tracked": [
"ref",
"delta",
"reason"
],
"task.brief": [
"ref",
"memory_limit",
"annotations_limit",
"annotations_offset",
"max_body_bytes"
],
"task.cancel": [
"ref",
"expected_rev",
"actor",
"force_claim"
],
"task.claim": [
"ref",
"lease",
"actor",
"expected_rev"
],
"task.done": [
"ref",
"session_id",
"transcript_path",
"client",
"tool",
"model",
"checks_passed",
"evidence",
"force",
"input_tokens",
"output_tokens",
"cache_read_tokens",
"cache_creation_tokens",
"total_tokens",
"expected_rev",
"actor",
"force_claim"
],
"task.get": [
"ref",
"annotations_limit",
"annotations_offset",
"max_body_bytes",
"explain"
],
"task.list": [
"filter",
"sort",
"limit",
"offset",
"fields",
"actor"
],
"task.modify": [
"ref",
"set",
"expected_rev",
"actor",
"force_claim"
],
"task.next": [
"project",
"filter",
"resume",
"actor"
],
"task.release": [
"ref",
"actor",
"force_claim",
"expected_rev"
],
"task.reopen": [
"ref",
"expected_rev"
],
"task.start": [
"ref",
"keep",
"session_id",
"transcript_path",
"client",
"actor",
"holder",
"expected_rev"
],
"task.stop": [
"ref",
"expected_rev",
"actor",
"force_claim"
],
"task.triage": [
"ref",
"action",
"reason",
"of",
"expected_rev",
"actor",
"force_claim"
],
"task.triage_list": [
"limit",
"offset"
],
"token.add": [
"ref",
"tool",
"source",
"model",
"input_tokens",
"output_tokens",
"cache_read_tokens",
"cache_creation_tokens",
"total_tokens",
"confidence",
"idempotency_key"
],
"token.remove": [
"measurement_id"
],
"tokens.recompute": [
"dry_run"
]
},
"store": "~/.local/share/tasqx/tasks.db"
},
"tasqx": "1"
}Illustrative. its store is the path of whatever store answered, so a capture would quote the machine that took it. The handshake at the top of this page is rendered live from this build's own capabilities() with a plausible store instead (D74).
No MCP tool. An agent reaches this method only through a human running tasqx api.
the MCP handshake already answers this question: initialize reports the protocol revision and scope, and tools/list reports the surface. A second, differently shaped capability document is a second thing to keep in sync.
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
OTLP methods
otlp.status
{received, attributed, orphaned, last_seen}. Read-only visibility into the opt-in local OTLP receiver's buffer (#18): received is every sample still inside the 30-day retention window, attributed how many turned into a source=otel measurement, orphaned the rest, and last_seen the newest sample's timestamp (null on an empty buffer) — the only way to tell a misconfigured exporter from a healthy, quiet one.
Parameters
None. Send params as {}, or leave it out.
Response
resultreceivedintegeralways- Samples still inside the 30-day retention window.
attributedintegeralways- How many of them turned into a measurement.
orphanedintegeralways- How many matched no task's window.
last_seenstringnullable- The newest sample's timestamp; null on an empty buffer — a misconfigured exporter told apart from a quiet one.
echo '{"tasqx":"1","id":"o1","method":"otlp.status"}' | tasqx api{
"id": "o1",
"ok": true,
"result": {
"attributed": 0,
"last_seen": null,
"orphaned": 0,
"received": 0
},
"tasqx": "1"
}No MCP tool. An agent reaches this method only through a human running tasqx api.
an operator diagnostic for a machine-local, opt-in receiver (#18) — is telemetry reaching THIS daemon on THIS machine — not a fact about any task an agent is working. tasqx config store / a CLI verb is where a human checks it (#222).
No verb reaches this method on its own — tasqx api is the way, and the envelope beside this is the whole call.
The MCP server
tasqx bundles an MCP server, so an AI agent reads and mutates your tasks with zero glue. It is the same core API underneath — the agent and your shell are peers.
It fails closed
The least-privilege default is read-only. Write access is an explicit operator choice for this local stdio process. Scope is configuration, not an authentication credential.
tasqx mcp serve
tasqx mcp serve --scope writetasqx mcp: serving over stdio (scope=read)
tasqx mcp: serving over stdio (scope=write)tasqx mcp serve # scope omittedtasqx mcp: serving over stdio (scope=read)Under read scope the 31 write tools are absent from tools/list entirely, so an agent is never aimed at a call it will be refused. Each tool below states the scope it needs.
Talking to it
Newline-delimited JSON-RPC 2.0 on stdin/stdout. Diagnostics go to stderr only — stdout carries nothing but responses, so the transport is never corrupted by a log line.
The initialize result carries instructions: a scope-aware workflow the host may inject into the agent's system prompt, so the agent is told when to reach for tasqx and not only what it can call. It is elided beside this, because it is a few paragraphs long.
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"demo","version":"1"}}}' | tasqx mcp serve 2>/dev/null | sed 's/"instructions":"[^"]*"/"instructions":"…"/'{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"tools":{}},"instructions":"…","protocolVersion":"2024-11-05","serverInfo":{"name":"tasqx","version":"0.16.0"}}}echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"tasqx_list_tasks","arguments":{"filter":"+api"}}}' | tasqx mcp serve 2>/dev/null{"id":3,"jsonrpc":"2.0","result":{"content":[{"text":"{\n \"count\": 1,\n \"next_offset\": null,\n \"store_empty\": false,\n \"tasks\": [\n {\n \"blocked\": false,\n \"due\": \"2026-07-17T00:00:00Z\",\n \"priority\": \"H\",\n \"project\": \"work.tasqx\",\n \"short_id\": 1,\n \"status\": \"pending\",\n \"tags\": [\n \"api\",\n \"release\"\n ],\n \"title\": \"Ship the v1 JSON API freeze\",\n \"urgency\": 17.5\n }\n ],\n \"total\": 1\n}","type":"text"}],"isError":false}}The same calls without the JSON-RPC wrapper are the JSON API: one envelope in, one out, and each tool below links to the method it routes to.
Refusing a write
Ask a read-only server to write, and it refuses by name:
echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"tasqx_add_task","arguments":{"title":"nope"}}}' | tasqx mcp serve 2>/dev/null{"id":4,"jsonrpc":"2.0","result":{"content":[{"text":"error [bad_request]: tool `tasqx_add_task` requires write scope, but this MCP server is running read-only. This cannot be changed from a tool call: the operator must relaunch the server as `tasqx mcp serve --scope write`.","type":"text"}],"isError":true}}Wiring it into a client
Most MCP clients take a command and arguments. Pass the scope you want the child process to have:
{
"mcpServers": {
"tasqx": {
"command": "tasqx",
"args": ["mcp", "serve", "--scope", "write"]
}
}
}Start an agent with the default read scope. Give it write only once you have watched what it does with read. A future network transport needs real authentication; this scope flag is not a credential.
Every tool
12 read tools always; 31 write tools only with write scope. Each carries MCP annotations (readOnlyHint, destructiveHint) so a client can reason about them before calling.
| Tool | Scope | Method | Does |
|---|---|---|---|
tasqx_list_tasks | read | task.list | List tasks matching a filter-DSL query — the CLI's grammar, e.g. |
tasqx_get_task | read | task.get | Get one task's full detail: fields, tags, annotations, dependencies. |
tasqx_brief_task | read | task.brief | Everything you need before starting one task, in ONE call (D136): the task, the tasks it depends on with what each of THEM concluded, what it blocks, and memory found under a query tasqx derives from the task's title, tags and project. |
tasqx_next | read | task.next | The ONE task to work on now (D213): the most urgent unblocked task, as tasqx next picks it, with the first paragraph of its opening note. |
tasqx_summary | read | report.summary | Aggregate report grouped by project, status or priority. |
tasqx_outcomes | read | report.outcomes | How the work went rather than what it cost: rework, estimate calibration, token cost, unannotated completions, started-then-cancelled work, forced (overrode blockers), unbriefed. |
tasqx_list_projects | read | project.list | List projects. |
tasqx_search_memory | read | memory.search | Search memory — imported docs and task annotations — by meaning and words, fused (D196); each hit's via says which matched. |
tasqx_list_memory | read | memory.list | Browse memory docs without knowing a word inside one — what tasqx_search_memory cannot do without a query. |
tasqx_get_memory | read | memory.get | Read one knowledge doc whole, by the id a search hit carries — tasqx_search_memory returns only an excerpt. |
tasqx_add_task | write | task.add | Create a task. |
tasqx_modify_task | write | task.modify | Change fields on a task via a set map. |
tasqx_complete_task | write | task.done | Mark a task done. |
tasqx_cancel_task | write | task.cancel | Cancel a task: backlog, pending or active moves to cancelled. |
tasqx_triage_task | write | task.triage | Settle a task in triage (D238): accept makes it pending, decline cancels it with an optional reason note, duplicate cancels it, moves its notes to of and links the two. |
tasqx_reopen_task | write | task.reopen | Reopen a closed task: done or cancelled goes back to pending, and its completion timestamp is cleared, so no week counts it as finished. |
tasqx_start_timer | write | task.start | Start the timer on a task, moving it to active. |
tasqx_claim_task | write | task.claim | Lease a task so parallel agents skip it and cannot start, stop or close it (D236). |
tasqx_release_task | write | task.release | Give up your claim (D236). |
tasqx_stop_timer | write | task.stop | Stop the timer on a task. |
tasqx_adjust_tracked | write | task.adjust_tracked | Correct tracked time by a signed delta (-2h25m, +30m); any status. |
tasqx_tag_task | write | tag.add | Add one or more tags to a task. |
tasqx_untag_task | write | tag.remove | Remove one or more tags from a task. |
tasqx_add_check | write | check.add | Add an acceptance criterion to a task — one thing that must be true for it to count as done (D138). |
tasqx_set_check | write | check.set | Mark one acceptance criterion passed or failed, with the evidence for it. |
tasqx_remove_check | write | check.remove | Drop an acceptance criterion that turned out to be the wrong thing to ask. |
tasqx_annotate_task | write | annotation.add | Attach a timestamped note to a task. |
tasqx_add_tokens | write | token.add | Record token spend on a task after the fact, e.g. |
tasqx_remove_annotation | write | annotation.remove | Permanently scrub one annotation's text, named by id, id prefix or position. |
tasqx_update_annotation | write | annotation.update | Correct one annotation's body in place, by id, id prefix or position (D165, D211): id, timestamp and position are kept, and search stops finding the old text. |
tasqx_move_annotation | write | annotation.move | Move one annotation to another task (D211): id, body and timestamp are kept and search follows. |
tasqx_add_dependency | write | dependency.add | Make one task depend on another: ref is blocked until depends_on is done or cancelled, and a ref already closed is never blocked. |
tasqx_remove_dependency | write | dependency.remove | Cut a dependency edge: ref stops waiting on depends_on. |
tasqx_add_memory | write | memory.add | Store a knowledge document in memory: patterns, documentation, decisions worth finding again. |
tasqx_update_memory | write | memory.update | Correct a memory doc in place — title, body, source and/or project — by id. |
tasqx_remove_memory | write | memory.remove | Remove one knowledge document from memory by id. |
tasqx_add_note | write | note.add | Capture a quick note: an idea, a URL to read later, a throwaway line. |
tasqx_list_notes | read | note.list | List notes, newest first, paged like tasqx_list_tasks. |
tasqx_get_note | read | note.get | Read one note. |
tasqx_update_note | write | note.update | Change a note's body, project or status; status: archived archives it and open brings it back. |
tasqx_remove_note | write | note.remove | Delete a note for good. |
tasqx_promote_note | write | note.promote | File a note as a task or a memory doc in one transaction, then archive it with promoted_to naming what it became. |
tasqx_create_project | write | project.create | Create a project. |
tasqx_list_tasks
List tasks matching a filter-DSL query — the CLI's grammar, e.g. "project:work status:pending +api due.before:tomorrow". Paged: the response carries count, total and next_offset, null once nothing is left. Rows carry a default field set unless you name fields (D152).
Routes to task.list, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- Who chooses: under @working, others' claims are hidden (D236).
fieldsarray of stringoptional- Restrict each row to these fields. Omit for short_id, title, status, priority, urgency, blocked, due, project, tags, with null-valued keys dropped; send [] for the whole row.
filterstringoptional- Filter DSL, e.g. "status:pending +api". "@working" is the actionable set. Bare words match the title ("memory explorer" as a phrase); "status:any" is every status. Omit it (or send "") for every task.
limitintegeroptional- Rows to return. Omit for this tool's page (100), shrunk to fit the response byte budget; a named limit is honoured up to 10000.
offsetintegeroptional- Matching rows to skip; pass the previous
next_offset. Ordering is stable across pages. sortarray of stringoptional- Sort keys, e.g. ["-urgency", "due"]; "-" descends. Valid: urgency, short_id, priority, due, created, modified, title, tokens, estimate, tracked. An unknown key is rejected.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"filter":"+docs","sort":["-urgency"]},"name":"tasqx_list_tasks"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.list, with a real request and the response it produced.
tasqx_get_task
Get one task's full detail: fields, tags, annotations, dependencies. Annotations come back in pages counted from the newest, oldest first within a page — the response carries annotations_total, and annotations_next_offset whenever older ones were left out.
Routes to task.get, and hands back that method's result — the shape is documented there, once.
Arguments
annotations_limitintegeroptional- Most-recent annotations to return; omit for this tool's page (20). An over-budget page is cut to what fits and the view names the
annotations_offsetfor the rest;include_json: truespends that budget first. 0 returns none. annotations_offsetintegeroptional- Annotations to skip, counted back from the newest; pass the previous
annotations_next_offset. explainbooleanoptional- Add
urgency_breakdown(priority,due_proximity,age,total) — the termsurgencysums (D1). Default false. include_jsonbooleanoptional- MCP only Send the machine-readable JSON block beside the rendered view. Default FALSE: it is one result twice and you read the view (D151). True when a SCRIPT parses this. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not something
task.gethas an opinion about. max_body_bytesintegeroptional- Cap each annotation body at this many BYTES in the response; default 16384. A longer body is cut on a character boundary and marked with its real size and the call that reads it whole; the store is never capped (D148).
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
viewstringoptional- MCP only How the rendered block is spelled. Default "markdown", the view YOU read; "card" is the D146 72-column box card in a text fence, for a PERSON deciding on the task (D164), and quotes only the first annotation. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not something
task.gethas an opinion about.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"51"},"name":"tasqx_get_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.get, with a real request and the response it produced.
tasqx_brief_task
Everything you need before starting one task, in ONE call (D136): the task, the tasks it depends on with what each of THEM concluded, what it blocks, and memory found under a query tasqx derives from the task's title, tags and project. Use tasqx_get_task when you only need the task.
Routes to task.brief, and hands back that method's result — the shape is documented there, once.
Arguments
annotations_limitintegeroptional- The task's most-recent annotations, as on tasqx_get_task; default 20. Over budget, memory is cut first, then this page (D220).
annotations_offsetintegeroptional- Annotations to skip from the newest, as on tasqx_get_task.
include_jsonbooleanoptional- MCP only Send the machine-readable JSON block beside the rendered view. Default FALSE: it is the brief again, snippets included (D151). True when a script parses this. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not something
task.briefhas an opinion about. include_rankbooleanoptional- MCP only Add the fused
rankandscoreto each memory hit; default false, and visible only in theinclude_jsonblock. LOWERrankis the better match and hits arrive sorted best-first. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not somethingtask.briefhas an opinion about. max_body_bytesintegeroptional- Cap each annotation body at this many BYTES in the response; default 16384. A longer body is cut on a character boundary and marked with its real size and the call that reads it whole; the store is never capped (D148).
memory_limitintegeroptional- Memory hits to return; default 5. Half the slots (rounded up) are reserved for knowledge docs, annotations fill the rest, and either kind takes the other's unused slots.
refinteger or stringrequired- Task short_id or UUID.
viewstringoptional- MCP only How the TASK HALF is spelled. Default "markdown", the view YOU read; "card" is the D146 72-column box card in a text fence, for a PERSON deciding on the task (D164). Prerequisites and memory hits stay markdown. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not something
task.briefhas an opinion about.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"memory_limit":3,"ref":"51"},"name":"tasqx_brief_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.brief, with a real request and the response it produced.
tasqx_next
The ONE task to work on now (D213): the most urgent unblocked task, as tasqx next picks it, with the first paragraph of its opening note. With resume: true it also lists the active tasks (and who holds each clock) and the five open tasks annotated most recently, each with the start of its newest note and when it was written. Use tasqx_list_tasks for more than one.
Routes to task.next, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- Who chooses: others' claims are skipped (D236).
filterstringoptional- Filter DSL ANDed onto
@working, e.g.+mcp. include_jsonbooleanoptional- MCP only Send the machine-readable JSON block beside the rendered view. Default FALSE (D151). This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not something
task.nexthas an opinion about. projectstringoptional- Limit the pick to this project.
resumebooleanoptional- Also answer "where did I leave off": active tasks and recently annotated open tasks in the same scope. Default false.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"project":"api","resume":true},"name":"tasqx_next"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.next, with a real request and the response it produced.
tasqx_summary
Aggregate report grouped by project, status or priority. Pure read.
Routes to report.summary, and hands back that method's result — the shape is documented there, once.
Arguments
allbooleanoptional- Count cancelled tasks too. By default a report with no status term skips cancelled only — done work always counts (D24).
filterstringoptional- Optional filter DSL to scope the report.
group_bystringoptional- Grouping axis. Optional; defaults to project.
metricsarray of stringoptional- Extra columns per group. Omit and each group carries only
count: every other metric is opt-in here, unliketasqx report. sincestringoptional- Window
tracked_totaland the token buckets to spend at or after this instant, not the task's lifetime total — a different axis fromfilter'scompleted.*terms (D97). untilstringoptional- The other end of
since: excludes anything at or after this instant. Together they window WHEN the spend happened.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"group_by":"project"},"name":"tasqx_summary"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under report.summary, with a real request and the response it produced.
tasqx_outcomes
How the work went rather than what it cost: rework, estimate calibration, token cost, unannotated completions, started-then-cancelled work, forced (overrode blockers), unbriefed. Every rate carries the n it was computed over. Scope is tasks that CLOSED; tasqx_summary reads work in flight.
Routes to report.outcomes, and hands back that method's result — the shape is documented there, once.
Arguments
filterstringoptional- Optional filter DSL to scope the report —
tasqx_list_tasks' grammar. group_bystringoptional- Grouping axis. Optional; defaults to project.
metricsarray of stringoptional- Which metrics to emit. Omit for ALL of them; naming none does not mean a bare count here.
sincestringoptional- Only count tasks that closed at or after this instant.
since/untilwindow WHEN THE TASK CLOSED, not when spend happened. untilstringoptional- The other end of
since: excludes tasks that closed at or after this instant.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"group_by":"project","metrics":["rework","calibration"]},"name":"tasqx_outcomes"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under report.outcomes, with a real request and the response it produced.
tasqx_list_projects
List projects. By default excludes archived projects.
Routes to project.list, and hands back that method's result — the shape is documented there, once.
Arguments
include_archivedbooleanoptional- Include archived projects in the list. Omitted, an archive is out of sight.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{},"name":"tasqx_list_projects"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under project.list, with a real request and the response it produced.
tasqx_search_memory
Search memory — imported docs and task annotations — by meaning and words, fused (D196); each hit's via says which matched. Words match by STEM; a hit with only some of them says partial: true and makes relaxed: true (D193). matched is the all-words expression, matched_any the any-word one. mode: lexical forces exact words; an id like D41 is matched by words alone. A hit is an excerpt: tasqx_get_memory reads a doc whole, tasqx_get_task an annotation. hits is sorted best-first (LOWER rank is better); total and has_more say what limit cut. A doc hit adds stale (D180) and stale_refs (D235).
Routes to memory.search, and hands back that method's result — the shape is documented there, once.
Arguments
include_rankbooleanoptional- MCP only Add the fused
rankandscoreto each hit. Default false. LOWERrankis better andhitsis already sorted best-first, so never use it as a threshold (D154). This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not somethingmemory.searchhas an opinion about. include_unscopedbooleanoptional- Widen a
projectscope to docs and annotations with NO project — whattasqx memory importproduces. Never admits another project's. Refused withoutproject. limitintegeroptional- Max hits (0 or more); default 10. The response's
total/has_moresay what this left out. min_similaritynumberoptional- Meaning floor; default 0.30.
modestringoptional- Default hybrid: meaning and words. lexical: words only.
projectstringoptional- Scope to one project: docs stored with it, and annotations whose task carries it. Omit to search every project.
querystringrequired- Plain-text search words. Matched as quoted phrases, so hyphens and dots are safe.
rawbooleanoptional- Pass the query through as FTS5 syntax (words only: needs mode lexical or none). Invalid syntax is bad_request.
scopestringoptional- What to search. Optional; defaults to all.
source_prefixstringoptional- Docs whose source starts with this, literally (D240).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"limit":2,"query":"release canary"},"name":"tasqx_search_memory"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under memory.search, with a real request and the response it produced.
tasqx_list_memory
Browse memory docs without knowing a word inside one — what tasqx_search_memory cannot do without a query. Newest-modified first, paged like tasqx_list_tasks. A row is id, title, source and modified, enough to RECOGNISE a doc; include_preview: true adds the rest, and tasqx_get_memory reads one whole (D154).
Routes to memory.list, and hands back that method's result — the shape is documented there, once.
Arguments
include_previewbooleanoptional- MCP only Return the whole engine row —
project,created,_rev, a 160-characterbody_preview,body_truncated— instead of the four identifying fields. Default false (D154). This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not somethingmemory.listhas an opinion about. limitintegeroptional- Rows to return; default 20, because an unbounded browse page grows with the store.
offsetintegeroptional- Matching docs to skip; pass the previous response's
next_offset. projectstringoptional- Restrict to docs stored with this
project. Omit to list every doc. standingbooleanoptional- true lists only standing docs, false only the rest; omit for all (D156).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{},"name":"tasqx_list_memory"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under memory.list, with a real request and the response it produced.
tasqx_get_memory
Read one knowledge doc whole, by the id a search hit carries — tasqx_search_memory returns only an excerpt. An annotation id is refused, naming the task to read it from instead.
Routes to memory.get, and hands back that method's result — the shape is documented there, once.
Arguments
idstringrequired- The doc UUID, as a search hit or
tasqx_add_memoryprints it.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":"eb864f1e-e68a-4d96-af89-597bd0d2d52e"},"name":"tasqx_get_memory"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under memory.get, with a real request and the response it produced.
tasqx_add_task
Create a task. Returns short_id, urgency and status — backlog while scheduled or wait is in the future, outside @working; triage for a proposal (D238) — plus the stored title, due, tags and resolved scheduled, to check a parsed date. CLI sugar in the title adds a warnings entry. checks and depends_on are created in the same transaction (D213) and echoed back, checks with ids. similar: up to three open look-alikes (D237).
Routes to task.add, and hands back that method's result — the shape is documented there, once.
Arguments
budget_tokensintegeroptional- Token budget (D139): a size gauge over FRESH tokens — input, output, cache creation, never cache reads. It STOPS NOTHING; a blown budget signals a task too big to hand over whole.
tasqx_get_taskreportsoveragainst it. checksarray of stringoptional- Acceptance criteria, created open with the task (D213).
clientstringoptional- The tool proposing the task, kept on its `add` event for `tasqx triage` (D238). The MCP server fills it from the handshake when omitted.
depends_onarrayoptional- Short_ids or UUIDs this task waits on (D213). An unknown one refuses the whole add.
discovered_frominteger or stringoptional- The task this one was found while working on (short_id or UUID). The new task gets a `derived_from` link to it, and `tasqx triage` names it (D238).
duestringoptional- The deadline; drives urgency and anchors relative reminders, and never hides the task. Date grammar: "tomorrow", "friday", "2026-07-20", "in 3 days", "eom", "2026-07-20T17:00" (bare clock = UTC; an offset is honoured).
estimatestringoptional- Duration: "4h", "90m", "1h30m", "2d", "1w", or ISO-8601 "PT4H".
prioritystringoptional- Priority: H (high), M (medium), or L (low).
projectstringoptional- The project to file it under. Omitted, the task lands in the store's default project — and the response says which, because that is the only place a caller learns it.
recurrencestringoptional- Recurrence rule (D2 subset): "daily", "every 3 days", "weekly on mon,wed", "monthly on day 15", "monthly on the last friday".
remindstringoptional- Reminder: a signed offset from
due("-1h", "-30m", "-2d", "+15m") or an absolute date in theduegrammar. scheduledstringoptional- When you intend to start; a future value holds the task in
backloguntil it arrives. Date grammar: "tomorrow", "friday", "2026-07-20", "in 3 days", "eom", "2026-07-20T17:00" (bare clock = UTC; an offset is honoured). statusstringoptional- triage: a proposal, outside @working (D238).
tagsarray of stringoptional- Tags to put on the task. Lowercased and deduplicated as they are stored.
titlestringrequired- Stored verbatim: CLI sugar (
+tag,due:,!prio) is not parsed; use the fields below. waitstringoptional- Hides the task until then, in
backloglikescheduledbut meaning "not my problem yet". Date grammar: "tomorrow", "friday", "2026-07-20", "in 3 days", "eom", "2026-07-20T17:00" (bare clock = UTC; an offset is honoured).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"due":"friday","priority":"H","project":"website","tags":["planning"],"title":"Draft the Q4 roadmap"},"name":"tasqx_add_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.add, with a real request and the response it produced.
tasqx_modify_task
Change fields on a task via a set map. Returns {short_id, _rev, set}, where set echoes the RESOLVED value stored for each field named. Optimistic concurrency is ON: when expected_rev is omitted this server pins the task's current _rev, so a concurrent edit is a conflict naming both revs rather than a silent overwrite, and there is no way to opt out. On conflict, re-read the task, re-apply, retry (D75).
Routes to task.modify, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- Holder (D236).
expected_revintegeroptional- Optimistic-concurrency guard. Supplied by the server from the task's current
_revwhen omitted; pass it to pin a rev you read earlier. There is no last-writer-wins mode. force_claimbooleanoptional- Override another holder's claim.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
setobjectrequired- Field → new value, e.g. {"priority":"M","due":"2026-07-22T17:00:00+02:00"}.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"52","set":{"due":"friday","priority":"H"}},"name":"tasqx_modify_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.modify, with a real request and the response it produced.
tasqx_complete_task
Mark a task done. Returns any tasks its completion newly unblocked. Report what this task cost via the *_tokens params, or total_tokens alone when you only know one number (D167): a transcript measurement wins when tasqx finds the session that ran this task, and self-report is the fallback, graded medium because nothing can check it (D50, D188). tool/model are recorded on the event even with no count. Open dependencies are a conflict unless force (D150, counted by tasqx_outcomes); another holder's claim, unless force_claim (D236). view: "card" leads with the task's box card (D153), for a person deciding on it; a routine completion needs none (D164).
Routes to task.done, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- Who is completing, as a claim holder (D236): another holder's live claim is a `conflict` unless `force_claim`. Filled in with the MCP connection's id when omitted there; `user` elsewhere.
cache_creation_tokensintegeroptional- Self-reported cache-creation tokens (0 or more).
cache_read_tokensintegeroptional- Self-reported cache-read tokens (0 or more).
checks_passedarray of stringoptional- Ids of the acceptance criteria this work proved (D138). Completing with criteria open or failed is not refused, but
checks_hintsays so andtasqx_outcomescounts it unproven. clientstringoptional- Calling tool as "<name> <version>". Filled in from the MCP handshake when omitted.
evidencestringoptional- One citation covering
checks_passed: a test name, an excerpt of output, a commit sha. expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
forcebooleanoptional- Past open dependencies (D150).
force_claimbooleanoptional- Complete past another holder's live claim; the `done` event records `claim_forced` with that holder (D236). `force` is only for open dependencies (D150).
include_retrobooleanoptional- MCP only Add the
retroblock (D222). Default true. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not somethingtask.donehas an opinion about. input_tokensintegeroptional- Self-reported input tokens (0 or more).
modelstringoptional- The model doing the work, e.g. "claude-opus-5". Recorded with or without token counts.
output_tokensintegeroptional- Self-reported output tokens (0 or more).
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
session_idstringoptional- Your agent-session id, recorded on this task's event for token attribution.
toolstringoptional- The AI tool doing the work, free-form (e.g. "claude-code"). Also names a token measurement, defaulting to
client. total_tokensintegeroptional- One unsplit count, instead of the four (D167).
transcript_pathstringoptional- Absolute path to your session transcript, recorded on the same event.
viewstringoptional- MCP only "card" puts the D146 box card of the task AS COMPLETED, in a text fence, before the JSON, for a PERSON deciding on it (D164). Default "markdown": the JSON alone. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not something
task.donehas an opinion about.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"47"},"name":"tasqx_complete_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.done, with a real request and the response it produced.
tasqx_cancel_task
Cancel a task: backlog, pending or active moves to cancelled. The row is kept and stops counting in reports. Returns the tasks it newly unblocked, as a completion does (D11). A task already closed is a conflict, not a no-op.
Routes to task.cancel, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- Holder (D236).
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
force_claimbooleanoptional- Override another holder's claim.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"60"},"name":"tasqx_cancel_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.cancel, with a real request and the response it produced.
tasqx_triage_task
Settle a task in triage (D238): accept makes it pending, decline cancels it with an optional reason note, duplicate cancels it, moves its notes to of and links the two. A task not in triage is a conflict.
Routes to task.triage, and hands back that method's result — the shape is documented there, once.
Arguments
actionstringrequired- `accept` (to pending), `decline` (cancelled, `reason` kept as a note) or `duplicate` (cancelled, notes moved to `of`, linked with `duplicates`). D238.
actorstringoptional- Holder (D236).
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written.
force_claimbooleanoptional- Override another holder's claim.
ofinteger or stringoptional- The task it duplicates (action
duplicate). reasonstringoptional- Why it was declined.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"action":"duplicate","of":"60","ref":"62"},"name":"tasqx_triage_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.triage, with a real request and the response it produced.
tasqx_reopen_task
Reopen a closed task: done or cancelled goes back to pending, and its completion timestamp is cleared, so no week counts it as finished. A task that is not closed is a conflict, not a no-op.
Routes to task.reopen, and hands back that method's result — the shape is documented there, once.
Arguments
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"12"},"name":"tasqx_reopen_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.reopen, with a real request and the response it produced.
tasqx_start_timer
Start the timer on a task, moving it to active. The correlation params land on the start event for token attribution.
Routes to task.start, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- Who is asking for the clock (D140); filled in with this connection's id when omitted. Starting while ANOTHER actor holds an active clock or a claim (D236) is a
conflict, not a silent stop of their timer; passkeep: trueto run both. Reconnecting? Pass a stable id. clientstringoptional- Calling tool as "<name> <version>". Filled in from the MCP handshake when omitted.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
include_memorybooleanoptional- MCP only Add
task.brief's memory search, 3 hits (D186). Default true; false answers the bare result. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not somethingtask.starthas an opinion about. keepbooleanoptional- Keep other active tasks running (opt out of single-active).
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
session_idstringoptional- Your agent-session id, recorded on this task's event for token attribution.
transcript_pathstringoptional- Absolute path to your session transcript, recorded on the same event.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"50"},"name":"tasqx_start_timer"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.start, with a real request and the response it produced.
tasqx_claim_task
Lease a task so parallel agents skip it and cannot start, stop or close it (D236). Claim again to renew. The default holder is this connection, and a restarted server is a NEW holder locked out of its old claims: if you may reconnect, pass the same stable actor on the claim AND on every later call (start, stop, complete, cancel, release); force_claim passes a stranded claim.
Routes to task.claim, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- The holder; default this connection. Reconnecting? Pass a stable id.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (D236).
leasestringoptional- Default 30m, max 24h.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"actor":"builder-2","lease":"1h","ref":"47"},"name":"tasqx_claim_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.claim, with a real request and the response it produced.
tasqx_release_task
Give up your claim (D236).
Routes to task.release, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- The holder; default this connection. Reconnecting? Pass a stable id.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (D236).
force_claimbooleanoptional- Override another holder's claim.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"actor":"builder-2","ref":"47"},"name":"tasqx_release_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.release, with a real request and the response it produced.
tasqx_stop_timer
Stop the timer on a task. Returns interval (the duration just closed) and tracked (the task's running total, matching tasqx_get_task).
Routes to task.stop, and hands back that method's result — the shape is documented there, once.
Arguments
actorstringoptional- Holder (D236).
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#638).
force_claimbooleanoptional- Override another holder's claim.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"48"},"name":"tasqx_stop_timer"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.stop, with a real request and the response it produced.
tasqx_adjust_tracked
Correct tracked time by a signed delta (-2h25m, +30m); any status. tasqx undo reverts it.
Routes to task.adjust_tracked, and hands back that method's result — the shape is documented there, once.
Arguments
deltastringrequired- Signed duration:
-2h25m,+30m. reasonstringrequired- Why the time is corrected.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"delta":"-2h25m","reason":"idle gap","ref":"42"},"name":"tasqx_adjust_tracked"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under task.adjust_tracked, with a real request and the response it produced.
tasqx_tag_task
Add one or more tags to a task. Tags are stored lowercase; one containing whitespace is refused. Returns the resulting tag set.
Routes to tag.add, and hands back that method's result — the shape is documented there, once.
Arguments
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
tagsarray of stringrequired- The tags to add. Already-present tags are a no-op rather than an error.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"53","tags":["q4"]},"name":"tasqx_tag_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under tag.add, with a real request and the response it produced.
tasqx_untag_task
Remove one or more tags from a task. Returns the resulting tag set. A tag the task does not carry is not_found and removes NONE of the tags named — all or nothing, so a typo can never answer ok.
Routes to tag.remove, and hands back that method's result — the shape is documented there, once.
Arguments
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
tagsarray of stringrequired- The tags to take off. All or nothing: a tag the task does not have is `not_found` and removes none of them, so a typo cannot answer ok.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"ref":"53","tags":["ops"]},"name":"tasqx_untag_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under tag.remove, with a real request and the response it produced.
tasqx_add_check
Add an acceptance criterion to a task — one thing that must be true for it to count as done (D138). tasqx NEVER RUNS a check: it is a claim you mark later with evidence, not a command.
Routes to check.add, and hands back that method's result — the shape is documented there, once.
Arguments
bodystringrequired- The criterion, in your own words. Stored verbatim.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"Every renamed symbol has a row in the table","ref":"51"},"name":"tasqx_add_check"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under check.add, with a real request and the response it produced.
tasqx_set_check
Mark one acceptance criterion passed or failed, with the evidence for it. failed is a normal outcome worth recording. Evidence is stored verbatim and never interpreted.
Routes to check.set, and hands back that method's result — the shape is documented there, once.
Arguments
check_idstringoptional- The check's id, from
tasqx_get_taskor the add. evidencestringoptional- Your proof: a test name, an excerpt of output, a commit sha. Optional — an invented citation is worse than an unproven
passed. expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
positionintegeroptional- Or its 1-based place in tasqx_get_task's list.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
statestringrequired- open | passed | failed.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"check_id":"7e37a508-7921-4cb2-bf0c-0a2944eb31e4","evidence":"docs/guides/sdk-3.0.md#rate-limits","ref":"51","state":"passed"},"name":"tasqx_set_check"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under check.set, with a real request and the response it produced.
tasqx_remove_check
Drop an acceptance criterion that turned out to be the wrong thing to ask. Use tasqx_set_check with failed when the criterion was right and the work did not meet it; deleting it would hide the finding.
Routes to check.remove, and hands back that method's result — the shape is documented there, once.
Arguments
check_idstringoptional- The check's id.
positionintegeroptional- Or its 1-based place in tasqx_get_task's list.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"check_id":"e6a1096b-6f05-4e95-96f5-52452080f2ac","ref":"51"},"name":"tasqx_remove_check"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under check.remove, with a real request and the response it produced.
tasqx_annotate_task
Attach a timestamped note to a task. The body is stored verbatim, newlines and markdown included, so long-form context lives here. The response echoes the body back as proof of that, or {id, created, body_bytes} under include_body: false. tasqx_remove_annotation takes a note back.
Routes to annotation.add, and hands back that method's result — the shape is documented there, once.
Arguments
bodystringrequired- Note text, stored verbatim. Multi-line markdown is fine.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
include_bodybooleanoptional- MCP only Echo the stored body back. Default true (D72/D75): your proof the store kept it verbatim. Pass false for
body_bytesin its place on a long note. This argument is read by the server and never forwarded: it describes the response envelope, which is the transport's own business and not somethingannotation.addhas an opinion about. refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"Ruling: the guide ships WITH the 3.0 release.","ref":"51"},"name":"tasqx_annotate_task"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under annotation.add, with a real request and the response it produced.
tasqx_add_tokens
Record token spend on a task after the fact, e.g. a count that arrived after completion (D167). Send the split counts, or total_tokens alone when only one number is known — never both. Stored as a self-report at confidence medium, as on tasqx_complete_task: nothing can check it, so it is never high (D50); only a session-confirmed transcript or OTLP telemetry earns that. Reuse idempotency_key on a retry.
Routes to token.add, and hands back that method's result — the shape is documented there, once.
Arguments
cache_creation_tokensintegeroptional- Tokens spent writing the cache.
cache_read_tokensintegeroptional- Tokens read from cache. Kept in its own bucket and never blended with the others (D48).
idempotency_keystringoptional- The same key twice on one task banks once.
input_tokensintegeroptional- Fresh input tokens.
modelstringoptional- The model that did the work. Recorded, never interpreted.
output_tokensintegeroptional- Output tokens.
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
toolstringoptional- The AI tool that spent them; defaults to the handshake's client.
total_tokensintegeroptional- One unsplit count, instead of the four.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"cache_creation_tokens":12000,"cache_read_tokens":91000,"confidence":"medium","input_tokens":18400,"model":"opus","output_tokens":2600,"ref":"51","source":"self-report","tool":"claude-code"},"name":"tasqx_add_tokens"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under token.add, with a real request and the response it produced.
tasqx_remove_annotation
Permanently scrub one annotation's text, named by id, id prefix or position. Unlike every other write here this is a HARD delete: the body is overwritten in the store and the original annotation.add event's body is redacted in the same transaction, so a secret is gone from event.list and store.export too (D113). A text-free tombstone stays for audit under annotations_removed. tasqx undo does NOT cover this. An unknown or removed id is not_found.
Routes to annotation.remove, and hands back that method's result — the shape is documented there, once.
Arguments
annotation_idstringrequired- Full id, unique prefix of 8+ characters, or the 1-based position
tasqx showprints (D211). refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"annotation_id":"17dd6621-7db4-43b5-9f36-ddf89018081e","ref":"51"},"name":"tasqx_remove_annotation"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under annotation.remove, with a real request and the response it produced.
tasqx_update_annotation
Correct one annotation's body in place, by id, id prefix or position (D165, D211): id, timestamp and position are kept, and search stops finding the old text. tasqx undo covers it. An omitted expected_rev pins the task's current _rev, as on tasqx_modify_task.
Routes to annotation.update, and hands back that method's result — the shape is documented there, once.
Arguments
annotation_idstringrequired- Full id, unique prefix of 8+ characters, or the 1-based position
tasqx showprints (D211). bodystringrequired- The corrected text; replaces the old body whole.
expected_revintegeroptional- Guard on the task's
_rev; supplied by the server when omitted. refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"annotation_id":"019f7c0a-3d51-7401-a1e6-c040f1ddbe51","body":"Ruling: the guide ships AFTER the 3.0 release.","ref":"51"},"name":"tasqx_update_annotation"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under annotation.update, with a real request and the response it produced.
tasqx_move_annotation
Move one annotation to another task (D211): id, body and timestamp are kept and search follows. For a note written on the wrong task. tasqx undo puts it back. The same task is conflict; a missing one not_found.
Routes to annotation.move, and hands back that method's result — the shape is documented there, once.
Arguments
annotation_idstringrequired- Full id, unique prefix of 8+ characters, or 1-based position on
ref. refinteger or stringrequired- Task short_id (integer) or full UUID (string).
tointeger or stringrequired- The task to move it to: short_id (integer) or full UUID.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"annotation_id":"1","ref":"51","to":"52"},"name":"tasqx_move_annotation"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under annotation.move, with a real request and the response it produced.
tasqx_add_dependency
Make one task depend on another: ref is blocked until depends_on is done or cancelled, and a ref already closed is never blocked. Returns the dependency list and blocked state. A cycle is a conflict.
Routes to dependency.add, and hands back that method's result — the shape is documented there, once.
Arguments
depends_oninteger or stringrequired- The task
refmust wait for: short_id or UUID. expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"depends_on":"50","ref":"55"},"name":"tasqx_add_dependency"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under dependency.add, with a real request and the response it produced.
tasqx_remove_dependency
Cut a dependency edge: ref stops waiting on depends_on. Returns the remaining dependency list and blocked state.
Routes to dependency.remove, and hands back that method's result — the shape is documented there, once.
Arguments
depends_oninteger or stringrequired- The blocker to stop waiting for: short_id or UUID.
expected_revintegeroptional- Optimistic-concurrency guard, as on `task.modify`: the task's `_rev` as you read it. A stale one is a `conflict` carrying the current rev, and nothing is written (#639).
refinteger or stringrequired- Task short_id (integer) or full UUID (string).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"depends_on":"50","ref":"51"},"name":"tasqx_remove_dependency"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under dependency.remove, with a real request and the response it produced.
tasqx_add_memory
Store a knowledge document in memory: patterns, documentation, decisions worth finding again. The body is stored verbatim (markdown fine) and becomes searchable through tasqx_search_memory. similar lists up to three look-alike docs (D237).
Routes to memory.add, and hands back that method's result — the shape is documented there, once.
Arguments
bodystringrequired- Stored verbatim; multi-line markdown is fine.
projectstringoptional- Optional project scope. Omit to leave the doc unscoped; it is never defaulted onto a current project.
sourcestringoptional- Where this came from: a path, URL, or ticket. One doc per source: one another doc holds is a
conflictnaming it (D174). standingbooleanoptional- True marks a ruling that belongs in every session of its scope until retracted; default false (D156).
titlestringrequired- The doc's title. Indexed with the body, so the words a future search will use belong in it.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"60 requests a minute per key, burst 120.","project":"api","source":"notes/limits.md","title":"rate-limit-ceiling"},"name":"tasqx_add_memory"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under memory.add, with a real request and the response it produced.
tasqx_update_memory
Correct a memory doc in place — title, body, source and/or project — by id. Prefer it over a second tasqx_add_memory: a correction written as a new doc leaves the stale one polluting search rankings, and tasqx_remove_memory is permanent. Optimistic concurrency as on tasqx_modify_task: an omitted expected_rev pins the current rev, so a concurrent edit is a conflict naming both (D75).
Routes to memory.update, and hands back that method's result — the shape is documented there, once.
Arguments
bodystringoptional- New body, stored verbatim. Omit to leave unchanged.
expected_revintegeroptional- Optimistic-concurrency guard. Supplied by the server from the doc's current
revwhen omitted; pass it to pin a rev you read earlier. idstringrequired- The doc UUID, as a search hit,
tasqx_add_memoryortasqx_list_memoryprints it. projectstringoptional- New project scope. Omit to leave unchanged.
sourcestringoptional- New source. Omit to leave unchanged. One another doc holds is a
conflictnaming it (D174). standingbooleanoptional- Set or clear the standing flag; omit to leave unchanged (D156).
titlestringoptional- New title. Omit to leave unchanged.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"The handover happens Friday at 16:00, in the on-call channel.","id":"1bce1a9b-5134-4ab7-8665-34cd79fe0c5f"},"name":"tasqx_update_memory"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under memory.update, with a real request and the response it produced.
tasqx_remove_memory
Remove one knowledge document from memory by id. Use it to retract something that turned out wrong: a correction written as a second document leaves both in the store, with search ranking them together. The removal is permanent — tasqx undo does not cover memory docs, and the body is not recoverable from the event log.
Routes to memory.remove, and hands back that method's result — the shape is documented there, once.
Arguments
idstringrequired- The document's id, as tasqx_search_memory reports it on a
dochit.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":"091eb5ff-05d5-4cb2-ba2f-0afdc77f7935"},"name":"tasqx_remove_memory"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under memory.remove, with a real request and the response it produced.
tasqx_add_note
Capture a quick note: an idea, a URL to read later, a throwaway line. Not a task and not memory, so it stays out of @working, reports and tasqx_search_memory until tasqx_promote_note files it (D241).
Routes to note.add, and hands back that method's result — the shape is documented there, once.
Arguments
bodystringrequired- Stored verbatim.
projectstringoptional- An existing project to file it under. Optional.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"body":"try the new tracing exporter","project":"infra"},"name":"tasqx_add_note"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under note.add, with a real request and the response it produced.
tasqx_list_notes
List notes, newest first, paged like tasqx_list_tasks. A body over max_body_bytes is cut and marked body_truncated; tasqx_get_note reads it whole. Never write a cut body back. query is a case-insensitive substring, not a ranked search; each row says has_links (D241).
Routes to note.list, and hands back that method's result — the shape is documented there, once.
Arguments
limitintegeroptional- Default 20.
linksbooleanoptional- Only notes holding an
http://orhttps://link. max_body_bytesintegeroptional- Default 640.
offsetintegeroptional- Pass the previous response's
next_offset. projectstringoptional- Only notes filed under this project.
querystringoptional- A substring of the body, any case.
statusstringoptional- Default open.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"links":true},"name":"tasqx_list_notes"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under note.list, with a real request and the response it produced.
tasqx_get_note
Read one note. A body over max_body_bytes is cut and marked body_truncated with its real body_bytes; raise the cap to that to read every byte (D148, D241).
Routes to note.get, and hands back that method's result — the shape is documented there, once.
Arguments
idstringrequired- The note's uuid or short id.
max_body_bytesintegeroptional- Default 16384.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":2},"name":"tasqx_get_note"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under note.get, with a real request and the response it produced.
tasqx_update_note
Change a note's body, project or status; status: archived archives it and open brings it back. A body that is a prefix of the stored one, as a body_truncated row is, is a conflict. An omitted expected_rev pins the current _rev, so a concurrent edit is a conflict (D241).
Routes to note.update, and hands back that method's result — the shape is documented there, once.
Arguments
bodystringoptional- New body. Omit to leave unchanged.
expected_revintegeroptional- Optimistic-concurrency guard. Supplied by the server from the note's current
_revwhen omitted. idstringrequired- The note's uuid or short id.
projectstringoptional- New project. Omit to leave unchanged.
statusstringoptional- `archived` archives the note, `open` brings it back (D241).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"expected_rev":0,"id":2,"status":"archived"},"name":"tasqx_update_note"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under note.update, with a real request and the response it produced.
tasqx_remove_note
Delete a note for good. Permanent: tasqx undo does not cover notes (D241). Archive it with tasqx_update_note to keep it.
Routes to note.remove, and hands back that method's result — the shape is documented there, once.
Arguments
idstringrequired- The note's uuid or short id.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":2},"name":"tasqx_remove_note"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under note.remove, with a real request and the response it produced.
tasqx_promote_note
File a note as a task or a memory doc in one transaction, then archive it with promoted_to naming what it became. The title is title or the body's first line; a task gets the full body as a note when it says more. A promoted note is refused (D241).
Routes to note.promote, and hands back that method's result — the shape is documented there, once.
Arguments
expected_revintegeroptional- Optional guard on the note's
_rev. idstringrequired- The note's uuid or short id.
titlestringoptional- Optional; defaults to the body's first line.
tostringrequired- `task` or `memory`: what the note becomes (D241).
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"id":2,"to":"task"},"name":"tasqx_promote_note"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under note.promote, with a real request and the response it produced.
tasqx_create_project
Create a project. Returns its id and name. It does NOT become the default project — MCP exposes no project.use (D22) — so pass project explicitly on every tasqx_add_task that should land here.
Routes to project.create, and hands back that method's result — the shape is documented there, once.
Arguments
descriptionstringoptional- What the project is for. Shown by `project.list` and nowhere load-bearing.
namestringrequired- The project's name. Dotted names (`work.tasqx`) are free-form: nothing nests, and a filter on `work` does not match `work.tasqx`.
{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"description":"Prototypes and spikes","name":"research"},"name":"tasqx_create_project"}}The answer is content text: the method's result as JSON, unless the description above says this tool sends less of it by default — a rendered view, a compact row, no rank — in which case an argument marked MCP only, or fields, asks for the rest. isError is true for a refusal, which is a value and not a transport failure.
The same call as an envelope — arguments are the method's params, one for one — is under project.create, with a real request and the response it produced.
What an agent cannot reach
An agent cannot reach every method: 20 of them are deliberately off the tool surface. Not exposing a tool is a decision here rather than silence — each one names its reason, and a method that ships with neither a tool nor a line in this table fails the build:
| Method | Why not |
|---|---|
task.triage_list | tasqx_list_tasks with status:triage already lists the inbox for an agent; the filing tool and origin task this adds are for the person settling it (D238). |
core.capabilities | the MCP handshake already answers this question: initialize reports the protocol revision and scope, and tools/list reports the surface. A second, differently shaped capability document is a second thing to keep in sync. |
event.list | the audit log is unbounded and has no paging, so exposing it would repeat the task.get mistake D63 fixed. It needs the same limit/offset treatment before it can be a tool; no client has asked for it yet. |
event.revert | tasqx has an undo (D54) and an agent cannot reach it, which is the sharpest single omission on this list. It stays off until the tool can show what it is about to undo: event.revert acts on the last matching event, and its blast radius depends on store state the calling agent has not read. A destructive one-shot whose effect the caller cannot see is not a tool, it is a coin flip. |
git.event | a PR event is reported by whatever watched the PR — the land skill reading gh pr view, a CI step, a git hook — through tasqx git event (D239). An agent that claimed a merge would be reporting a fact it has not seen; it completes a task with tasqx_complete_task and cites the merge as evidence. |
graph.query | an agent already reaches the graph's ends: tasqx_search_memory returns the docs and annotations the inferred half would match, and tasqx_get_task returns a task's notes, checks and dependencies. What graph.query adds over those is a LAYOUT — nodes, edges and provenance to draw — and that is a desktop client's question (D160), not something a model gains by reading two hundred node objects into its context. |
link.add | D160's explicit links are a curation surface: an agent proposing edges between docs, tasks and decisions is exactly the noise the graph's inferred half already covers without persisting anything. A link is durable and shows as fact, so it stays a human act until a promotion flow exists for an agent to ask for one. |
link.list | nothing asks for it yet: an agent reaching the graph wants a node's neighbourhood with labels, which is graph.query's job, not a page of endpoint ids it would then have to resolve one by one. |
link.remove | the corrective half of link.add, off for the same reason: an agent that cannot write an edge has nothing of its own to retract, and deleting a human's curated link unsupervised is a bigger foot-gun than the gap it closes (token.remove's reasoning). |
memory.import | it takes a batch of documents read off a filesystem, and the filesystem the CLI reads is not the one an MCP client is on. memory.add is the per-document tool that does reach across the wire. |
memory.refresh | the sweep re-reads the files memory.import recorded on THIS machine (D180), and the filesystem an MCP client is on is not that one — memory.import's own reason, one method over. Keeping a server's own docs fresh is the server's job, reached by calling the engine, not an agent's to trigger across the wire. |
otlp.status | an operator diagnostic for a machine-local, opt-in receiver (#18) — is telemetry reaching THIS daemon on THIS machine — not a fact about any task an agent is working. tasqx config store / a CLI verb is where a human checks it (#222). |
project.archive | retiring a project is a decision about the human's workspace, not about the work. An agent asked to tidy the project list is being asked to make that decision on their behalf, and the CLI is where it belongs. |
project.unarchive | the other half of project.archive, and for the same reason: whether a project is in rotation is a decision about the human's workspace (D215), and the CLI is where it belongs. |
project.use | ruled out by D22: an agent has project on task.add and should name it, rather than silently re-aiming the human's default for every later call, including the human's own. |
reminder.fire | daemon-internal. Its reminded event is a dedupe key and a push surface, not a thing a client asks for. |
store.export | an agent cannot snapshot what it just wrote, and that is a real gap — but the payload is the whole store, which is exactly the size problem D63 and D66 spent two rounds on. It needs a filter and a budget before it is a tool rather than a way to blow a client's limit in one call. |
store.import | it overwrites, in bulk, from a document nobody has reviewed. The confirmation model (§7) defers to the host's gate, and a host gate on a call whose diff nobody can see is not a safeguard. |
token.remove | the corrective half of token.add (#210), kept off although its other half is tasqx_add_tokens (D167): an agent that reports a wrong count fixes it by reporting the right one, and a removal an agent could reach unsupervised on the ledger a lead reads for budget decisions is a bigger foot-gun than the gap it closes. A human runs tasqx api token.remove instead. |
tokens.recompute | a maintenance pass over the whole store's attribution. It is an operator action with a runtime proportional to history, not a step in anybody's task. |
Task
A task is one piece of work: a title, the dates and priority that score its urgency, the time tracked against it, and everything hung off it — notes, acceptance criteria, dependencies and token spend, each documented on its own page. No method deletes a task, and event.revert refuses to undo its add: a finished task is done, an abandoned one cancelled, and task.reopen brings either back.
Fields
idstringalways- The task's uuid, stable across exports and machines.
short_idintegeralways- The task's short id — the small number every
refaccepts and the CLI prints. titlestringalways- The task's title, verbatim.
statusstringalwayspending,backlog,active,doneorcancelled— or, on a stored row whose status text this build does not recognize, that text verbatim, withstatus_unrecognized: truebeside it.prioritystringnullableH,M,L, or null for none.projectstringnullable- The project the task belongs to, or null.
duestringnullable- When it is due, as an instant, or null.
scheduledstringnullable- When work is planned to start, or null. While it is in the future a
pendingtask reads asbacklog, out of the working set, as withwait;active,doneandcancelledkeep their status. waitstringnullable- Holds the task out of the working set until this instant, or null. While it is in the future a
pendingtask reads asbacklog;active,doneandcancelledkeep their status. estimatestringnullable- The estimate as an ISO 8601 duration (
PT4H), or null. recurrencestringnullable- The recurrence rule (
every 3 days), or null. remindstringnullable- The reminder spec — an offset from
dueor an absolute instant — or null. urgencynumberalways- The urgency score (D1): priority, due proximity and age, summed.
tagsarrayalways- Every tag on the task, as plain strings.
createdstringalways- When the task was captured.
modifiedstringalways- When it last changed.
completedstringnullable- When
task.donecompleted it, or null.task.reopenclears it, andtask.canceldoes not set it. budget_tokensintegernullable- The size gauge over FRESH tokens (D139), or null for no threshold. It stops nothing.
_revintegeralways- The task's revision counter. The methods that change the task or its tags, notes, checks and dependencies bump it;
token.add,token.removeandreminder.fireleave it alone. Send it back asexpected_revto make a change conditional. status_unrecognizedbooleanoptional- Present, and true, only on a row whose
statusno writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one.store.importnow refuses such a status. trackedstringalways- Time on the clock as an ISO duration, the interval still running included up to the moment of the read (D166).
active_sincemarks where that one began. tracked_adjustmentstringalways- The net of every
task.adjust_trackedcorrection already insidetracked, as a signed ISO duration (-PT2H25M);PT0Swhen there is none. active_sincestringnullableintask.list·task.get·task.brief·task.next- When the open interval started, or null when the timer is not running.
active_sincestringoptionalinstore.export- The open interval's anchor, omitted when no timer is running.
tracked_secondsintegeroptional- Tracked time in raw seconds (D42). Omitted when it would be zero.
tracked_adjustment_secondsintegeroptional- The signed net of the
task.adjust_trackedcorrections insidetracked_seconds(D166). Omitted when it would be zero. fresh_tokensintegeralways- Fresh tokens spent on this task — input, output and cache creation, never cache reads (D139), plus any unsplit
total_tokensin full (D167). overbooleannullable- Whether
fresh_tokensis pastbudget_tokens; null when no budget was set, because there is no verdict to give. spawned_fromintegernullableintask.get·task.brief- The short id of the task whose completion spawned this one (a recurrence, D170); null on any other task.
spawned_fromstringoptionalinstore.export- The uuid of the task whose completion spawned this one (D170). Omitted on any other task.
claimed_bystringnullableintask.list·task.next- Who holds a live claim on this task (D236), or null.
claimed_bystringnullableintask.get·task.brief- Who holds a live claim on this task (D236): the holder's actor key,
userfor a shell. Null when nobody does, or the lease has expired. claimed_bystringoptionalinstore.export- The claim's holder (D236), expired or not. Omitted on an unclaimed task.
claim_expiresstringnullableintask.list·task.next- When that claim's lease runs out (UTC); null with
claimed_by. claim_expiresstringnullableintask.get·task.brief- When that claim's lease runs out (UTC); null with
claimed_by. Liveness is read against the clock, so an expired lease is simply null here. claim_expiresstringoptionalinstore.export- When that claim's lease runs out (D236). Omitted with
claimed_by. urgency_breakdownobjectoptional- The terms
urgencysums — present only whenexplain: trueasked for them. urgency_breakdown.prioritynumberalways- The priority term.
urgency_breakdown.due_proximitynumberalways- The due-date term.
urgency_breakdown.agenumberalways- The age term.
urgency_breakdown.totalnumberalways- The sum — the same number as the row's
urgency, so nobody has to recompute the rounding.
From task.get.
{
"_rev": 1,
"active_since": null,
"annotations": [
{
"body": "Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week late is a support ticket that already happened.",
"created": "2026-09-11T11:20:00Z",
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e"
},
{
"body": "Blocked until the rate limit lands: the guide has to state the real ceiling, and quoting a number that then changes is worse than waiting.",
"created": "2026-09-15T11:20:00Z",
"id": "ee4a9a3b-10de-465a-aab1-84eeb0e48236"
},
{
"body": "Scope: the 2.x → 3.0 rename table, the auth change, and a worked example per SDK. Not the reference — that generates itself.",
"created": "2026-09-04T11:20:00Z",
"id": "fd496ca3-cd12-4457-8b43-5ef0668cab3c"
}
],
"annotations_next_offset": null,
"annotations_offset": 0,
"annotations_removed": [],
"annotations_total": 3,
"blocked": true,
"blocks": [],
"budget_tokens": null,
"checks": [
{
"body": "Every renamed symbol has a row in the table",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "ebb86ee2-69ed-4938-b57c-c12a89e9414e",
"modified": "2026-09-14T09:10:00Z",
"position": 0,
"state": "passed"
},
{
"body": "One worked example per SDK (js, py, go)",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "6776fd34-ec65-4b9e-8ce6-a106f4f51c13",
"modified": "2026-09-14T09:10:00Z",
"position": 1,
"state": "passed"
},
{
"body": "The rate-limit ceiling is stated with its real number",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "7e37a508-7921-4cb2-bf0c-0a2944eb31e4",
"modified": "2026-09-14T09:10:00Z",
"position": 2,
"state": "open"
},
{
"body": "Reviewed by whoever ships 3.0",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "e6a1096b-6f05-4e95-96f5-52452080f2ac",
"modified": "2026-09-14T09:10:00Z",
"position": 3,
"state": "open"
}
],
"claim_expires": null,
"claimed_by": null,
"completed": null,
"created": "2026-08-16T10:00:00Z",
"delivered_annotation": null,
"delivered_annotation_id": null,
"depends_on": [
50
],
"due": "2026-09-21T00:00:00Z",
"estimate": "PT5H",
"first_annotation": {
"body": "Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week late is a support ticket that already happened.",
"created": "2026-09-11T11:20:00Z",
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e"
},
"fresh_tokens": 0,
"git_refs": [
{
"created": "2026-09-13T14:05:00Z",
"id": "019f5a10-0000-7000-8000-00000000a001",
"kind": "branch",
"modified": "2026-09-13T14:05:00Z",
"noted": null,
"ref": "task/51-sdk-migration-guide",
"state": "open"
},
{
"created": "2026-09-13T14:05:00Z",
"id": "019f5a10-0000-7000-8000-00000000a002",
"kind": "pr",
"modified": "2026-09-13T14:05:00Z",
"noted": null,
"ref": "https://github.com/acme/api/pull/412",
"state": "open"
}
],
"id": "46773aad-c4aa-435a-abe1-fcde8ce09658",
"modified": "2026-08-16T10:00:00Z",
"over": null,
"priority": "M",
"project": "api",
"recurrence": null,
"remind": null,
"scheduled": null,
"short_id": 51,
"spawned_from": null,
"status": "pending",
"tags": [
"docs"
],
"title": "Write the migration guide for SDK 3.0",
"tokens": [],
"tracked": "PT0S",
"tracked_adjustment": "PT0S",
"unmet_blockers": [
{
"short_id": 50,
"title": "Rate-limit the /search endpoint"
}
],
"urgency": 12.2,
"wait": null
}Lifecycle
A task has six statuses. Four are open — triage, backlog, pending and active — and two are closed: done and cancelled. Each arrow is labelled with the method that makes the move.
triage holds a proposal (D238): task.add with status: "triage" files one, and no clock moves it. It is outside @working, task.next and the urgency ranking, still blocks the tasks that depend on it, and task.start and task.done refuse it. task.triage settles it: accept moves it into the box below, landing wherever its dates say; decline and duplicate cancel it, as task.cancel may too. event.revert takes back an accept, a decline or a cancel, putting it back in triage, and task.reopen of a cancelled proposal returns it to triage rather than to the box.
task.add
│
status: triage │
┌───────────────────┤
v │
┌─────────────┐ │
│ triage ├─ accept ──>│
└─────────────┘ │
decline, duplicate: │
to cancelled │
┌─ open ─────────────────────┼─────────────────────────────┐
│ v │
│ ┌────────────────────────────────────────────────────┐ │
│ │ backlog while a future wait or scheduled date │ │
│ │ holds it │ │
│ │ pending otherwise — the clock and task.modify │ │
│ │ move a task between the two │ │
│ └────────────────────────────────────────────────────┘ │
│ │ ^ ^ │
│ │ task.start │ task.stop, or │ │
│ │ (pending only) │ another task.start │ │
│ v │ without keep (D6) │ │
│ ┌──────────────────────────┐ │ │
│ │ active │ │ │
│ └──────────────────────────┘ │ │
└────────────┬──────────────────────┬───────────────┼──────┘
│ task.done from │ task.cancel │
│ pending or active │ from any │
│ │ open status │
v v │
┌─────────┐ ┌───────────┐ │
│ done │ │ cancelled │ │
└─────────┘ └───────────┘ │
│ │ │
└──────────────────────┴───────────────┘
task.reopenbacklog and pending are one question asked of the clock every time a task is read: a task is in backlog while its wait or scheduled instant is still in the future, and in pending otherwise. Only those two statuses are read against the clock: active, done and cancelled keep their status whatever the task's dates say. So every arrow into that box lands in whichever of the two the task's dates say — a task added, stopped or reopened with a future date is in backlog, and moves to pending only once neither its wait nor its scheduled is still in the future — each has passed or been cleared with task.modify. There is no separate waiting status. task.start and task.done refuse a backlog task. Starting a task without keep stops every task already running (D6), with one exception (D140): when the start names an actor and the most recently started running clock was started under a different named actor, the start is refused instead. A start that names no actor, as tasqx start sends none, auto-stops as usual; the MCP server fills in its connection's id when a call names none. A running task under its own clock holder's live claim is never stopped by anybody else's start (D236). When the newest event is a task.stop, event.revert takes it back, putting the task back to active if it is still pending. task.modify refuses a status other than cancelled.
Blocked is not a status either: it is the blocked flag, true while an open task waits on a task that is neither done nor cancelled. @working is pending or active and not blocked, and an archived project's tasks are left out of it unless the filter names the project (D215). task.done refuses a blocked task unless force records the override (D150).
Urgency
urgency is one fixed formula (D1), the sum of three terms rounded to one decimal. The weights are not configurable.
| Term | Score |
|---|---|
| priority | H 6.0 · M 3.9 · L 1.8 · none 0.0 |
| due proximity | 12 once the due date is reached or past; before that 12 × (1 − days left ÷ 14), never below 0 — a due date more than 14 days away adds nothing. No due date, 0. |
| age | 0.01 per day since created, capped at 1.0. |
tasqx why prints the three terms for one task. The screen beside this is a task in the demo store: the terms, then their sum, then what still blocks it. task.get with explain: true answers the same terms as urgency_breakdown.
tasqx why 51#51 Write the migration guide for SDK 3.0 priority M 3.9 deadline due Mon 8.0 age 30 days 0.3 urgency 12.2 blocked by #50 Rate-limit the /search endpoint — `next` skips this task
Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
Project
A project is a name tasks and knowledge docs are filed under. One project can be the store's default, where a task added without a project lands. Archiving hides a project and removes nothing.
Fields
idstringalways- The project's uuid.
namestringalwaysinproject.list- The project's name — the value
project:filters on andtask.addtakes. namestringalwaysinstore.export- Its name — what a task's
projectnames. descriptionstringnullableinproject.list- The project's description, or null.
descriptionstringnullableinstore.export- Its description, or null.
archivedbooleanalwaysinproject.list- Whether the project is archived. Archived projects are omitted unless
include_archivedasked for them. archivedbooleanalwaysinstore.export- Whether it is archived.
defaultbooleanalways- Whether this is the project a bare
task.addlands in. createdstringalways- When it was created.
From project.list, at result/projects/0.
{
"archived": false,
"default": false,
"description": "Public REST API and its SDKs",
"id": "309d6b79-965e-4a32-9ae4-45508201e2bd",
"name": "api"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
Annotation
An annotation is a note on a task, stored verbatim. annotation.update corrects one in place, keeping its id, timestamp and position (D165), and store.import replaces an imported task's notes with the document's. annotation.remove scrubs a note's text and leaves a tombstone — its id and when it went — under annotations_removed; undoing the annotation.add that wrote a note deletes it outright, with no tombstone.
Fields
idstringalways- The note's uuid — what
annotation.removetakes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211). bodystringalways- The note, stored verbatim (D41).
task.getandtask.briefgivenmax_body_bytesreturn a prefix of a longer body and mark it withbody_truncatedandbody_bytes(D148). createdstringalways- When the note was written.
body_bytesintegeroptional- The body's real size in bytes. Present only on a body this response cut (D148).
body_truncatedbooleanoptional- Present and true only on a body this response cut, so "was this cut?" stays a presence question.
annotations_removed[].idstringalwaysintask.get·task.brief- The scrubbed note's id.
removed.idstringalwaysinannotation.remove- The scrubbed note's id.
annotations_removed[].removedstringalwaysintask.get·task.brief- When it was scrubbed. Never the text — that is the point of the method.
removed.removedstringalwaysinannotation.remove- When it was scrubbed. Never the text — that is the point of the method.
annotationsarrayalwaysintask.get·task.brief- The notes on this task, oldest first within the page. Pages are counted back from the newest note, so
annotations_offset: 0is the most recent page. annotationsarrayalwaysinstore.export- Every note on the task — an export carries the whole history, unpaged.
annotations_removedarrayalways- One tombstone per scrubbed note (D113) — id and instant, never the text. Excluded from
annotationsand its total. delivered_annotation_idstringnullableintask.get·task.brief- The note
task.donepinned as the delivery note — the newest one at the instant of completion — or null.task.reopenclears it (D165). delivered_annotation_idstringoptionalinstore.export- The note
task.donepinned as the delivery note (D165). Present only on a task a completion pinned one on. first_annotationobjectnullable- The oldest note, whatever page
annotations_limit/annotations_offsetasked for — the card's Description — or null when there is none (D165). delivered_annotationobjectnullable- The pinned delivery note — on a closed task with no pin, the newest note — or null on an open task, or when the pinned note was removed (D165).
From task.get, at result/annotations/0.
{
"body": "Ruling: the guide ships WITH the 3.0 release, not after it. A migration note that lands a week late is a support ticket that already happened.",
"created": "2026-09-11T11:20:00Z",
"id": "17dd6621-7db4-43b5-9f36-ddf89018081e"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
Check
A check is one acceptance criterion on a task (D138): a sentence tasqx stores and does not execute. It starts open, and check.set moves it to passed, failed or back to open, with the evidence that decided it.
Fields
idstringalways- The criterion's uuid — what
check.setandcheck.removetake. bodystringalways- The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalwaysopen,passedorfailed. A failure is a normal outcome, not an error.evidencestringnullable- The citation for the state, or null — some criteria are met by something nobody can quote.
positionintegeralways- Where it sits in the list, from zero.
createdstringalways- When the criterion was added.
modifiedstringalways- When its state or evidence last changed.
checksarrayalways- The acceptance criteria on the task, in position order (D138).
From task.get, at result/checks/0.
{
"body": "Every renamed symbol has a row in the table",
"created": "2026-09-04T11:30:00Z",
"evidence": null,
"id": "ebb86ee2-69ed-4938-b57c-c12a89e9414e",
"modified": "2026-09-14T09:10:00Z",
"position": 0,
"state": "passed"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
Dependency
A dependency is an edge between two tasks: this task waits on that one. It has no id of its own: the API reads it off the tasks at both ends — depends_on on the task that waits, blocks on the one it waits on. A task is blocked while it is open and at least one task it waits on is neither done nor cancelled.
Fields
depends_onarrayalways- The short ids this task waits on.
blocksarrayalways- The short ids this task is holding back — the reverse edge of
depends_on. blockedbooleanalways- Whether an unmet dependency is holding this task back.
unmet_blockersarrayalways- The open prerequisites, named — what
blocked: trueis blocked ON. unmet_blockers[].short_idintegeralwaysintask.get·task.brief- The blocking task's short id.
blocked_by[].short_idintegeralwaysintask.done- The blocking task's short id.
unmet_blockers[].titlestringalwaysintask.get·task.brief- The blocking task's title, so a refusal can be read without a second call.
blocked_by[].titlestringalwaysintask.done- The blocking task's title, so a refusal can be read without a second call.
From task.get.
{
"blocked": true,
"blocks": [],
"depends_on": [
50
],
"unmet_blockers": [
{
"short_id": 50,
"title": "Rate-limit the /search endpoint"
}
]
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
Memory document
A memory document is a piece of knowledge the store keeps beside the tasks: a title and a body, searchable together with the notes on tasks. It can be scoped to a project and marked standing (D156). Importing a doc with a source already stored replaces that doc in place; removing one is permanent.
Fields
idstringalways- The doc's uuid.
sourcestringnullableinmemory.get·store.export- Where it came from — the path
memory.importkeyed on, or null. sourcestringnullableinmemory.list- Where it came from, or null.
titlestringalwaysinmemory.get·store.export- The doc's title.
titlestringalwaysinmemory.list- Its title.
bodystringalways- The whole body, stored and returned verbatim (D41).
createdstringalwaysinmemory.get·store.export- When it was first stored.
createdstringalwaysinmemory.list- When it was stored.
modifiedstringalwaysinmemory.get·store.export- When it last changed.
modifiedstringalwaysinmemory.list- When it last changed — the column this list sorts by.
projectstringnullable- The project it is scoped to, or null.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. standingbooleanalwaysinmemory.get·store.export- Whether it is a standing ruling, meant for every session of its scope (D156). Stated on every doc, so a restore carries it.
standingbooleanalwaysinmemory.list- Whether it is a standing ruling (D156) — on every row, not only on a page filtered by
standing. origin_pathstringnullable- The absolute path of the file
memory.importread this doc from, or null when no import set one (D180). origin_mtimeintegernullable- That file's modification time in unix seconds when it was read, or null (D180).
origin_sizeintegernullable- That file's size in bytes when it was read, or null (D180).
stale_refsarraynullable- The paths and
path::symbolrefs the doc cites that are gone from the git toplevel of the caller'sworkdir; empty when none are, or none can be checked; null when noworkdirwas given (D235). body_previewstringalways- The opening of the body, or all of it when the body is short (
body_truncatedsays which) — browsing pays for a preview, not a payload. body_truncatedbooleanalways- Whether the preview is shorter than the body.
From memory.get.
{
"_rev": 0,
"body": "---\ndescription: How an SDK release is cut, tagged and announced\n---\nCut the release branch on Monday, tag after the canary has run for a day, and announce in the changelog feed once the packages are live.",
"created": "2026-09-05T09:15:00Z",
"id": "eb864f1e-e68a-4d96-af89-597bd0d2d52e",
"modified": "2026-09-10T09:15:00Z",
"origin_mtime": null,
"origin_path": null,
"origin_size": null,
"project": "api",
"source": "docs/release.md",
"stale_refs": null,
"standing": false,
"title": "release-process"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
Note
A note is a quick capture — an idea, a URL to read later, a line of brainstorm — kept apart from the work and the knowledge (D241). It is not a task, so it scores no urgency and stays out of @working and reports, and not a memory document, so search never offers it as knowledge. note.promote files one as a task or a memory document and archives it naming what it became. Its short id comes from a counter of its own; removing one is permanent.
Fields
idstringalways- The note's uuid.
short_idintegeralways- The note's own small number, from a counter tasks do not share — what
note.gettakes beside the uuid. bodystringalways- The whole body, stored and returned verbatim.
projectstringnullable- The project it is filed under, or null.
statusstringalwaysopenorarchived.promoted_tostringnullable- Where
note.promotefiled it —task:<uuid>ormemory:<uuid>— or null while it has not been promoted. Terminal: a promoted note stays archived, and a merge keeps the promotion from either side. createdstringalways- When it was captured.
modifiedstringalways- When it last changed.
_revintegeralways- The row's revision counter, bumped by every write. Send it back as
expected_revto make a change conditional. has_linksbooleanalways- Whether the body holds an
http://orhttps://link — derived on every read, the same testnote.list {links: true}filters on. promoted_task_short_idintegernullable- The short id the task
promoted_tonames holds in this store, read at the call so a renumbered task reads right; null when the note was not promoted to a task or the task is gone.
From note.get.
{
"_rev": 0,
"body": "idea: a dark-mode toggle in the docs header",
"created": "2026-09-15T16:05:00Z",
"has_links": false,
"id": "019f5a10-0000-7000-8000-00000000b002",
"modified": "2026-09-15T16:05:00Z",
"project": null,
"promoted_task_short_id": null,
"promoted_to": null,
"short_id": 2,
"status": "open"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
Link
A link is an explicit edge between two nodes of the knowledge graph (D160): a task, a memory document, an annotation or a project at either end, and a relation naming what it asserts. Links are written and removed, never edited, and a repeat of one that already exists answers with the link that is there rather than a second row. Unlike a dependency they may form cycles, and unlike an inferred graph edge they are durable: nothing mints one but a caller who asked for it. Removing a memory document removes its links with it; an annotation tombstone leaves them alone, because the node is still there.
Fields
idstringalways- The link's uuid — what
link.removetakes. fromstringalways- The node the link starts at, as
<type>:<uuid>:task:,memory:,annotation:orproject:. tostringalways- The node the link points at, spelled the same way.
relationstringalways- What the link asserts:
references,supersedes,implements_decision,derived_from,contradictsorduplicates(D238). metadataobjectnullable- The caller's own object, stored verbatim and never interpreted, or null.
created_atstringalways- When the link was written. Named
created_atbecauselink.addanswers with a booleancreatedbeside it.
From link.add.
{
"created_at": "2026-09-17T09:12:04Z",
"from": "task:019f7c0a-3d51-7c42-9a08-1f0c4e5b62d7",
"id": "019f8b31-77a4-7f10-8c55-2d7e9a13b004",
"metadata": null,
"relation": "implements_decision",
"to": "memory:eb864f1e-e68a-4d96-af89-597bd0d2d52e"
}Illustrative. a new link mints a fresh v7 id whose low bits are random, and the response also carries the task's own uuid — the pinned clock fixes the timestamp half of each and nothing fixes the rest, so a captured answer would differ on every run.
Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
| JSON API | CLI | MCP |
|---|---|---|
link.add | — | — |
link.remove | — | — |
link.list | — | — |
store.export | export · sync | — |
Graph
A graph projection is what graph.query hands back: the nodes around one root and the edges between them, bounded and deterministically ordered so the same call twice is the same picture. A node is a task, a memory document, an annotation or a project, wearing the id a link's endpoints use. An edge is either structural — read out of a table, and naming that table as its source — or inferred, computed for that one call from an FTS search on the root's title or from the tags two tasks share, carrying a confidence and stored nowhere. Nothing here is a row in the store: a projection is a view, and the durable edges it draws are links.
Fields
idstringalways- The node's stable id,
<type>:<uuid>— the same spelling a link's endpoints use, and what a follow-upgraph.querytakes as itsroot. typestringalways- Which kind of node:
task,memory,annotationorproject. labelstringalways- The one line to draw in the box: a task's title, a document's title, a note's first line (cut to 80 characters) or a project's name.
summarystringnullable- The second line:
#<short_id> · <status> · <priority>for a task, the source for a document, the owning task for a note, the description for a project. Null when a project has no description. projectstringnullable- The project this node is filed under, or null — including on a project node, which is not filed under itself.
statusstringnullable- A task's status, read the way every other surface reads it (a future
waitshowsbacklog). Null on every other kind of node. modifiedstringnullable- When the node last changed:
modifiedfor a task or a document,createdfor a note, which carries no modified time, so an edit does not move it. Null for a project, which carries no such column — so the date window cannot exclude one. short_idintegernullable- The small number a human types for a task. Null on every other kind of node.
taskstringnullable- The node id of the task a note is written on. Null on every other kind of node.
idstringalways- The edge's id, stable for the same edge across calls:
dep:,ann:,member:orlink:for a stored one,search:ortag:for a computed one. fromstringalways- The node id the edge starts at. A
depends_onedge always points from the task that waits to the one it waits on, whichever end the walk reached it from. tostringalways- The node id the edge points at.
relationstringalways- What the edge asserts:
depends_on,has_annotation,belongs_to_project, one of the five link relations, orsearch_match/shared_tagwheninclude_inferredasked for them. kindstringalwaysstructuralfor an edge read out of a table,inferredfor one computed for this call and never stored.confidencenumbernullable- How strong a computed edge is, 0 to 1 within this answer. Null on a structural edge: a stored fact has no confidence to report.
sourcestringalways- Where the edge came from: the table (
dependencies,annotations,tasks.project,docs.project,links) or what computed it (memory.search: <words>,tags: <shared>).
From graph.query, at result/nodes/0.
{
"id": "task:46773aad-c4aa-435a-abe1-fcde8ce09658",
"label": "Write the migration guide for SDK 3.0",
"modified": "2026-08-16T10:00:00Z",
"project": "api",
"short_id": 51,
"status": "pending",
"summary": "#51 · pending · M",
"task": null,
"type": "task"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
| JSON API | CLI | MCP |
|---|---|---|
graph.query | — | — |
Event
An event is one row of the audit log. Writes to tasks — their notes, checks, dependencies and measurements included — to projects and to memory documents append one. Events are not edited, with one narrow exception: annotation.remove redacts the body out of the annotation.add event that wrote the note, so a removed note's text does not survive in the log. event.revert rewrites nothing either: it undoes the newest event by appending its inverse, and refuses by name the ops it cannot reverse.
Fields
idstringalways- The event's uuid.
entitystringalways- What kind of thing it happened to:
task,project,doc,linkornote. entity_idstringalways- That thing's uuid.
opstringalways- What happened:
add,done,start,stop,tag.remove, … payloadobjectnullable- The op's own vocabulary. Null only for a row whose payload will not parse — a corrupt store.
tsstringalways- When it happened.
actorstringnullable- Who did it, when the caller said, or null.
From event.list, at result/events/0.
{
"actor": "user",
"entity": "project",
"entity_id": "73f778aa-f6fa-4db8-a56a-bd72fb710734",
"id": "a66b0d38-9d95-447e-bd29-9753a7677796",
"op": "create",
"payload": {
"default": false,
"description": null,
"name": "home"
},
"ts": "2026-06-18T00:00:00Z"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
| JSON API | CLI | MCP |
|---|---|---|
store.export | export · sync | — |
event.list | dashboard · chart | — |
event.revert | undo | — |
Git ref
A git ref is a branch, a PR or a merge commit a task was delivered through (D239). git.event records them from what a caller reports — tasqx never asks GitHub — and a task's refs are read under git_refs.
Fields
idstringalways- The ref row's id.
kindstringalwaysbranch,prorcommit.refstringalways- The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalwaysopen,closedormerged.mergedis final; a PR can move betweenopenandclosed.createdstringalways- When the ref was first recorded.
modifiedstringalways- When
statelast changed. Betweenopenandclosed, the later change wins when two stores merge. notedstringnullable- On a branch row, what
git.eventhas already done for that branch (JSON keyed per PR: the PRs that started the task, the fact its last note stated, the delivery note, the note it completed the task with), so a retry writes a missing note, never a duplicate, and never overrides the user. Null before any.
git_refsarrayalwaysintask.get·task.brief- The branch, PR and merge commit
git.eventrecorded on this task (D239), oldest first. git_refsarrayoptionalinstore.export- The branch, PR and merge commit recorded on this task (D239). Omitted entirely for a task that has none.
From task.get, at result/git_refs/1.
{
"created": "2026-09-13T14:05:00Z",
"id": "019f5a10-0000-7000-8000-00000000a002",
"kind": "pr",
"modified": "2026-09-13T14:05:00Z",
"noted": null,
"ref": "https://github.com/acme/api/pull/412",
"state": "open"
}Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.
| JSON API | CLI | MCP |
|---|---|---|
task.get | pick · show · why | tasqx_get_task |
task.brief | brief | tasqx_brief_task |
store.export | export · sync | — |
git.event | git | — |
Token measurement
A token measurement is AI token spend banked against a task: four buckets kept apart (D48), the tool and model that spent them, and how checkable the figure is. A task's measurements are read under tokens.
Fields
idstringalways- The measurement's uuid — what
token.removetakes. toolstringalways- Which tool spent the tokens (
claude-code,codex, …). sourcestringalways- How the figure was obtained:
self-report,log-parseorotel. modelstringnullable- The model that did the work, or null.
input_tokensintegeralways- Fresh input tokens.
output_tokensintegeralways- Output tokens.
cache_read_tokensintegeralways- Tokens read from cache — never blended with the others (D48).
cache_creation_tokensintegeralways- Tokens spent writing the cache.
confidencestringalways- How checkable the figure is:
high,mediumorlow. A self-report may not claimhigh. createdstringalways- When the measurement was banked.
total_tokensintegeralways- One unsplit count, from a reporter that could not split it — 0 on a split measurement, and never folded into the four (D167).
tokensarrayalwaysintask.get·task.brief- Every token measurement banked against this task.
tokensarrayoptionalinstore.export- Every token measurement against this task. Omitted entirely for a task that has none.
From token.add, at result/measurement.
{
"cache_creation_tokens": 12000,
"cache_read_tokens": 91000,
"confidence": "medium",
"created": "2026-09-16T09:00:00Z",
"id": "019f7c0a-3d51-7d22-b73c-9353de0a3360",
"input_tokens": 18400,
"model": "opus",
"output_tokens": 2600,
"source": "self-report",
"tool": "claude-code",
"total_tokens": 0
}Illustrative. the measurement's id is minted fresh, v7, half of it random.
Operations
Every method that acts on this object or answers with it, and the verb and tool that reach it.