tasqx user guide
v0.16.0

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

PrincipleWhat it means for you
FastOne-shot commands open SQLite, do the work, and exit. No daemon required, no warmup.
Local-firstYour data is one SQLite file on your disk. No account, no cloud, works offline forever.
One APIThe CLI does nothing you cannot do over JSON. Every verb below names the method it calls.
AI-nativeThe same typed API drives a bundled MCP server, so an agent is a first-class user.
HonestEvery 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.

Scroll sideways for the whole picture →
How the pieces fit The tasqx CLI, the report, plugins and the MCP server each send one JSON envelope over stdio; a TUI or GUI talks to the daemon over a socket or named pipe. Both paths reach the same dispatch layer, which calls storage, which writes SQLite (tasks.db plus its WAL) and the append-only events log. tasqx CLIreportplugins MCPTUI / GUI stdioone JSON envelope socket / named pipedaemon dispatchstorage SQLitetasks.db + WAL eventsappend-only log

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

WhatWhere
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)
Note

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.

SettingHomeDefaultWhat it does
theme.nameconfig.tomlnordTerminal theme: a built-in (nord, gruvbox, dracula, solarized, mono) or a user file.
dashboard.enabledconfig.tomltrueOpen the dashboard when tasqx is run bare on a terminal (D58).
dashboard.panelsconfig.tomltasks,projects,burndown,pulse,effort,tokensPanels the dashboard draws; membership is visibility, position is focus and tab order (D58).
dashboard.refreshconfig.tomlautoWhether the dashboard re-reads on a timer, or only on r (D58).
dashboard.windowconfig.tomlweekThe burndown window the dashboard opens on; w cycles it live (D58).
notify.enabledconfig.tomlfalseAllow the daemon to raise native OS notifications for reminders.
tokens.enabledconfig.tomlfalseAllow the daemon to attribute AI token usage by parsing local tool transcripts (#17).
otlp.enabledconfig.tomlfalseRun a local OTLP/HTTP receiver in the daemon to ingest token telemetry from AI tools (#18).
otlp.portconfig.toml4318TCP port the OTLP receiver binds on 127.0.0.1 (OTLP/HTTP convention: 4318).
board.portconfig.toml0TCP 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_timeoutconfig.toml0Minutes with no clients and no work after which tasqx daemon exits by itself; 0 stays up until Ctrl-C (D5).
default_projectthe store—Project a bare tasqx add files into. Lives in the store; set it with tasqx use.
mcp.add_statusconfig.tomlpendingStatus a task added over MCP takes when the call names none: triage holds agent proposals out of @working until tasqx accept (D238).
detail.time_formatconfig.tomlbothHow 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/tasqx

Windows, with Scoop:

$
scoop bucket add tasqx https://github.com/dimitritholen/scoop-tasqx
scoop install tasqx

With the installer script

macOS and x86-64 Linux:

$
curl -fsSL https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.sh | sh

Windows (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 | iex

Prebuilt 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 --force

Requires 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 --force

Sixty 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 project

Now 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 4h

The 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   +docs

Every 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   +ops
Careful

Flags 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        +docs

The 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 next
next    #1  Ship the v1 JSON API freeze
        H ▄▄▄▄ 15.7   work.tasqx   due Fri   +api +release
        tasqx start 1  ·  tasqx why 1

And 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.7

Work 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 4h

Where to go next

If you want to…Read
Know every verb and flagCommands
Ask precise questions of your storeFilter grammar
Say "friday" or "every 3 days"Dates & recurrence
Be told about a task before it is lateReminders
Give an AI agent accessMCP
Script tasqx from another languageJSON 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:00

Predicates

PredicateMatches
+apiTasks tagged api.
-apiTasks not tagged api.
project:work.tasqxExact project match.
proj:workThe same as project:.
priority:highExact priority: H, M, L, or high, medium, low.
status:pendingExact status: triage, backlog, pending, active, done, cancelled. backlog is the date-held backlog; a proposal in triage is never in it.
status:anyEvery status, done and cancelled included; also spelled status:all. Overrides a default that hides closed tasks.
weekly reviewFree 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.
@workingStatus pending or active, and not blocked. The default filter.
@blockedOpen, 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.
@claimedUnder 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/or are ordinary words, so a project named a (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" +paint

Combining

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  +ops

Two 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.

Careful

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/tasqx

Windows, with Scoop:

$
scoop bucket add tasqx https://github.com/dimitritholen/scoop-tasqx
scoop install tasqx

Or without one — Linux and macOS:

$
curl -fsSL https://raw.githubusercontent.com/dimitritholen/tasqx/main/install.sh | sh

Windows (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 | iex

Prebuilt 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))) -DryRun

The 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-latest provides. 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 folder
  • tasqx add Buy milk: capture a task (lands in the default project)
  • tasqx next: the one thing to do now
  • tasqx done 1: complete it
$
tasqx init work
tasqx add Buy milk
tasqx next
tasqx done 1

That'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. init creates no folder and touches nothing on disk except the task store. The first project you create becomes the default, so bare tasqx add knows 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 !high sets a due date, a tag and a priority in one line. See Adding and Editing Tasks.
  • A bare tasqx in 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.

CommandWhat it does
tasqx manualTable of contents
tasqx manual addEverything about add
tasqx manual filtersThe 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.

CommandWhat it does
tasqx docsOpen in the browser
tasqx docs --out guide.htmlWrite 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 about

The 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 -h

Where 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 work is all the setup you need.
  • Creating a second project does not move the default. Use tasqx use for 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 projects marks the current default with a *.

tasqx projects

List your projects.

CommandWhat it does
tasqx projectsThe live ones, with the default marked *
tasqx projects --allInclude 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-remodel

Things to know before you archive:

  • Once archived, no command will accept the project's name anymore — you can't use it, and you can't add or modify a task into it.
  • If you archive the project that is your default, the default is cleared: a bare tasqx add then has no home until you run tasqx use again. The command tells you when this happened.
  • Tasks in an archived project are left out of tasqx next and of the @working set — 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, report and agenda still show them. The rule applies to any filter that mentions @working, so @working or +x also hides a pending +x task in an archived project; leave @working out of the filter to see it.
  • tasqx unarchive is the way back. tasqx undo does 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 use does 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 milk

The title can carry inline shortcuts ("sugar"), so one line captures everything:

$
tasqx add Ship the release due:friday +api !high est:4h
ShortcutMeaning
+apiadd the tag api
project:work (or proj:work)file it under work
!high / !med / !lowpriority
due:fridaydue date — natural language is fine
scheduled:friday (or sched:)when you can start — parks the task in backlog until then
wait:mondayhide 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:-1hremind 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:

CommandWhat it does
tasqx modify 42 due:friday !highSet fields
tasqx modify 42 Fix the login bugBare 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 200000

It 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.

CommandWhat it does
tasqx tag 42 api releaseTwo tags, one command
tasqx tag 42 +apiThe 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 api

All 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 quote

Annotations 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.md

On 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 Friday

The 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 57

The 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 2

This 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).

CommandWhat it does
tasqx listThe working set
tasqx list project:work +apiNarrowed
tasqx list due.before:fridayDeadline pressure only
tasqx list --sort due --limit 20Order by a key (-due for descending, repeat or comma-separate several); the default is -urgency
tasqx list --json --fields short_id,title,dueReturn only those fields; use it with --json, the human table is not projected
tasqx list --holder meChoose 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.

CommandWhat it does
tasqx agendaThe next 14 days
tasqx agenda --days 3Just 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 --days that reaches the furthest one, or tasqx list when it is further out than --days goes.
  • 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 --ascii

When 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:

FilterMatches
project:workIn a project
proj:workThe same as project:
status:pendingBy status: triage, backlog, pending, active, done, cancelled
priority:highBy priority: H, M, L, or high, medium, low
+apiHas a tag
-apiDoes NOT have a tag
due.before:fridayDue before a date
due.after:mondayDue after a date
completed.before:fridayFinished before a date
completed.after:mondayFinished after a date (pair it with status:done)
weekly reviewTitle contains every word, any case
'"memory explorer"'Title contains the phrase
title:reviewThe same, spelled as a key
status:anyEvery status, done and cancelled too (status:all is the same)
@workingPending 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
@blockedOpen, with a dependency that is not yet done or cancelled
@claimedUnder 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 3

You 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 show per 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 --keep

By 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-2
  • tasqx claim 42 --holder builder-2 --lease 2h: renew it for two hours
$
tasqx claim 42 --holder builder-2

While 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-2

An 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-2

tasqx stop

Alias: st

Pause an active task. The tracked time is kept.

$
tasqx stop 42

While 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 --outcomes calibration can be fixed after the fact. A correction that would take the total below zero is refused.
  • show prints the net of every correction beside the total: tracked 2h30m (adjusted -2h25m).
  • tasqx undo takes 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 --force completes it anyway; the override is recorded on the completion and tasqx report --outcomes counts it under FORCED. Cancelling a blocked task never needs the flag.
  • Report what the work cost with --tool, --model and --input-tokens, --output-tokens, --cache-read-tokens, --cache-creation-tokens (or one unsplit --total-tokens). Any count records a measurement; --tool and --model alone still land on the completion.
$
tasqx done 42 --tool claude-code --model claude-opus-5 --input-tokens 4200 --output-tokens 900

tasqx 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 42

If 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 42

If 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 43

A 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 triage

lists 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 12

Makes 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 7

Cancels 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.

CommandWhat it does
tasqx pickBrowse the working set
tasqx pick project:workNarrow the candidates first
KeyWhat it does
j/kmove, 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
enteropen that task's card, the one tasqx show prints (Esc goes back)
sstart the task under the cursor, from the list or from its card
qleave; 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 undo

undo is deliberately narrow, and honest about it:

  • Twelve operations are undoable: done, cancel, modify, tag, stop, untag, undep, annotate, annotate --edit, annotate --move, adjust and accept. Everything else is refused by name, with the command that does take it back — undoing a start is tasqx stop, undoing an archive is tasqx unarchive.
  • Undoing a done puts 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's checks_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, and undo does not retract one.
  • Undoing a cancel puts back the status it left. Undoing a modify puts back every field it changed, and the answer lists them, so a cleared project is back and due is cleared again. Undoing a tag takes off only the tags that call attached, not one the task already had.
  • A modify or tag written 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 1

Task 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 1

If 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 schema
  • tasqx 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 2

Now 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

FormWhat you can write
Absolute2026-07-20, 2026-07-20T17:00, "2026-07-20 17:00", any RFC3339
Relative daystoday, tomorrow, yesterday, now
Boundarieseom / "end of month", eow / "end of week" (ISO week ends Sunday)
Weekday namesmonday..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 spansin 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
Times17: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

SituationResolution
A date with no time00: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 todayThe next one — seven days out, not zero.
A naive date with a timeA UTC clock, whatever your zone. An explicit offset (+02:00) is honoured and converted to UTC instead.
The literal nowThis 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.
Careful

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 -2h
error [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   +ops

The four date fields

FieldWhat it saysEffect
dueWhen it must be finishedThe 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)
scheduledWhen you plan to startA future value parks the task in the backlog, out of the @working set, until it passes
waitHide it until this momentSame parking, different intent: "not my problem yet"
remindWhen to nudge youFires 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:soon
error [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:

CommandWhat it does
tasqx add Call the bank due:"friday 9am" remind:-30m30 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

FormExampleBehaviour
Offset from due-1h, -30m, -2d, +15mStays symbolic in the store. Move due and the reminder moves with it.
Absolute instant"friday 9am", 2026-07-20T17:00Resolved 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         2
Note

Notice 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 store
tasqx daemon: listening on tasqx-remdemo (Ctrl-C to stop)
tasqx daemon: store ~/.local/share/tasqx/tasks.db

Silence — 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.

RuleExample
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 brief on 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 months can drift across short months; anchor with monthly on day 15 when 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         1

Missed 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:

RuleFrom Jan 31Why
monthly on day 31Jan 31 → Feb 28 → Mar 31Re-clamps against the stored target day every step, so month-end recovers.
every 1 monthJan 31 → Feb 28 → Mar 28Advances from the previous (already clamped) date, so it drifts and stays there.
Note

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.

CommandWhat it opens
tasqx pickbrowse tasks, search them, read one, start one
tasqx dashboardthe overview, and what a bare tasqx opens
tasqx config editsettings, previewing a theme as you move over it
tasqx memory listyour docs, with the one under the cursor beside them
tasqx notesyour notes: read, archive, promote or remove one
tasqx watcha 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).

CommandWhat it does
tasqxOn a terminal, a bare tasqx opens the dashboard
tasqx dashboardThe same screen, spelled out
tasqx --json dashboardThe panel data as one JSON document, no screen
  • It's read-only, with two exceptions: p opens the task browser, pick. Enter there reads a task, and s starts it and brings you back. N opens 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": true under --json), and the header counts them (3 blocked).
  • --json is not the screen in text. The document carries a payload for four of the six panels — tasks, projects, burndown and tokens — beside the status header it always writes and a panels array naming what you asked for. pulse and effort are drawn from the screen's own model, so tasqx --json dashboard --panels pulse,effort is 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 list shows 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.

CommandWhat it does
tasqx daemonIn one terminal: the server
tasqx watch project:workIn 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 nope
tasqx 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 3
4 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=3

Leave 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.

CommandWhat it does
tasqx boardPrint the board's URL and open it in your browser
tasqx board --no-openPrint the URL only
tasqx board --port 8123Serve on a fixed port (or set board.port)
tasqx board --scope readServe 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 onFromWhat runs
Activeanytasqx start (the running task stops, as it always does)
Doneanytasqx done
ReadyActivetasqx stop
ReadyDonetasqx reopen
ReadyBacklogclears its wait and scheduled dates
BacklogReadywaits a week (change the date with tasqx modify)
Blocked—refused: a task is blocked by its dependencies, not by a drag
anyTriagerefused: 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?

ModeWhenWhy
One-shotThe default. Scripts, cron, the HTML report.No process to manage. Open the DB, run one command, exit.
DaemonLong-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.

PlatformDefault
WindowsThe 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-docsdemo
tasqx daemon: listening on tasqx-docsdemo (Ctrl-C to stop)
tasqx daemon: store ~/.local/share/tasqx/tasks.db

It 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   +docs

Reports 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.

CommandWhat it does
tasqx reportTotals
tasqx report projectGrouped by project
tasqx report statusGrouped by status (or priority)
tasqx report +urgentAny filter narrows the scope

The same numbers as one self-contained HTML file:

$
tasqx report --html --out review.html

The 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
SectionWhat it answers
The bandDone, 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
StandupDone 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
ProjectsOpen, 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 flowAdded and done per 7 days since the store started, the net change, and the open backlog under it
OutcomesHow tracked time compared with the estimate, and what was reopened, cancelled or completed unproven — the same counts report --outcomes prints
Find a taskA 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 report
PROJECT               COUNT         EST  OVERDUE     TRACKED        TOKENS
home                      1        PT0S        1        PT0S             -
work.tasqx                3     PT5H30M        1        PT0S   cacheR 1.2M
$
tasqx report status
STATUS                COUNT         EST  OVERDUE     TRACKED        TOKENS
pending                   4     PT5H30M        2        PT0S   cacheR 1.2M

The 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.html
Wrote self-contained HTML report → review.html

Without --out it writes to stdout. One small inline script does the clicking; the page reads fully without it.

Note

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 --outcomes
PROJECT  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.
ColumnWhat it counts
REWORKCompletions that were later reopened
FORCEDCompletions that overrode open blockers
SILENTCompletions carrying no annotation
UNBRIEFEDCompletions of a task with no tasqx brief, and no tasqx_start_timer that carried memory, on record
CALIBMedian tracked-over-estimate, and how many completions had both
DROPPEDWork that was started and then cancelled
OVERCompletions that went past their token budget, of those that had one
UNPROVENCompletions with a check still open or failed, of those that had checks
TOKENSThe 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.

CommandWhat it does
tasqx chart throughputTasks added vs done, per week
tasqx chart heatmapGitHub-style activity calendar
tasqx chart burndownOpen 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 30 widens 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:work

only 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 12
Weekly 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 up
$
tasqx chart heatmap --weeks 4
Completions - last 4 weeks   . 0  : 1-2  + 3-4  # 5+
  Mon . . . . 
      . . . . 
  Wed . . . . 
      . . . : 
  Fri . . . . 
      . . . . 
  Sun . . . . 
  > 1 done - current streak 1 days - best 1
$
tasqx chart burndown --days 7
Remaining open - all tasks
    4  ______#
    0  2026-07-10 -> 2026-07-16
  > 4 left - up 4 over 7 days
ChartFlags
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"
  • --standing marks 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, add answers with a hint to merge or retract.
  • --source names where the doc came from, and names one doc: a source another document already holds is refused with conflict (exit 5), naming that document, so import always knows which one to replace. The same goes for update --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/k move, / searches as you type, Enter reads the document beside the list, q leaves.
  • 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.
  • --standing lists only the standing docs (and prints the table).

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 D41 or #607 has no word to match by meaning, so it is looked up by its characters alone. --mode lexical does that for any query: exact words only, the way search worked before meaning was added. --mode semantic is 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 docs or --scope annotations searches only one half (default all). --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 --raw for 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 stale on its line, in memory search and in the FROM MEMORY section of brief, so you can tell a ruling that is behind its file from one that still matches it. --json has it as "stale": true. memory list and memory show do 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-6f1b0e5c8a12

tasqx memory update

Correct a document in place, keeping its id.

$
tasqx memory update 019f8422-7b3e-7c41-a2d9-6f1b0e5c8a12 --body "corrected text"
  • --title, --body, --source, --project and --standing true|false each replace that field; omit one and it stays as it was.
  • Guarded by the same optimistic-concurrency rev tasqx modify uses: --expected-rev fails with conflict (exit 5) if the document moved under you since you read it.
  • This is the correction path, and the reason rm is 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_docs under --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 as unchanged (2 replaced, 3 unchanged; unchanged under --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-rev taken before the re-import is refused with conflict instead of silently overwriting the freshly imported text.
  • --project scopes 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 --standing on 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 source is 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 as docs/, ./docs/ or its absolute path, from any directory, is one document per file, not several, and two repos' README.md are 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 a note: 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 unix docs/a\b.md is its own file name, never a spelling of docs/a/b.md. A symlink pointing at another file in the same import collapses onto it too — same source once resolved — so only the real file becomes a document, and the alias is named in a note: line instead of refusing the batch.
  • tasqx memory import --refresh takes 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 own initialize when 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) and origin_size (its size in bytes), all shown by memory show --json and carried by export. They say what the file looked like when it was read, so a later check can tell whether it has changed since; identity is still source, and memory add never 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-6f1b0e5c8a12

A 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 as ok.

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-6f1b0e5c8a12

This 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
  • --project files it under a project. Without it the note has none. There is no project: shortcut as add has: 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 3

Change 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 rm
  • edit replaces the text.
  • archive puts a note out of sight; it stays under tasqx notes --archived. unarchive brings it back.
  • rm deletes it for good. tasqx undo does 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 notes
2 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.

  • --links shows only notes holding an http:// or https:// link.
  • --archived shows the archived ones, promoted notes among them. --all shows open and archived together.
  • --search <text> keeps notes containing that text, ignoring case. It is a plain match, not the ranked search memory search does.
  • --project <name> keeps the notes filed under that project.
$
tasqx notes --links
tasqx notes --search exporter --all

The 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.

KeyWhat it does
j / kmove down / up (the arrows too)
g / Gfirst / last note
/search the notes' text, every word you type
Enterread the note whole; a link is shown in full and never opened
Escclear the search; with none, leave (in a note, back to the list)
qleave (in a note, back to the list)
aarchive the note, or bring an archived one back
tpromote it to a task
mpromote it to a memory doc
xremove it for good, after you answer y
lshow 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

QuestionNoteTaskTriage proposalMemory doc
Fora thought you might act onwork you will dowork someone suggestsknowledge to keep
Urgency, @working, reportsnoyesnot until acceptedno
Found by memory searchnoits annotationsits annotationsyes
Becomes something elsepromote—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 setup

It'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 item
  • tasqx setup --yes: install everything not yet present, no screen
  • tasqx setup --only retro: limit to one item (mcp, tasqx-workflow, retro); repeatable. Codex and Gemini items carry the tool's name: codex:mcp, gemini:retro
  • tasqx 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 default
  • tasqx mcp serve --scope write: explicit write access
$
tasqx mcp serve
tasqx mcp serve --scope write

To wire it into Claude Code by hand, without the skills that tasqx setup adds:

$
claude mcp add --scope user tasqx -- tasqx mcp serve --scope write

By 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 write

Any 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_task returns 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. next is tasqx next over MCP: the most urgent unblocked task, scoped by project and filter, 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. Like get_task, it answers the rendered view, and include_json: true adds the machine block.
  • A task with its criteria and prerequisites, in one call. add_task takes checks (strings) and depends_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_dependency and 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. outcomes is 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_memory gives even a read-only agent retrieval by meaning and by words over your imported docs and every task annotation — feed it your ADRs with tasqx 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_task takes token counts plus who spent them — or total_tokens, one unsplit number, when that is all the harness reports — and add_tokens records 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.
  • initialize answers with instructions, 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 directory mcp serve runs in (or one of its parents), else the default project, within a 3 KB budget.
  • A write-scoped initialize also 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, and instructions names 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_tasks answers a compact row by default — short_id, title, status, priority, urgency, blocked, due, project, tags — with any null key omitted; an explicit fields list, [] included, returns the engine's own full row.
  • tasqx_complete_task refuses a task with open blockers and names them; force: true completes it anyway, and the override is recorded and counted in tasqx_outcomes. Pass view: "card" when a person has to decide on the outcome: the answer then leads with the box card of the task as completed, so no tasqx_get_task re-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_task response with a marker naming its real size; max_body_bytes raises the cap, and a removed annotation is listed as a tombstone rather than disappearing from the count.
  • tasqx_brief_task holds half its memory page for knowledge docs (from memory add / memory import), so a ruling is not buried under a project's own, more numerous, task annotations.
  • tasqx_brief_task pages the task's annotations the way tasqx_get_task does: the newest 20 by default, annotations_limit and annotations_offset to choose another page. A brief over the response budget drops memory hits first, then older annotations, and the view names the annotations_offset that 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_limit and include_json spend the same budget. A tasqx_get_task response is fitted to a byte budget, and the machine-readable JSON block is charged first. A big annotation page together with include_json: true therefore buys less history than the page you asked for.
  • budget_tokens stops 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_timer takes actor and keep. 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. Pass keep: true to run both deliberately — which is what several agents working in parallel need.
  • tasqx_claim_task is how parallel agents stay off each other's work. A claim is a lease, 30 minutes unless lease names another length and at most 24 hours, held by the connection (or the actor you name). While it is live the task leaves every other connection's @working and tasqx_next, their tasqx_stop_timer, tasqx_complete_task and tasqx_cancel_task on it need force_claim, and their tasqx_start_timer on 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 stable actor (its session id) to claim, start, complete, cancel and release; a claim stranded by a dead session is freed by force_claim: true on tasqx_release_task or by its lease running out.
  • tasqx_remove_annotation redacts 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 undo cannot bring it back.
  • tasqx_update_annotation corrects 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 undo puts the previous text back.
  • tasqx_move_annotation moves a note to another task. For a note written on the wrong task: the id, text and timestamp are kept, search follows, and tasqx undo puts it back. tasqx_update_annotation and tasqx_remove_annotation take 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_task with status: "triage" files a task outside @working, next and the urgency ranking, so work an agent discovers mid-task does not compete with the planned backlog; discovered_from links it to the task it was found on. Set tasqx config set mcp.add_status triage and every MCP add that names no status lands there. tasqx_triage_task (or tasqx accept, decline, duplicate at a shell) settles each one; tasqx triage lists 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 api

Method 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
  1. 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.
  2. Live pushes. Every change is announced to subscribers — that's what tasqx watch listens to.
  3. 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:4318

Gemini 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.

CommandWhat it does
tasqx tokens add 602 --total 37898Record a count that arrived after completion, as one unsplit number
tasqx tokens add 602 --in 18400 --out 2600The same, split into buckets
tasqx tokens recomputeDry run: shows what would change, writes nothing
tasqx tokens recompute --applyActually 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 it
  • tasqx 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.add in the API) is kept only when both of its ends are in the document, and the rest are counted under dropped_links for 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_links like 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): sharing tasqx export project:ledger no 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 document tasqx import would refuse.
$
tasqx export project:work --include-unscoped > work-and-global.json

With 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.json
note: 1 dependency points outside the export, left out — widen the filter to keep it
Note

The 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.

Note

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.json
imported   2 tasks · 1 project · 3 memory docs
$
tasqx export +api | TASQX_DB=/tmp/other.db tasqx import -
imported   1 task · 1 project · no memory docs
Note

The 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-run

tasqx 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 _rev check 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.json

Run 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.json

Copy a.json to machine B, then there:

$
tasqx import a.json --merge --dry-run
tasqx import a.json --merge
tasqx export > b.json

Copy b.json back to machine A and do the reverse:

$
tasqx import b.json --merge --dry-run
tasqx import b.json --merge

Both 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

WantDo
Back uptasqx export > backup.json
Version your tasks in gittasqx export > tasks.json && git commit -am wip — canonical output means clean diffs.
Move to another machinetasqx export > all.json, copy, tasqx import all.json
Hand one project to a colleaguetasqx export project:work.tasqx > slice.json — mind the edge note.
Query beyond the filter grammartasqx 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 sync

Nothing 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.

  1. Create a bucket. In the Cloudflare dashboard, under R2, create a bucket. tasqx does not create it for you.

  2. 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.

  3. Find your account ID. It's on the R2 overview page: 32 hexadecimal characters.

  4. Run setup:

    $
    tasqx sync setup r2

    You'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.tqx inside the bucket.

  5. 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 the tasqx-remote-* connectors found on your PATH, 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 write tasqx sync setup makes — there's exactly one way "set up" happens either screen uses.
  • s — sync now: runs the same sync tasqx sync runs 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 --status

tasqx 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 sync

Limits

  • 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 sync runs 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 sync is a client of the same dispatch every other command uses, so with tasqx daemon running 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

CommandWhat it does
tasqx config listEvery setting: value, source, default
tasqx config get theme.nameOne resolved value
tasqx config set theme.name gruvboxWrite it (your file comments survive)
tasqx config unset theme.nameBack to the default
tasqx config pathWhere config.toml lives
tasqx config storeWhich task store you'd be writing to
tasqx config editInteractive full-screen editor

Worth knowing:

  • Resolution order is: command-line flag, then $TASQX_* environment variable, then config.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 edit opens 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 use set/unset.
  • config store answers which SQLite file a command would actually write to, and why: through a running daemon, or in-process — including the case where your $TASQX_DB names 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.

SettingDefaultWhat it does
theme.namenordThe terminal theme (below). Also --theme and $TASQX_THEME
dashboard.enabledtrueA bare tasqx on a terminal opens the dashboard; false prints the list instead. $TASQX_DASHBOARD overrides it
dashboard.panelstasks,projects,burndown,pulse,effort,tokensWhich panels the dashboard draws, in tab order
dashboard.refreshautoauto re-reads on a timer, manual only on r
dashboard.windowweekThe burndown window it opens on (week, 14d, 30d); w cycles it
notify.enabledfalseLets the daemon raise OS notifications for reminders; needs a build with the notify-os feature (Reminders)
tokens.enabledfalseLets the daemon attribute AI token use from local tool transcripts
otlp.enabledfalseRuns a local OTLP/HTTP receiver in the daemon for token telemetry
otlp.port4318The port it binds on 127.0.0.1
board.port0The 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_timeout0Minutes of idleness after which tasqx daemon exits; 0 stays up
default_projectnoneWhere a bare tasqx add files; kept in the store, set with tasqx use
mcp.add_statuspendingStatus of a task added over MCP when the call names none; triage holds agent proposals until tasqx accept
detail.time_formatbothHow show and the MCP task view write timestamps: iso, relative or both

tasqx theme

CommandWhat it does
tasqx theme listBuilt-ins + your own theme files
tasqx theme show nordPreview a theme's colors and roles
tasqx theme set nordPersist 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
  mono

The * 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.

Note

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.

Note

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:

VariableEffect
NO_COLORSet to anything: drop all colour, keep bold/underline. Wins over everything below.
CLICOLOR_FORCESet to anything but 0: force colour even through a pipe — for less -R and CI logs.
TASQX_FORCE_COLORSet to anything: same as CLICOLOR_FORCE, scoped to tasqx.
COLUMNSHow 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
CommandWhat it does
tasqx completions <shell>print the line for your startup file
tasqx completions bash >> ~/.bashrcadd 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 | source

elvish, 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_COMPLETE

Platform 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 >> $PROFILE

And 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 — reopen and why need the closed ones.
  • An alias only surfaces when no canonical name claims the prefix: ls<TAB> gives ls, mod<TAB> gives modify.

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 product
  • tasqx init myapp.checkout: the feature
$
tasqx init myapp
tasqx init myapp.checkout

Capture 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 #1
  • tasqx 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 2

Put 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 task
  • tasqx done 1: prints #2 unblocked - <title> under the done line for each task it released
$
tasqx next
tasqx start 1
tasqx done 1

If 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:

CommandWhat 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

CommandWhat it does
tasqx modify 42 due:mondayPush it out, reminder moves along
tasqx cancel 17Reversible, kept in history
tasqx reopen 17Changed 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

CommandWhat it does
tasqx list "status:done completed.after:-7d"What got finished
tasqx report statusOpen vs done; cancelled stays out
tasqx chart heatmapCompletion 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 output
  • tasqx 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

CommandWhat it does
tasqx reportPer-project: count, estimates, overdue, tracked, tokens
tasqx report statusThe same, grouped by lifecycle state
tasqx chart throughputAdded vs done per ISO week
tasqx chart heatmapCompletion density, calendar-style
tasqx chart burndownRemaining 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.html

One 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 -30d

REWORK 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 setup

It'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 write

Any 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 table
  • TASQX_DASHBOARD=false tasqx: switches the screen off for the whole image instead
$
tasqx list
TASQX_DASHBOARD=false tasqx

The 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:

  1. tasqx_list_tasks with "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 pass fields for any other column, or fields: [] for the whole row.

  2. 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 the detail.time_format config key (iso, relative or both) decides how it writes timestamps. Pass include_json: true when 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_task is still the right call when you only want the task itself.

  3. 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 a tokens_hint in the response saying exactly that. Pass view: "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 no tasqx_get_task re-read is needed.

  4. 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_task first and tasqx_release_task when done: a claimed task leaves the other agents' next and @working, and @claimed lists what is held. See Working on Tasks.
  • Proposals. tasqx config set mcp.add_status triage makes a task an agent adds over MCP without naming a status a proposal that waits for your tasqx triage, accept, decline or duplicate. See Working on Tasks.
  • Duplicates. tasqx_add_task returns similar, the open tasks in the same project that read like the new one; look before adding another.
  • Notes. tasqx_add_note and 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 event starts 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 --list shows what is installed and --yes --only retro installs 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:

  1. tasqx_brief_task before touching payment code — the query is derived from the task, so nothing is guessed.
  2. 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_limit widens it.
  3. tasqx_search_memory with "payment idempotency" for anything the brief's scope did not reach.
  4. After the work, tasqx_add_memory stores 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 -30d

It 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_task is 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 a conflict and re-reads instead of clobbering. The other writes — complete, tag, annotate, timers, dependencies — take an optional expected_rev but 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 chart and 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:

  1. Did the user correct or redirect you?
  2. Goal versus delivered?
  3. Rework?
  4. A discovery written down nowhere?
  5. Would you redo anything?
  6. 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:

  1. The task read whole: tasqx_get_task with include_json: false and annotations_limit set to the task's annotations_total. A long history is still cut to the response budget; a non-null annotations_next_offset means there is more, so page until it is null.
  2. The executor's final report for that task, verbatim.
  3. tasqx_outcomes for the task's project.
  4. 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 retro

The 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

WhatWhere it landsHow to find it
What a task taughtA retro: annotation on that tasktasqx_get_task, or tasqx show <id>
Facts and conventionsMemory docstasqx_search_memory, or tasqx memory search
ProceduresTasks titled skill candidate: …, tagged skill-candidatetasqx 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 true

That 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-code

Log-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.

ChannelEarnsWhen
Telemetry (otel)highAlways: samples are matched to the task by session id
Log-parsehighAn explicit --transcript-path, with the completion's session id confirmed in the file
Log-parsemediumAn explicit path to a file with no per-session anchor (Gemini, Copilot)
Log-parselowNo path: the transcript was found by scanning and matched on time overlap alone
Log-parselowtokens recompute found the transcript gone, so it kept the counts and lowered the grade
Self-reportmediumEvery 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:4318

The 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 200000

It 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

CommandWhat it does
tasqx tokens recomputeDry-run: per-task delta, writes nothing
tasqx tokens recompute --applyWrite 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.

verbrequestreply
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_THEME and the config.
--socket<addr>optional
Socket / named pipe of a daemon. Overrides $TASQX_SOCK. Refused by api, mcp, chart and report --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.

VerbAliasesMethodWhat it does
init—project.createCreate a project — just a name, no folder.
use—project.useSet the default project for bare adds.
archive—project.archiveRetire a project — out of rotation, tasks untouched.
unarchive—project.unarchivePut an archived project back into rotation.
adda, newtask.addCapture a task — title plus inline sugar.
modifymod, m, edittask.modifyChange a task — set fields or --clear them.
listls, ltask.listList tasks matching a filter.
agendaag, caltask.listWhat is coming up, when — list ordered by time.
next—task.nextThe one highest-urgency unblocked task.
dashboarddashtask.list + report.summary + project.list + event.listOpen the overview screen, or ask for its panels as data.
pickp, fzftask.list + task.get + task.startBrowse tasks on a full-screen list: read one, search them, start one.
showgettask.getShow one task in full detail.
why—task.getExplain a task's urgency score.
startstask.startMark a task active.
stopsttask.stopPause an active task.
claim—task.claimClaim a task so other agents skip it.
release—task.releaseGive a claim up.
adjust—task.adjust_trackedCorrect a task's tracked time.
doned, x, completetask.doneComplete a task.
canceldelete, del, rmtask.cancelCancel a task.
reopen—task.reopenReopen a completed or cancelled task.
triage—task.triage_listList the proposals waiting in triage, oldest first.
accept—task.triageAccept a proposal from triage: it becomes pending.
decline—task.triageDecline a proposal from triage: it is cancelled.
duplicate—task.triageMark a proposal in triage a duplicate of another task.
undouevent.revertTake back the last thing this store recorded.
annotate—annotation.add + annotation.update + annotation.moveAttach a timestamped note to a task, correct one, or move it.
unannotate—annotation.removePermanently scrub one annotation's text.
tag—tag.addAttach tags to a task.
untag—tag.removeRemove tags from a task.
dep—dependency.addMake one task depend on another.
undep—dependency.removeRemove a dependency edge.
projects—project.listList projects (default marked with *).
brief—task.briefEverything needed before starting a task, in one read.
check—check.add + check.set + check.removeAcceptance criteria on a task: add, mark, drop.
git—git.eventLet a pull request start or complete its task.
report—report.summary + report.outcomesSummary counts, or outcomes, optionally grouped, as text or HTML.
chart—event.listRender 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.refreshStore and search knowledge: docs, patterns, and your task annotations (D41).
note—note.add + get/update/remove/promoteJot down a quick note — an idea, a URL to read later — outside the backlog (D241).
notes—note.listList your notes, newest first (D241).
tokens—tokens.recompute + token.addRecord token spend after the fact, or repair stored attribution (D50, D167).
export—store.exportDump tasks as canonical JSON.
import—store.importLoad tasks from a JSON file or stdin.
sync—store.import + store.exportSync 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 + pushLive-updating task view.
board—task.list + push + lifecycle verbsLive 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).
manualman— (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: @working alone)
--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 @working rather than replacing it: tasqx next project:work still skips blocked and backlog tasks in that project, unlike tasqx list project:work which 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:old reaches an archived old. tasqx unarchive old puts the whole project back.
  • --card prints the picked task as a fixed 72-column box-drawn card meant to be pasted into a document (a chat, a PR); --ascii draws its borders with + - |. The default screen is unchanged (D146).

See also list · why · start.

tasqx next project:work
tasqx next --card
$
tasqx next
next    #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 --json answers with, comma-separated (now,next,due,blocked,recent,projects,burndown,tokens). Narrows dashboard.panels for this call only, and only on the --json path — the interactive screen still reads dashboard.panels from 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.

KeyDoes
1-6focus a panel (or place it in the analytics slot)
scycle the list order: urgency, due, touched
tab / S-tabnext / previous panel
j / kmove the cursor in the focused panel
g / Gcursor to the first / last row
rrefresh now
Rtoggle auto-refresh
wcycle the burndown window
enteropen the row under the cursor
pbrowse the tasks: enter reads one, s starts it
Nbrowse your notes: a archives, t and m promote, x removes
lleave and print the task list
?this help
q / escclose
ctrl-cclose (inside pick, go back to the dashboard)
  • The same screen a bare tasqx opens on a terminal. Spelling it explicitly works even when dashboard.enabled is 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 tasqx in a window that small falls back to the working-set table instead, silently: whoever typed nothing did not ask for a dashboard.
  • --json skips both of those checks, because it opens no screen. It is the only verb where --json decides whether the terminal gate applies, and it is what makes the panel data reachable from a script.
  • --panels tasks,burndown narrows the --json document to those panels, on that one call — it does not touch dashboard.panels or the interactive screen. The task rows are row-capped per group with total/truncated alongside 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 mean tasks, which is where their rows went.
  • Read-only, with two exceptions: p opens pick, the task browser, over the working set — or, from a PROJECTS row, over that project. Enter there reads a task and s starts it, which brings you back here; q or esc there comes back having started nothing. N opens the notes browser tasqx notes opens, where a note can be archived, promoted or removed; closing it comes back here. q, esc and ctrl-c close the dashboard.
  • Every key (also behind ? in the screen itself): 1-6 focus a panel; s cycles the list order between urgency, due and touched; tab/S-tab cycle panels; j/k move the cursor; g/G jump to the first/last row; r refreshes now; R toggles auto-refresh; w cycles the burndown window; enter opens the row under the cursor; N browses your notes; l leaves and prints the task list.

See also list · pick · agenda · chart.

tasqx --json dashboard

the 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 s starts as this holder (D236). Default user, the shell

The keys are generated from the tables the screen's own key bar is drawn from.

In the list

KeyDoes
j / kmove down / up (the arrows too)
/search ids, titles, projects and tags
enterread the task under the cursor
sstart the task under the cursor, and leave
g / Gfirst / last task
q / escleave, starting nothing (back to the dashboard, when opened from it)

In the list, with a search kept

KeyDoes
j / kmove down / up (the arrows too)
/change the search
enterread the task under the cursor
sstart the task under the cursor, and leave
g / Gfirst / last task
escclear the search
qleave, starting nothing (back to the dashboard, when opened from it)

In the search

KeyDoes
enter / esckeep the filter and go back to moving
up / downmove while typing (ctrl-p / ctrl-n too)
ctrl-uclear the query (ctrl-w deletes a word)

On a task's card

KeyDoes
j / kscroll down / up
space / ba page down / up
g / Gtop / bottom
sstart this task, and leave
esc / qback to the list
  • Each row is the row tasqx list prints: 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 — as list's summary does.
  • j/k or the arrows move, g/G jump to the ends. Enter opens the task's tasqx show card; there j/k/space/b scroll and esc or q goes back to the list. s starts 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 rule tasqx start follows. q leaves.
  • / opens the search on the header line: a fuzzy SUBSEQUENCE match over id, title, project and tags, so wac finds Write 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 or esc goes back to the list with the filter kept. esc in the list clears the filter, and only then leaves.
  • Leaving without starting a task exits 0: pick is 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 survive q. A filter that matches no task still exits 4, and so does an empty working set: that is a request pick could 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 next answers the same question and tasqx start <ref> acts on it.

See also next · list · start · agenda.

tasqx pick project:work +api

narrow the candidates first

$
tasqx pick
 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   

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

  • describe prints a setting's meaning, unit and default — for daemon.idle_timeout the number get/list show is minutes, and the unit is otherwise only visible in the daemon's own startup banner.
  • store answers 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_DB names a different store than the daemon on the default socket serves (D204).
  • list shows both homes. Most settings live in config.toml; default_project lives in the store and is set with tasqx use (D21).
  • Resolution order is --flag, then $TASQX_*, then config.toml, then the built-in default (D9). The SOURCE column names the layer that won.
  • edit opens 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.
  • edit needs a real terminal. Piped or redirected it refuses and exits 2 rather than writing escape codes into your pipe — scripts should use set/unset (D26).
  • tokens.enabled allows the daemon to attribute AI token usage from local tool transcripts; otlp.enabled (+ otlp.port) runs a local OTLP/HTTP receiver instead — see tasqx manual reports and tasqx manual daemon. Both require a running daemon; self-reported counts (token.add) work with both off.

See also use · theme · manual.

tasqx config get theme.name
tasqx config describe daemon.idle_timeout
tasqx config path
tasqx config store
tasqx config set theme.name gruvbox

writes config.toml, preserving your comments

tasqx config edit

full-screen editor; arrow through themes and watch them apply

$
tasqx config list
SETTING              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 NAME writes ONE captured screen instead of the guide: a standalone page carrying the guide's terminal styling and nothing else, to --out or 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 docs

open the browser guide

tasqx docs --stdout
tasqx docs --screen list

one captured screen, standalone

Note

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 string tasqx --version prints — the crate version plus the commit — and reads unknown on 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 --json is declined with a note (D31's carve-out list, D127).
  • --notices prints 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 the NOTICE file the release archives carry, so an install that kept only the binary still has it.

See also manual · docs.

tasqx about
tasqx about --notices

the third-party licence texts

Note

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 docs is the fuller browser guide.

See also docs.

tasqx manual init
tasqx manual filters
$
tasqx manual
TASQX 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.

See also use · add · projects.

tasqx init keuken-verbouwen
tasqx init work --desc "Day job"
tasqx init home && tasqx use home

claim then set default

$
tasqx init research
research
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 projects marks the default with *.

See also init · projects · add · archive.

tasqx use keuken-verbouwen

move where a bare add lands

$
tasqx use api
api
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 --all still lists the project.
  • An archived project is out of rotation for WRITES — use refuses it (exit 5), and so does an add/modify that names it, and so does a second archive of it (project is already archived, exit 5). No write may name an archived project, this one included; store.import restoring the flag from a document is the one write that still can. Reads are unaffected: list, report and agenda still show it and its tasks — archiving is a rotation change, not a hide.
  • tasqx unarchive <name> is the way back (D215). undo does 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 next and of @working — the working set is what to do next — unless the filter names the project, as in tasqx next project:old. list, report and agenda are unaffected.
  • Archiving the project that IS the default clears the default: a bare tasqx add then has no project until tasqx use <project>. The line says which of the two happened.

See also projects · use · init.

tasqx archive keuken-verbouwen

retire it; archiving your default project clears the default

$
tasqx archive home
home
archived   2 open tasks left in it   default unchanged   tasqx list project:home
$
tasqx archive home
error [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.

See also archive · projects · use.

tasqx unarchive keuken-verbouwen

put it back among the projects use, add and next take

Note

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
  • --all is the only way to see an archived project: without it the table shows the live ones, which is what add and use will accept.

See also init · use · archive.

tasqx projects --all
$
tasqx projects
  PROJECT  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 @working until tasqx 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:

FlagSugarNotes
--project <p>project:, proj:Free-form dotted name.
--priority <p> / -p!high, !hH, M, L (or high/medium/low).
--tag <t> / -t+tagRepeatable.
--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> / -eest:, estimate:4h, 90m, 1h30m, 2d, or ISO PT4H.
  • Inline sugar: +tag, project:p (or proj:), !high, due:…, scheduled:… (or sched:), wait:…, repeat:… (or every:/recur:), remind:…, est:4h (or estimate:).
  • A bare add lands in the default project (tasqx use to change it).
  • --triage files the task as a proposal: it stays out of @working, next and the urgency ranking until tasqx accept (D238). --discovered-from links it to the task it was found on. Tasks an agent adds over MCP land in triage when mcp.add_status is triage.
  • 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 what export carries.

See also modify · use · list · next.

tasqx add Buy milk
tasqx add Ship it due:friday +api !high --project work
tasqx add Water plants repeat:"every 3 days"
tasqx add Call bank due:"friday 9am" --remind -30m

reminder 30m before due

tasqx add Retry the flaky e2e test --triage --discovered-from 42

a 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> --json reports 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.
  • --clear covers the steering fields only. modify 42 +api adds a tag; taking one off is tasqx untag 42 api.
  • title and status are absent from --clear on purpose: a task without a title is not a task, and lifecycle moves through start/done/cancel so their invariants hold. Naming a field in both a set and a --clear is a bad_request, not a precedence puzzle.
  • --expected-rev fails with conflict (exit 5) if the task moved on.

See also add · show · why · untag.

tasqx modify 42 due:friday !high est:4h

set fields

tasqx modify 42 --clear due --clear remind

clear 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.
  • --card prints a fixed 72-column box-drawn card meant to be pasted into a document (a chat, a PR); --ascii draws its borders with + - |. The default screen is unchanged (D146).

See also modify · why · annotate.

tasqx show 1
tasqx show 1 --card
$
tasqx 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.

See also stop · done · next.

tasqx start 1

single-active by default

tasqx start 1 --keep

keep others running

tasqx start 1 --client 'claude-code 2.1' --session-id $SID

record 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)

See also start · done · adjust.

tasqx stop 1
$
tasqx 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 --lease names another length, at most 24h. Claiming again as the same holder renews it. Without --holder the shell holds it as user; over MCP the holder is the connection. tasqx start --holder starts a task claimed under a named holder, the timer itself staying the shell's; next, list and pick take --holder to see that holder's claims as yours.
  • While the lease is live, a claim or start by anyone else exits 5 naming the holder and the expiry, and the task leaves their next, pick and list --holder. tasqx list @claimed lists 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. done and cancel end a claim; tasqx release gives one up.

See also release · start · next.

tasqx claim 12 --holder builder-2

hold #12 for builder-2 for 30 minutes

tasqx claim 12 --holder builder-2 --lease 2h

renew 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-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, under your holder.

See also claim.

tasqx release 12 --holder builder-2

builder-2 is done with #12 without finishing it

Note

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 --outcomes can 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 undo takes the newest one back. show prints the net of every correction beside the total: tracked 2h30m (adjusted -2h25m).

See also stop · undo · report.

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 --outcomes counts 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 --outcomes counts 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.

See also cancel · reopen · start.

tasqx done 1

completes; spawns the next recurrence if any

tasqx done 1 --force

complete it although a dependency is still open; the override is recorded

tasqx done 1 --client 'claude-code 2.1' --session-id $SID

close the interval an agent opened with the same ids

tasqx done 1 --tool claude-code --model claude-opus-5 --input-tokens 4200 --output-tokens 900

self-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/rm are aliases for cancel: the task keeps its history, stays in the event log, and tasqx reopen <ref> undoes it.

See also done · reopen.

tasqx cancel 1

a cancelled dependency releases its dependents (D11)

tasqx delete 1

same 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

See also done · cancel.

tasqx reopen 1
$
tasqx 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 triage

Takes no arguments of its own; the global flags still apply.

  • Each row names the tool that filed it — the client an MCP add records — and the task it was found on (--discovered-from, a derived_from link). Settle each with accept, decline or duplicate (D238). tasqx list status:triage is the same set as a table.

See also accept · decline · duplicate · add.

tasqx triage
$
tasqx triage
2 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 wait or scheduled still holds it in backlog. tasqx undo puts it back in triage. A task not in triage exits 5 (D238).

See also triage · decline · duplicate.

tasqx accept 12
$
tasqx 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 undo puts it back in triage; the reason note stays. A task not in triage exits 5 (D238).

See also triage · accept · duplicate.

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 --move would) and a duplicates link joins the two. tasqx undo does not take it back: the newest event is the link, which it refuses by name (D238).

See also triage · accept · decline.

tasqx duplicate 12 --of 7

cancels 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 undo

Takes 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, adjust and accept (D238). Every other one exits 5 naming itself and the verb that does take it back (start -> tasqx stop, archive -> tasqx unarchive).
  • Undoing a done puts 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 a cancel restores the status it left. Undoing a modify puts back every field it changed, and the answer lists them; a modify or tag recorded 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 undo event lands behind it, so tasqx chart and the audit trail read X happened, then it was undone.
  • There is no redo, so tasqx undo twice 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 2 where no such edge exists, or tasqx start on a task already running — so undo reaches 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 add or start, 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.

See also untag · undep · reopen · chart.

tasqx undo

reverses the newest event, and says which one

tasqx untag 42 api && tasqx undo

the tag comes back off the shelf

Note

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 --to instead of adding one; its id, text and timestamp are kept (maps to annotation.move, D211)
--to<task>optional
The task --move sends 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 bare tasqx annotate <ref> when stdin is a pipe. On a terminal a bare annotate <ref> opens $VISUAL or $EDITOR (else vi); an empty or unchanged file stores nothing. Empty stdin is refused. A note that is just - can no longer be written as an argument. --edit takes 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] position tasqx show prints 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), and tasqx undo puts 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, and tasqx undo puts the old text back.

See also show · modify · unannotate.

tasqx annotate 1 Called the plumber, waiting on a quote
$
tasqx 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] position tasqx 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 annotate event's own body, so tasqx chart (event.list) and tasqx export stop showing it too — not only tasqx show.
  • Name the note by the [n] position tasqx show <ref> prints, a unique id prefix of 8+ characters, or its full id (annotations[].id in --json).
  • tasqx undo does 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.

See also annotate · show · undo.

tasqx unannotate 1 018f2f7e-...
Note

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/modify sugar: +api and api name 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.

See also untag · modify · list · show.

tasqx tag 1 api release

two tags, one call

tasqx tag 1 +api

the 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.

See also tag · show · list.

tasqx untag 1 api

removes it, and prints what remains

tasqx untag 1 api release

all 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.

See also undep · show.

tasqx dep 2 1

task 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 1
$
tasqx 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 set like any other client.
  • Completing with a criterion still open or failed is not refused — nothing is blocked — but done says so and tasqx report --outcomes counts it as an unproven completion: only a task whose every check passed is proven.
  • failed is a normal outcome. Use rm only when the criterion was the wrong thing to ask; removing one the work failed hides the finding.

See also done · show · report.

tasqx check add 1 the notes name every breaking change

add a criterion

tasqx check set 1 019f-abc passed --evidence "suite green"

mark it

tasqx check rm 1 019f-abc

drop a criterion

tasqx check set 1 2 failed

the 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 search is the wider read.
  • --card prints a fixed 72-column box-drawn card meant to be pasted into a document (a chat, a PR); --ascii draws its borders with + - |. The default screen is unchanged (D146).

See also show · next · memory.

tasqx brief 1
tasqx brief 1 --memory-limit 3
tasqx brief 1 --card
$
tasqx 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, next and the reports, and memory search never 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 ?. No project: 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 readme notes "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 is tasqx annotate 42 "…" now. After -- every word is text: tasqx note -- 7 notes "7". A flag the subverb does not take is a usage error (exit 2), wherever it stands.
  • promote task files 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 memory files 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.
  • rm is permanent and tasqx undo does not cover notes; archive is the way to put one out of sight.

See also notes · add · memory · annotate.

tasqx note https://example.com/blog/sqlite-tips --project infra

a link to read later, filed under a project

tasqx note 3

print note 3 whole

tasqx note 3 edit try the OTLP exporter instead

replace its text

tasqx note 3 archive

out 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 memory

file it as a memory doc instead

tasqx note 3 rm

delete it for good

tasqx note -- -5 degrees tonight

after --, a number or a leading dash is text

$
tasqx note try the new tracing exporter
Noted #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]
--archivedflagoptional
Archived notes instead of open ones
--allflagoptional
Open and archived notes
--project<project>optional
Only notes filed under this project
  • One line per note: its number, its first line, a link mark (↗, L without Unicode) when it holds an http:// or https:// 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/k move, g/G jump to the ends, / searches the text, enter reads a note whole (a link is shown whole, never opened), esc clears the search or leaves, q leaves. a archives or unarchives the note under the cursor, t and m promote it to a task or a memory doc, x removes it after a y/n, l keeps 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's N opens the same browser.

See also note · memory.

tasqx notes --links
tasqx notes --search exporter --all

a substring, ignoring case, in open and archived notes

$
tasqx notes
2 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 due and scheduled — 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 --days says: 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 --days that reaches the furthest one, or, when the row is further out than the widest window --days accepts, says so and points at tasqx list.
  • Done and cancelled tasks are left out unless your filter names a status, the same rule report applies to cancelled tasks (D24). tasqx agenda status:done shows 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-05 under the 4th west of Greenwich.

See also list · next · add · modify.

tasqx agenda --days 3
tasqx agenda project:work
$
tasqx agenda
through 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. due or -due for descending (repeatable, or comma-separated). Maps straight onto task.list's own sort param — see tasqx docs for the valid names. Default: -urgency, unchanged from before this flag existed
--limit<limit>optional
Cap the number of rows returned. Maps to task.list's limit. Defaults to 100 when omitted, and is clamped to 10,000 even when given explicitly
--offset<offset>optional
Skip this many matching rows before --limit is applied. Maps to task.list's offset
--fields<fields…>optional
Return only these task fields (repeatable, or comma-separated) — see tasqx docs for the valid names. Maps to task.list's fields; 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 tasqx is tasqx list over 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-stripped project:Home Renovation is the project Home plus 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 it title:review. status:any (or status:all) lists every status, done and cancelled included.
  • --sort, --limit, --offset and --fields map straight onto task.list's own params (core.capabilities names 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 with COLUMNS=200 tasqx list or narrow the row with --fields.

See also next · report · show.

tasqx list project:work status:pending +api
tasqx list due.before:friday
$
tasqx 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).
  • --card prints the fixed 72-column box-drawn card (D146) first, with the arithmetic underneath instead of the plain text header; --ascii draws the card's borders with + - |.

See also next · show · list.

tasqx why 1
tasqx why 1 --card
$
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

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's metrics param 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. --html defaults 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 --all or your filter names a status.
  • The terminal's TOKENS column names the largest of four buckets by volume; --metrics naming a tokens_* bucket shows all four as their own columns instead, same as --html and --json.
  • --since/--until window tracked_total and the token buckets by WHEN the time or spend happened (D97) — a different axis from completed.after:/completed.before: in the filter, which selects tasks by completion date. With --html they set the page's period instead (default: the 7 days ending today).
  • --html leads 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; --all adds cancelled tasks to its search; --metrics is 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.

See also chart · list · why.

tasqx report project
tasqx report --all
tasqx report --metrics tokens_in,tokens_out,tokens_cache_read,tokens_cache_creation
tasqx report --since -7d
tasqx report --outcomes
tasqx report --outcomes project --since -30d
tasqx report --html --out review.html

self-contained HTML

tasqx report +urgent --html --out sprint.html

scoped HTML

tasqx report --html --since 2026-09-01 --until 2026-10-01 --out september.html

HTML for one month

$
tasqx report
PROJECT  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/agenda take (#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) to filter above — kept for scripts and docs already spelling it this way; project:work on 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/agenda do (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 work is shorthand for the project:work filter 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 --project alone cannot.

See also report.

tasqx chart throughput
tasqx chart heatmap --year
tasqx chart burndown --days 30
tasqx chart burndown project:work
$
tasqx chart burndown
remaining 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 list marks the theme in effect with *, the mark tasqx projects puts on the default project.

See also report · manual.

tasqx theme show nord
$
tasqx theme list
  THEME
* 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.

See also watch · api.

tasqx daemon

bind the socket/named pipe and serve

Note

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)

See also daemon · list.

tasqx watch project:work

live view; needs a running daemon

Note

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|writeoptionaldefault write
write lets a drag change tasks; read refuses 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 board as the actor. --scope read refuses every write server-side.
  • --port or board.port fixes the port and keeps the token between runs, so a bookmark survives; otherwise a free port and a new token each run.

See also watch · daemon.

tasqx board

open the board; needs a running daemon

tasqx board --scope read

a wall screen: every change refused

Note

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). --task names it when the branch does not.
  • merged completes 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. opened on a task that cannot start, and closed, only add a note. Every write is recorded under the actor git.
  • Report merged again 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 (closed after merged), changes nothing and says why.

See also done · check · show.

tasqx git event opened --branch task/42-pricing --pr https://github.com/acme/web/pull/7

a pending task becomes active

tasqx git event merged --branch task/42-pricing --pr https://github.com/acme/web/pull/7 --sha 4f9c2e1

complete it, the PR in the delivery note

tasqx git event closed --branch fix-typo --pr https://github.com/acme/web/pull/8 --task 43

a 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 --offset on. Asking for a page prints the table, never the browser
--offset<offset>optionaldefault 0
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

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 add and memory 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 add and memory 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 add with 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, which tasqx memory show <id> reads, or annotation on #N, which tasqx show N opens — memory show refuses 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 modify uses. 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 --refresh takes 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 — and list --standing shows them; more than 15 in one scope earns a hint to merge or retract (D156).

See also annotate · mcp · api.

tasqx memory add "Deploy runbook" "deploys go through the blue-green pipeline"
tasqx memory search blue-green
tasqx memory search D41 --mode lexical

exact words only; an id with no word of three letters is always matched this way

tasqx memory search 'pipel*' --raw

FTS5 operator syntax: prefix search, AND/OR, column filters

tasqx memory import docs/adr

one doc per .md file; title from the first # heading

tasqx memory import docs/adr --project ledger

scopes every doc in the batch; a re-import naming a different project moves it (#657)

tasqx memory import --refresh

re-reads every imported doc whose file changed; takes no path

tasqx memory verify

flags 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-6f1b0e5c8a12

by the id search printed

$
tasqx memory list
5 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>optionaldefault unknown
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
  • add records a self-report at confidence medium — what done's token flags record, for a count that arrives later (D167). --total is 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 recompute prints the per-task delta and writes NOTHING; --apply is 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-parse only, self-report/OTLP never touched), and downgrade confidence to low for 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 as skipped.
  • 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.

See also report · done · api.

tasqx tokens add 602 --total 37898

a count that arrived after completion, as one unsplit number

tasqx tokens add 602 --in 18400 --out 2600 --tool claude-code

the same, split

tasqx tokens recompute
tasqx tokens recompute --apply

write the repair, after reviewing the dry-run delta

Note

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|writeoptionaldefault read
Operator-selected capability scope for this stdio process
  • Scope configures the local process; it is not authentication. Omitted scope is read-only.

See also api · daemon.

tasqx mcp serve

serve read-only over stdio JSON-RPC

tasqx mcp serve --scope write

serve with explicit write access

Note

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, .codex and .gemini
  • Three items per tool: mcp registers tasqx mcp serve --scope write at user level by running the tool's own mcp add; tasqx-workflow and retro are skills written to ~/.claude/skills, ~/.codex/skills or ~/.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, codex or gemini is on PATH; with none, a hint says how to install one. Their items are named codex:mcp, gemini:retro and 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-only tasqx mcp serve; replacing it runs claude mcp remove first.
  • 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/ripwire and put it on PATH.

See also mcp.

tasqx setup --list

what is installed; writes nothing

tasqx setup

a checklist: space ticks, enter installs what is ticked

tasqx setup --yes

installs everything not installed, and keeps a skill you edited

tasqx setup --yes --force --only retro

replaces the retro skill with the copy this build carries

Note

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 projects and default_project too, so a restore gives back the store and not only its tasks (D37).
  • A filter narrows projects/docs/links/events to what the exported tasks need — an unfiltered export still carries all of them; --include-unscoped widens 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: --out names removed_annotations (a removed note's tombstone; its removal event travels), skipped_events (the bookkeeping a past import wrote about itself) and dropped_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 as events_logged, and exporting the restored store gives the same document (D207).

See also import · api.

tasqx export
tasqx export project:work
tasqx export --out backup.json

to a file, replaced whole

Note

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}, as tasqx export writes) 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 source a DIFFERENT doc here already holds merges onto that doc rather than being refused: the stored id is kept, the later modified wins the text, and every merge is printed and reported as docs_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-run runs 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 with nothing was written (D184).
  • --merge folds 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 _rev guard 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 as merged (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 modified stamp, so there is no later-wins call to make — reported as project_description_conflicts (D190). archived is 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 as deduplicated (D191).

See also export · api.

tasqx import backup.json

from a file

tasqx export | tasqx import -

from stdin

tasqx import backup.json --dry-run

preview it first

tasqx import other-machine.json --merge

fold in another store

Note

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: dir runs tasqx-remote-dir from 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 and ps: answer it at the prompt, which does not echo, or in TASQX_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 setup asks it what it needs, hands it your answers, and once it accepts them records its name in <store>.sync.json beside 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 and ps would keep: answer it at the prompt, which does not echo, or put it in TASQX_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 setup asks for the passphrase twice at a prompt that does not echo, or reads TASQX_SYNC_PASSPHRASE; never --set. Every machine syncing with the remote needs the same one. It is kept in <store>.sync.key beside the store, readable by you alone, and sync --status says whether one is set. A snapshot that is not encrypted, or will not open with this passphrase, stops the sync before anything is merged.
  • sync pulls every snapshot the remote holds and merges each into this store exactly as import --merge does (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.

See also import · export.

tasqx sync setup dir --set path=/mnt/share/tasqx

a shared folder as the remote

tasqx sync setup r2

a Cloudflare R2 bucket; asks for each setting

tasqx sync
tasqx sync --status
Note

No 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 stdout

Takes 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): api runs against an in-process engine, never a daemon. --theme is likewise never read — there is nothing here to render.

See also mcp · daemon · export.

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 completion names the file for each shell.

See also manual · docs.

tasqx completions bash
tasqx completions bash >> ~/.bashrc

the printed line is one line for exactly this

Note

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.

CodeMeaning
0Success.
1internal, or a failure beneath the request: the store would not open, a local write failed, or watch had no daemon to follow.
2bad_request — a bad value, an unparseable date, contradictory flags.
4not_found — no such task, project, or reference.
5conflict — a lost --expected-rev race, or a lifecycle rule.
6unsupported_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=5

The 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 api
$
echo '{"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.

CodeExitMeans
bad_request2Malformed params, an unparseable date, contradictory input.
not_found4No such task/project/reference.
conflict5A lost expected_rev race, or a lifecycle rule.
unsupported_version6The tasqx major you sent is not this build's.
internal1A 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

result
idstringalways
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"
}
Note

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 init

init — Create a project — just a name, no folder.

project.list

{projects}; each row carries default.

Objects: Project.

Parameters

include_archivedbooleanoptionaldefault false
Include archived projects in the list. Omitted, an archive is out of sight.

Response

result
countintegeralways
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 and task.add takes.
descriptionstringnullable
The project's description, or null.
archivedbooleanalways
Whether the project is archived. Archived projects are omitted unless include_archived asked for them.
defaultbooleanalways
Whether this is the project a bare task.add lands 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.

tasqx dashboard

dashboard — Open the overview screen, or ask for its panels as data.

tasqx projects

projects — List projects (default marked with *).

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

result
namestringalways
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.

Note

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 use

use — 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

result
namestringalways
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.

Note

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 archive

archive — 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

result
namestringalways
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"
}
Note

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.

Note

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 unarchive

unarchive — 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 backlog until 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 backlog like scheduled but 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 the due grammar.
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_task reports over against 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

result
idstringalways
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 ref accepts and the CLI prints.
statusstringalways
The status the task landed in: pending, or backlog when a future wait parks 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 due is.
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 ids check.set takes.
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 similar is 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 with derived_from.
result.checks[]
idstringalways
The criterion's id — what check.set takes as check_id.
bodystringalways
The criterion as written.
statestringalways
Always open here: 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"
}
Note

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 add

add — 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

result
countintegeralways
How many rows came back.
totalintegeralways
How many tasks matched the filter, before limit truncated.
next_offsetintegernullable
The offset that 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 ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
statusstringalways
pending, backlog, active, done or cancelled — or, on a stored row whose status text this build does not recognize, that text verbatim, with status_unrecognized: true beside it.
prioritystringnullable
H, 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 pending task reads as backlog, out of the working set, as with wait; active, done and cancelled keep their status.
waitstringnullable
Holds the task out of the working set until this instant, or null. While it is in the future a pending task reads as backlog; active, done and cancelled keep 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 due or 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.done completed it, or null. task.reopen clears it, and task.cancel does 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.remove and reminder.fire leave it alone. Send it back as expected_rev to 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_since marks where that one began.
tracked_adjustmentstringalways
The net of every task.adjust_tracked correction already inside tracked, as a signed ISO duration (-PT2H25M); PT0S when 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 status no writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one. store.import now 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 list

list — List tasks matching a filter.

tasqx agenda

agenda — What is coming up, when — list ordered by time.

tasqx dashboard

dashboard — Open the overview screen, or ask for its panels as data.

tasqx pick

pick — Browse tasks on a full-screen list: read one, search them, start one.

tasqx watch

watch — Live-updating task view.

tasqx board

board — 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_offset for the rest; include_json: true spends 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 terms urgency sums (D1). Default false.

Response

result
delivered_annotation_idstringnullable
The note task.done pinned as the delivery note — the newest one at the instant of completion — or null. task.reopen clears it (D165).
first_annotationobjectnullable
The oldest note, whatever page annotations_limit/annotations_offset asked 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_tokens in full (D167).
overbooleannullable
Whether fresh_tokens is past budget_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.event recorded 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 ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
statusstringalways
pending, backlog, active, done or cancelled — or, on a stored row whose status text this build does not recognize, that text verbatim, with status_unrecognized: true beside it.
prioritystringnullable
H, 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 pending task reads as backlog, out of the working set, as with wait; active, done and cancelled keep their status.
waitstringnullable
Holds the task out of the working set until this instant, or null. While it is in the future a pending task reads as backlog; active, done and cancelled keep 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 due or 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.done completed it, or null. task.reopen clears it, and task.cancel does 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.remove and reminder.fire leave it alone. Send it back as expected_rev to 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_since marks where that one began.
tracked_adjustmentstringalways
The net of every task.adjust_tracked correction already inside tracked, as a signed ISO duration (-PT2H25M); PT0S when 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: 0 is 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, user for 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 annotations and 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 status no writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one. store.import now refuses such a status.
urgency_breakdownobjectoptional
The terms urgency sums — present only when explain: true asked for them.
unmet_blockersarrayalways
The open prerequisites, named — what blocked: true is blocked ON.
result.checks[]
idstringalways
The criterion's uuid — what check.set and check.remove take.
bodystringalways
The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalways
open, passed or failed. 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.
kindstringalways
branch, pr or commit.
refstringalways
The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalways
open, closed or merged. merged is final; a PR can move between open and closed.
createdstringalways
When the ref was first recorded.
modifiedstringalways
When state last changed. Between open and closed, the later change wins when two stores merge.
notedstringnullable
On a branch row, what git.event has 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.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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_annotation
idstringalways
The note's uuid — what annotation.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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_annotation
idstringalways
The note's uuid — what annotation.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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.remove takes.
toolstringalways
Which tool spent the tokens (claude-code, codex, …).
sourcestringalways
How the figure was obtained: self-report, log-parse or otel.
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, medium or low. A self-report may not claim high.
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_breakdown
prioritynumberalways
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.

tasqx pick

pick — Browse tasks on a full-screen list: read one, search them, start one.

tasqx show

show — Show one task in full detail.

tasqx why

why — Explain a task's urgency score.

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

result
taskobjectalways
task.get's own result for this task, without urgency_breakdown — the brief does not take explain.
neighbourhoodobjectalways
What this task waits on and what it releases.
memoryobjectalways
A memory.search result 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.task
delivered_annotation_idstringnullable
The note task.done pinned as the delivery note — the newest one at the instant of completion — or null. task.reopen clears it (D165).
first_annotationobjectnullable
The oldest note, whatever page annotations_limit/annotations_offset asked 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_tokens in full (D167).
overbooleannullable
Whether fresh_tokens is past budget_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.event recorded 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 ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
statusstringalways
pending, backlog, active, done or cancelled — or, on a stored row whose status text this build does not recognize, that text verbatim, with status_unrecognized: true beside it.
prioritystringnullable
H, 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 pending task reads as backlog, out of the working set, as with wait; active, done and cancelled keep their status.
waitstringnullable
Holds the task out of the working set until this instant, or null. While it is in the future a pending task reads as backlog; active, done and cancelled keep 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 due or 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.done completed it, or null. task.reopen clears it, and task.cancel does 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.remove and reminder.fire leave it alone. Send it back as expected_rev to 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_since marks where that one began.
tracked_adjustmentstringalways
The net of every task.adjust_tracked correction already inside tracked, as a signed ISO duration (-PT2H25M); PT0S when 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: 0 is 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, user for 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 annotations and 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 status no writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one. store.import now refuses such a status.
unmet_blockersarrayalways
The open prerequisites, named — what blocked: true is blocked ON.
result.task.checks[]
idstringalways
The criterion's uuid — what check.set and check.remove take.
bodystringalways
The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalways
open, passed or failed. 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.
kindstringalways
branch, pr or commit.
refstringalways
The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalways
open, closed or merged. merged is final; a PR can move between open and closed.
createdstringalways
When the ref was first recorded.
modifiedstringalways
When state last changed. Between open and closed, the later change wins when two stores merge.
notedstringnullable
On a branch row, what git.event has 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.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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_annotation
idstringalways
The note's uuid — what annotation.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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_annotation
idstringalways
The note's uuid — what annotation.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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.remove takes.
toolstringalways
Which tool spent the tokens (claude-code, codex, …).
sourcestringalways
How the figure was obtained: self-report, log-parse or otel.
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, medium or low. A self-report may not claim high.
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.neighbourhood
depends_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 ref accepts 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 ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
statusstringalways
The dependent's status.
result.memory
countintegeralways
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.search returns, each with its via.
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}, query being the text the brief derived from the task's title, tags and project; null when it compared nothing (D196).
result.last_time
short_idintegeralways
The task's short id — the small number every ref accepts 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.
kindstringalways
doc or annotation — 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 ascending rank; 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::symbol refs it cites that are gone from the git toplevel of the caller's workdir; empty when none are, or none can be checked. Null on an annotation hit and when no workdir was given (D235).
modifiedstringalways
When the entry last changed: a doc's modified, an annotation's created (annotations carry no modified time, so an edit does not move it, the same rule graph.query uses for a node) — what a date window filters hits on (D219).
viastringalways
Which side found it: lexical (words — all of them, which matched explains, or only some, which partial says and matched_any explains), semantic (meaning; similarity explains it) or both (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. relaxed is true when any hit on the page is.
result.memory.semantic
modelstringalways
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"
}
Note

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 brief

brief — 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

result
taskobjectnullable
The most urgent unblocked task — the first row task.list gives 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 task is 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_by is 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}. note is {body, created} with the body cut at 200 characters.
result.task
idstringalways
The task's uuid, stable across exports and machines.
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
statusstringalways
pending, backlog, active, done or cancelled — or, on a stored row whose status text this build does not recognize, that text verbatim, with status_unrecognized: true beside it.
prioritystringnullable
H, 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 pending task reads as backlog, out of the working set, as with wait; active, done and cancelled keep their status.
waitstringnullable
Holds the task out of the working set until this instant, or null. While it is in the future a pending task reads as backlog; active, done and cancelled keep 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 due or 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.done completed it, or null. task.reopen clears it, and task.cancel does 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.remove and reminder.fire leave it alone. Send it back as expected_rev to 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_since marks where that one began.
tracked_adjustmentstringalways
The net of every task.adjust_tracked correction already inside tracked, as a signed ISO duration (-PT2H25M); PT0S when 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 status no writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one. store.import now 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[].note
bodystringalways
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[].note
bodystringalways
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 next

next — 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; pass keep: true to 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

result
idstringalways
The task's uuid.
statusstringalways
active once 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 ref accepts 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 keep was 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.

tasqx pick

pick — Browse tasks on a full-screen list: read one, search them, start one.

tasqx start

start — Mark a task active.

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

result
statusstringalways
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.get uses.
short_idintegeralways
The task's short id — the small number every ref accepts 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 stop

stop — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
opstringalways
claim, renew (the holder claimed again) or takeover (another holder's lease had expired) — the event it wrote.
claimed_bystringalways
The holder: the call's actor, else user.
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 claim

claim — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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 force released a claim someone else held; the release event 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"
}
Note

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 release

release — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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.get uses.
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 adjust

adjust — 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_hint says so and tasqx_outcomes counts 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_claimbooleanoptionaldefault false
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

result
statusstringalways
done — 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 ref accepts 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 done

done — 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 _rev when 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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.remove and reminder.fire leave it alone. Send it back as expected_rev to 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.title carries CLI inline sugar, stored verbatim: one line naming those words, as on task.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 modify

modify — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
statusstringalways
cancelled.
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 cancel

cancel — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
statusstringalways
The status the task went back to.
blockedarrayalways
The open dependents this reopen put back into blocked — the mirror of unblocked.
$
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 reopen

reopen — 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

result
short_idintegeralways
The proposal's short id.
titlestringalways
Its title.
actionstringalways
accept, decline or duplicate, as asked.
statusstringalways
Its status now: pending (or backlog while a future wait or scheduled holds it) after accept, cancelled otherwise (D238).
_revintegeralways
The proposal's revision after the call.
unblockedarrayalways
Short ids of open tasks the decline or duplicate released, as task.cancel reports them (D11). Empty after accept.
ofobjectoptional
Present on duplicate: {short_id, title} of the task it repeats, now linked to it with duplicates.
moved_annotationsintegeroptional
Present on duplicate: how many notes moved to of (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.

tasqx accept

accept — Accept a proposal from triage: it becomes pending.

tasqx decline

decline — Decline a proposal from triage: it is cancelled.

tasqx duplicate

duplicate — Mark a proposal in triage a duplicate of another task.

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

limitintegeroptionaldefault 100
Proposals per page, at most 10,000 — `task.list`'s bounds.
offsetintegeroptionaldefault 0
Proposals to skip; pass the previous answer's `next_offset`.

Response

result
tasksarrayalways
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 offset that 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 add event names, or null when none did.
discovered_fromobjectnullable
{short_id, title} of the task its derived_from link 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.

Note

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 triage

triage — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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 tag

tag — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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_found and 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 untag

untag — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
annotationobjectalways
The note as stored, echoed back so a caller can see its bytes survived.
result.annotation
idstringalways
The note's uuid — what annotation.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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"
}
Note

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 annotate

annotate — 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 show prints (D211).

Response

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
removedobjectalways
The tombstone: the id and the instant, never the text (D113).
result.removed
idstringalways
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 unannotate

unannotate — 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

result
short_idintegeralways
The short id of the task the note was on — the ref you 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.annotation
idstringalways
The note's uuid — what annotation.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_bytes (D148).
createdstringalways
When the note was written.
result.to
short_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 annotate

annotate — 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 show prints (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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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_rev to make the next change conditional.
result.annotation
idstringalways
The note's uuid — what annotation.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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"
}
Note

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 annotate

annotate — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
checkobjectalways
The criterion as stored, appended at the end and starting open.
result.check
idstringalways
The criterion's uuid — what check.set and check.remove take.
bodystringalways
The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalways
open, passed or failed. 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"
}
Note

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 check

check — 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_task or 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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 check

check — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
check_idstringalways
The criterion that was dropped.
removedbooleanalways
Always true on success; an unknown id is not_found instead.
$
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 check

check — 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 ref must 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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 dep

dep — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts 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 undep

undep — Remove a dependency edge.

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

result
idstringalways
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"
}
Note

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.

Note

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

result
idstringalways
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"
}
Note

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.

Note

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.
limitintegeroptionaldefault 100
How many links to return, newest first. Clamped to 1,000.
offsetintegeroptionaldefault 0
Where the page starts. `next_offset` from the previous page keeps the walk going.

Response

result
countintegeralways
How many links came back in this page.
totalintegeralways
How many matched before limit and offset cut the page.
next_offsetintegernullable
The offset that keeps walking, or null once nothing is left.
linksarrayalways
The links themselves, newest first. A ref matches EITHER endpoint, so a node's neighbourhood is one call.
result.links[]
idstringalways
The link's uuid — what link.remove takes.
fromstringalways
The node the link starts at, as <type>:<uuid>: task:, memory:, annotation: or project:.
tostringalways
The node the link points at, spelled the same way.
relationstringalways
What the link asserts: references, supersedes, implements_decision, derived_from, contradicts or duplicates (D238).
metadataobjectnullable
The caller's own object, stored verbatim and never interpreted, or null.
created_atstringalways
When the link was written. Named created_at because link.add answers with a boolean created beside 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.

Note

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.
depthintegeroptionaldefault 2
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_inferredbooleanoptionaldefault false
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_nodesintegeroptionaldefault 250
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_edgesintegeroptionaldefault 750
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

result
rootstringnullable
The node the projection is anchored at, resolved to its stable <type>:<uuid> id. Null on a whole-store select, which has no anchor.
depthintegeralways
How many hops were walked — the depth that was asked for, or 2. Always 0 on a whole-store select, which walks nothing.
selectstringnullable
The whole-store set that was asked for (blocked or orphans), or null on a walk from a root.
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, then to. 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_nodes cut.
omitted_edgesintegeralways
How many edges max_edges cut, 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-up graph.query takes as its root.
typestringalways
Which kind of node: task, memory, annotation or project.
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 wait shows backlog). Null on every other kind of node.
modifiedstringnullable
When the node last changed: modified for a task or a document, created for 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: or link: for a stored one, search: or tag: for a computed one.
fromstringalways
The node id the edge starts at. A depends_on edge 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, or search_match/shared_tag when include_inferred asked for them.
kindstringalways
structural for an edge read out of a table, inferred for 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.

Note

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 conflict naming 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

result
idstringalways
The new doc's uuid — what a search hit carries and memory.get takes.
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 similar is 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.get takes.
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"
}
Note

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 memory

memory — 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_more say 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 project scope to docs and annotations with NO project — what tasqx memory import produces. Never admits another project's. Refused without project.
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

result
countintegeralways
How many hits came back.
totalintegeralways
How many entries matched before limit truncated: 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: 0 is told apart from a store holding nothing on the subject. Null under mode: 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; under mode: lexical it runs only when no entry has every word, and then matched is that OR. Always false for raw and mode: 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: lexical or raw, a query with no word of three letters or more that is not a stopword (an id such as D41), fewer than half its content words known to the model whole, or nothing to compare against — semantic_skipped says 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 under mode: lexical or raw (#838).
result.hits[]
idstringalways
The doc's id, or the annotation's.
kindstringalways
doc or annotation — 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 ascending rank; 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::symbol refs it cites that are gone from the git toplevel of the caller's workdir; empty when none are, or none can be checked. Null on an annotation hit and when no workdir was given (D235).
modifiedstringalways
When the entry last changed: a doc's modified, an annotation's created (annotations carry no modified time, so an edit does not move it, the same rule graph.query uses for a node) — what a date window filters hits on (D219).
viastringalways
Which side found it: lexical (words — all of them, which matched explains, or only some, which partial says and matched_any explains), semantic (meaning; similarity explains it) or both (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. relaxed is true when any hit on the page is.
result.semantic
modelstringalways
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_skipped
codestringalways
Why, as a stable code to branch on: no_content_word (no word of three letters or more — an id such as D41), 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 D41 is 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"
}
Note

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 memory

memory — 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_memory prints 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

result
idstringalways
The doc's uuid.
sourcestringnullable
Where it came from — the path memory.import keyed 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_rev to 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.import read 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::symbol refs the doc cites that are gone from the git toplevel of the caller's workdir; empty when none are, or none can be checked; null when no workdir was 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 memory

memory — 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 doc hit.

Response

result
idstringalways
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 memory

memory — 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

result
importedintegeralways
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 in replaced_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, _rev as it was (#86).
_revintegeralways
The row's revision counter, bumped by every write. Send it back as expected_rev to 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.

Note

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 memory

memory — 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_runbooleanoptionaldefault false
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

result
checkedintegeralways
How many docs named an origin file and were examined. refreshed, missing and unchanged account 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 rev did 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"
}
Note

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.

Note

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 memory

memory — 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

result
countintegeralways
How many docs came back.
totalintegeralways
How many docs the scope holds.
next_offsetintegernullable
The offset that 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_rev to make a change conditional.
body_previewstringalways
The opening of the body, or all of it when the body is short (body_truncated says 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 memory

memory — 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_memory or tasqx_list_memory prints 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 conflict naming 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 rev when omitted; pass it to pin a rev you read earlier.

Response

result
idstringalways
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_rev to 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 memory

memory — 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

result
idstringalways
The note's uuid.
short_idintegeralways
The note's own small number, from a counter tasks do not share — what note.get takes beside the uuid.
bodystringalways
The whole body, stored and returned verbatim.
projectstringnullable
The project it is filed under, or null.
statusstringalways
open or archived.
promoted_tostringnullable
Where note.promote filed it — task:<uuid> or memory:<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_rev to make a change conditional.
has_linksbooleanalways
Whether the body holds an http:// or https:// link — derived on every read, the same test note.list {links: true} filters on.
promoted_task_short_idintegernullable
The short id the task promoted_to names 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"
}
Note

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 note

note — 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.
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

result
countintegeralways
How many notes came back.
totalintegeralways
How many notes match before the page.
next_offsetintegernullable
The offset that 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.get takes beside the uuid.
bodystringalways
The whole body, stored and returned verbatim.
projectstringnullable
The project it is filed under, or null.
statusstringalways
open or archived.
promoted_tostringnullable
Where note.promote filed it — task:<uuid> or memory:<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_rev to make a change conditional.
has_linksbooleanalways
Whether the body holds an http:// or https:// link — derived on every read, the same test note.list {links: true} filters on.
promoted_task_short_idintegernullable
The short id the task promoted_to names 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 notes

notes — 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

result
idstringalways
The note's uuid.
short_idintegeralways
The note's own small number, from a counter tasks do not share — what note.get takes beside the uuid.
bodystringalways
The whole body, stored and returned verbatim.
projectstringnullable
The project it is filed under, or null.
statusstringalways
open or archived.
promoted_tostringnullable
Where note.promote filed it — task:<uuid> or memory:<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_rev to make a change conditional.
has_linksbooleanalways
Whether the body holds an http:// or https:// link — derived on every read, the same test note.list {links: true} filters on.
promoted_task_short_idintegernullable
The short id the task promoted_to names 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 note

note — 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 _rev when omitted.

Response

result
idstringalways
The note's uuid.
short_idintegeralways
The note's own small number, from a counter tasks do not share — what note.get takes beside the uuid.
bodystringalways
The whole body, stored and returned verbatim.
projectstringnullable
The project it is filed under, or null.
statusstringalways
open or archived.
promoted_tostringnullable
Where note.promote filed it — task:<uuid> or memory:<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_rev to make a change conditional.
has_linksbooleanalways
Whether the body holds an http:// or https:// link — derived on every read, the same test note.list {links: true} filters on.
promoted_task_short_idintegernullable
The short id the task promoted_to names 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 note

note — 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

result
idstringalways
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 note

note — 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

result
noteobjectalways
The note after the promote: archived, with promoted_to naming what it became.
taskobjectoptional
The task it became, on to: task; absent on to: memory.
docobjectoptional
The memory doc it became, on to: memory; absent on to: task.
result.note
idstringalways
The note's uuid.
short_idintegeralways
The note's own small number, from a counter tasks do not share — what note.get takes beside the uuid.
bodystringalways
The whole body, stored and returned verbatim.
projectstringnullable
The project it is filed under, or null.
statusstringalways
open or archived.
promoted_tostringnullable
Where note.promote filed it — task:<uuid> or memory:<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_rev to make a change conditional.
has_linksbooleanalways
Whether the body holds an http:// or https:// link — derived on every read, the same test note.list {links: true} filters on.
promoted_task_short_idintegernullable
The short id the task promoted_to names 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.task
idstringalways
The task's uuid.
short_idintegeralways
The task's short id — the small number every ref accepts.
titlestringalways
Its title: the one given, or the body's first line cut at 80 characters.
statusstringalways
Its status, as task.add answers it.
projectstringnullable
The project it landed in — the note's, else the store's default — or null.
result.doc
idstringalways
The doc's uuid — what memory.get takes.
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"
}
Note

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 note

note — 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_tokensintegeroptionaldefault 0
Fresh input tokens.
output_tokensintegeroptionaldefault 0
Output tokens.
cache_read_tokensintegeroptionaldefault 0
Tokens read from cache. Kept in its own bucket and never blended with the others (D48).
cache_creation_tokensintegeroptionaldefault 0
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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
measurementobjectalways
The measurement as banked. A repeated idempotency_key returns the one already stored, unchanged.
result.measurement
idstringalways
The measurement's uuid — what token.remove takes.
toolstringalways
Which tool spent the tokens (claude-code, codex, …).
sourcestringalways
How the figure was obtained: self-report, log-parse or otel.
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, medium or low. A self-report may not claim high.
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"
}
Note

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 tokens

tokens — 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

result
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
removedobjectalways
The measurement that is now gone — the same object token.add hands back (#210).
result.removed
idstringalways
The measurement's uuid — what token.remove takes.
toolstringalways
Which tool spent the tokens (claude-code, codex, …).
sourcestringalways
How the figure was obtained: self-report, log-parse or otel.
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, medium or low. A self-report may not claim high.
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"
}
Note

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.

Note

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_runbooleanoptionaldefault true
Report the per-task delta and write nothing. Send `false` to apply it — this is the one method that deletes measurement rows.

Response

result
dry_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, update or delete.
beforeobjectalways
The four buckets as they stand.
afterobjectalways
The four buckets the pass computed.
result.tasks[].before
input_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[].after
input_tokensintegeralways
Fresh input tokens.
output_tokensintegeralways
Output tokens.
cache_read_tokensintegeralways
Tokens read from cache.
cache_creation_tokensintegeralways
Tokens spent writing the cache.
result.totals
beforeintegeralways
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.

Note

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 tokens

tokens — 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, unlike tasqx 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_total and the token buckets to spend at or after this instant, not the task's lifetime total — a different axis from filter's completed.* terms (D97).
untilstringoptional
The other end of since: excludes anything at or after this instant. Together they window WHEN the spend happened.

Response

result
groupsarrayalways
One row per group, on the axis group_by named.
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 is project, status or priority.
countintegeralways
How many tasks fell in this group.
est_totalstringalways
Their estimates summed, as an ISO duration. Only when metrics names it — the default is count alone.
overdueintegeralways
How many of them are past due. Only when metrics names 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.

tasqx dashboard

dashboard — Open the overview screen, or ask for its panels as data.

tasqx report

report — Summary counts, or outcomes, optionally grouped, as text or HTML.

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/until window WHEN THE TASK CLOSED, not when spend happened.
untilstringoptional
The other end of since: excludes tasks that closed at or after this instant.

Response

result
groupsarrayalways
One row per group, each metric beside the n it was computed over.
group_bystringalways
The axis the rows are grouped on, echoed.
metricsarrayalways
The metrics computed. Omitting metrics on 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.brief and whose start carried no memory (D227).
result.groups[].rework
countintegeralways
How many occurrences.
nintegeralways
The denominator this rate was computed over. A rate never travels without it.
ratenumbernullable
count over n; 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[].calibration
median_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[].cost
tokens_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_tokens counts, 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[].silent
countintegeralways
How many occurrences.
nintegeralways
The denominator this rate was computed over. A rate never travels without it.
ratenumbernullable
count over n; 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[].abandonment
countintegeralways
How many started tasks were then cancelled.
nintegeralways
The denominator this rate was computed over. A rate never travels without it.
ratenumbernullable
count over n, 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[].overrun
countintegeralways
How many occurrences.
nintegeralways
The denominator this rate was computed over. A rate never travels without it.
ratenumbernullable
count over n; 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[].unproven
countintegeralways
How many occurrences.
nintegeralways
The denominator this rate was computed over. A rate never travels without it.
ratenumbernullable
count over n; 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 count had a check marked failed rather than left open (D205).
result.groups[].forced
countintegeralways
How many occurrences.
nintegeralways
The denominator this rate was computed over. A rate never travels without it.
ratenumbernullable
count over n; 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[].unbriefed
countintegeralways
How many occurrences.
nintegeralways
The denominator this rate was computed over. A rate never travels without it.
ratenumbernullable
count over n; 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 report

report — 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_unscopedbooleanoptionaldefault false
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

result
tasksarrayalways
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 with include_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 with include_unscoped. The stored row, without the derived has_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.list pages.
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.remove or link.remove of 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.add names 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 ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
statusstringalways
pending, backlog, active, done or cancelled — or, on a stored row whose status text this build does not recognize, that text verbatim, with status_unrecognized: true beside it.
prioritystringnullable
H, 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 pending task reads as backlog, out of the working set, as with wait; active, done and cancelled keep their status.
waitstringnullable
Holds the task out of the working set until this instant, or null. While it is in the future a pending task reads as backlog; active, done and cancelled keep 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 due or 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.done completed it, or null. task.reopen clears it, and task.cancel does 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.remove and reminder.fire leave it alone. Send it back as expected_rev to 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_tracked corrections inside tracked_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.done pinned 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 status no writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one. store.import now 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.remove takes.
toolstringalways
Which tool spent the tokens (claude-code, codex, …).
sourcestringalways
How the figure was obtained: self-report, log-parse or otel.
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, medium or low. A self-report may not claim high.
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.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_bytes (D148).
createdstringalways
When the note was written.
result.tasks[].checks[]
idstringalways
The criterion's uuid — what check.set and check.remove take.
bodystringalways
The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalways
open, passed or failed. 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.
kindstringalways
branch, pr or commit.
refstringalways
The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalways
open, closed or merged. merged is final; a PR can move between open and closed.
createdstringalways
When the ref was first recorded.
modifiedstringalways
When state last changed. Between open and closed, the later change wins when two stores merge.
notedstringnullable
On a branch row, what git.event has 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 project names.
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.import keyed 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_rev to 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.import read 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.get takes beside the uuid.
bodystringalways
The whole body, stored and returned verbatim.
projectstringnullable
The project it is filed under, or null.
statusstringalways
open or archived.
promoted_tostringnullable
Where note.promote filed it — task:<uuid> or memory:<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_rev to make a change conditional.
result.links[]
idstringalways
The link's uuid — what link.remove takes.
fromstringalways
The node the link starts at, as <type>:<uuid>: task:, memory:, annotation: or project:.
tostringalways
The node the link points at, spelled the same way.
relationstringalways
What the link asserts: references, supersedes, implements_decision, derived_from, contradicts or duplicates (D238).
metadataobjectnullable
The caller's own object, stored verbatim and never interpreted, or null.
created_atstringalways
When the link was written. Named created_at because link.add answers with a boolean created beside it.
result.events[]
idstringalways
The event's uuid.
entitystringalways
What kind of thing it happened to: task, project, doc, link or note.
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"
}
Note

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.

Note

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.

tasqx export

export — Dump tasks as canonical JSON.

tasqx sync

sync — Sync this store with its remote: pull, merge, push.

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.
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_runbooleanoptionaldefault false
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).
mergebooleanoptionaldefault false
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

result
importedintegeralways
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 docs section 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 import per task, project and note, a memory.add per doc — on top of events_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 links section.
default_projectstringnullable
The default project after the import, or null.
renumberedarrayalways
Every task whose short_id a 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_id a 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 notes section carried in (D241): a new note keeps its short id unless one here holds it, a known one is replaced, or on merge takes the later modified. 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, and took is payload when the payload's modified was later (its title, body, project and standing were written) or store when it was not (nothing was written). dropped_id is null when the payload doc carried no id of its own, rather than a minted one a dry run would then roll back. D183. Empty when nothing merged.
dry_runbooleanalways
Echoes the dry_run param (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 merge unioned 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_payload and from_store name the fields that differed and the side each was taken from, and took is payload or store when one side supplied all of them, mixed when both did, and D185's later-modified side when none differed. tracked_delta_seconds is 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 when merge was 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 no modified stamp, so there is no later-wins call to make and the destination's text always wins (D190). archived is 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 sharing spawned_from, due and scheduled keeps the lowest id and takes the other's notes, checks, tags, edges, links, tokens and events. status is 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_dependencies names 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.

Note

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.

tasqx import

import — Load tasks from a JSON file or stdin.

tasqx sync

sync — Sync this store with its remote: pull, merge, push.

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

limitintegeroptionaldefault 50
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

result
countintegeralways
How many events came back.
eventsarrayalways
The events, newest first for everything this engine wrote — id is a UUIDv7, time-ordered — but a row brought in by store.import keeps 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, link or note.
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.

Note

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.

tasqx dashboard

dashboard — Open the overview screen, or ask for its panels as data.

tasqx chart

chart — Render throughput, heatmap, or burndown charts.

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

result
revertedobjectalways
The event that was undone.
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
titlestringalways
The title of the task the undo touched.
restoredobjectalways
What the inverse put back — per-op: done and cancel answer {status, tracked?, interval_started?, checks?, removed_spawn?}, modify answers {fields} (each restored column and its value), tag.add answers {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.remove and reminder.fire leave it alone. Send it back as expected_rev to make a change conditional.
result.reverted
eventstringalways
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"
}
Note

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.

Note

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 undo

undo — 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

result
changedbooleanalways
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, merged or closed.
short_idintegeralways
The task's short id — the small number every ref accepts and the CLI prints.
titlestringalways
The task's title.
statusstringalways
The task's status after the event.
transitionstringnullable
started, completed or annotated; 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 changed is false: already recorded, or the state that kept the event from moving its ref (merged is 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.

Note

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 git

git — 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

result
firedbooleanalways
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 ref accepts 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.

Note

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

result
apistringalways
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"
}
Note

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.

Note

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

result
receivedintegeralways
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.

Note

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 write
tasqx mcp: serving over stdio (scope=read)
tasqx mcp: serving over stdio (scope=write)
$
tasqx mcp serve   # scope omitted
tasqx 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"]
    }
  }
}
Note

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.

ToolScopeMethodDoes
tasqx_list_tasksreadtask.listList tasks matching a filter-DSL query — the CLI's grammar, e.g.
tasqx_get_taskreadtask.getGet one task's full detail: fields, tags, annotations, dependencies.
tasqx_brief_taskreadtask.briefEverything 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_nextreadtask.nextThe 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_summaryreadreport.summaryAggregate report grouped by project, status or priority.
tasqx_outcomesreadreport.outcomesHow 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_projectsreadproject.listList projects.
tasqx_search_memoryreadmemory.searchSearch memory — imported docs and task annotations — by meaning and words, fused (D196); each hit's via says which matched.
tasqx_list_memoryreadmemory.listBrowse memory docs without knowing a word inside one — what tasqx_search_memory cannot do without a query.
tasqx_get_memoryreadmemory.getRead one knowledge doc whole, by the id a search hit carries — tasqx_search_memory returns only an excerpt.
tasqx_add_taskwritetask.addCreate a task.
tasqx_modify_taskwritetask.modifyChange fields on a task via a set map.
tasqx_complete_taskwritetask.doneMark a task done.
tasqx_cancel_taskwritetask.cancelCancel a task: backlog, pending or active moves to cancelled.
tasqx_triage_taskwritetask.triageSettle 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_taskwritetask.reopenReopen 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_timerwritetask.startStart the timer on a task, moving it to active.
tasqx_claim_taskwritetask.claimLease a task so parallel agents skip it and cannot start, stop or close it (D236).
tasqx_release_taskwritetask.releaseGive up your claim (D236).
tasqx_stop_timerwritetask.stopStop the timer on a task.
tasqx_adjust_trackedwritetask.adjust_trackedCorrect tracked time by a signed delta (-2h25m, +30m); any status.
tasqx_tag_taskwritetag.addAdd one or more tags to a task.
tasqx_untag_taskwritetag.removeRemove one or more tags from a task.
tasqx_add_checkwritecheck.addAdd an acceptance criterion to a task — one thing that must be true for it to count as done (D138).
tasqx_set_checkwritecheck.setMark one acceptance criterion passed or failed, with the evidence for it.
tasqx_remove_checkwritecheck.removeDrop an acceptance criterion that turned out to be the wrong thing to ask.
tasqx_annotate_taskwriteannotation.addAttach a timestamped note to a task.
tasqx_add_tokenswritetoken.addRecord token spend on a task after the fact, e.g.
tasqx_remove_annotationwriteannotation.removePermanently scrub one annotation's text, named by id, id prefix or position.
tasqx_update_annotationwriteannotation.updateCorrect 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_annotationwriteannotation.moveMove one annotation to another task (D211): id, body and timestamp are kept and search follows.
tasqx_add_dependencywritedependency.addMake 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_dependencywritedependency.removeCut a dependency edge: ref stops waiting on depends_on.
tasqx_add_memorywritememory.addStore a knowledge document in memory: patterns, documentation, decisions worth finding again.
tasqx_update_memorywritememory.updateCorrect a memory doc in place — title, body, source and/or project — by id.
tasqx_remove_memorywritememory.removeRemove one knowledge document from memory by id.
tasqx_add_notewritenote.addCapture a quick note: an idea, a URL to read later, a throwaway line.
tasqx_list_notesreadnote.listList notes, newest first, paged like tasqx_list_tasks.
tasqx_get_notereadnote.getRead one note.
tasqx_update_notewritenote.updateChange a note's body, project or status; status: archived archives it and open brings it back.
tasqx_remove_notewritenote.removeDelete a note for good.
tasqx_promote_notewritenote.promoteFile a note as a task or a memory doc in one transaction, then archive it with promoted_to naming what it became.
tasqx_create_projectwriteproject.createCreate a project.

tasqx_list_tasks

read scopeidempotent

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

read scopeidempotent

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_offset for the rest; include_json: true spends 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 terms urgency sums (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.get has 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.get has 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

read scopeidempotent

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.brief has an opinion about.
include_rankbooleanoptional
MCP only Add the fused rank and score to each memory hit; default false, and visible only in the include_json block. LOWER rank is 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 something task.brief has 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.brief has 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

read scopeidempotent

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.next has 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

read scopeidempotent

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, unlike tasqx report.
sincestringoptional
Window tracked_total and the token buckets to spend at or after this instant, not the task's lifetime total — a different axis from filter's completed.* 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

read scopeidempotent

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/until window 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

read scopeidempotent

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

read scopeidempotent

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 rank and score to each hit. Default false. LOWER rank is better and hits is 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 something memory.search has an opinion about.
include_unscopedbooleanoptional
Widen a project scope to docs and annotations with NO project — what tasqx memory import produces. Never admits another project's. Refused without project.
limitintegeroptional
Max hits (0 or more); default 10. The response's total/has_more say 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

read scopeidempotent

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-character body_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 something memory.list has 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 scopeidempotent

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_memory prints 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

write scope

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_task reports over against 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 the due grammar.
scheduledstringoptional
When you intend to start; a future value holds the task in backlog until 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 backlog like scheduled but 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

write scopedestructive

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 _rev when 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

write scopedestructive

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_hint says so and tasqx_outcomes counts 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 retro block (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 something task.done has 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.done has 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

write scopedestructive

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

write scopedestructive

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

write scopedestructive

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

write scope

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; pass keep: true to 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 something task.start has 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

write scope

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

write scope

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

write scopeidempotent

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

write scope

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

write scopeidempotent

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

write scopedestructiveidempotent

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

write scope

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

write scopedestructiveidempotent

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_task or 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

write scopedestructiveidempotent

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

write scope

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_bytes in 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 something annotation.add has 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

write scope

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

write scopedestructiveidempotent

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 show prints (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

write scopedestructiveidempotent

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 show prints (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

write scope

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

write scopeidempotent

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 ref must 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

write scopedestructiveidempotent

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

write scope

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 conflict naming 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

write scopedestructive

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 rev when omitted; pass it to pin a rev you read earlier.
idstringrequired
The doc UUID, as a search hit, tasqx_add_memory or tasqx_list_memory prints it.
projectstringoptional
New project scope. Omit to leave unchanged.
sourcestringoptional
New source. Omit to leave unchanged. One another doc holds is a conflict naming 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

write scopedestructiveidempotent

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 doc hit.
{"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

write scope

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

read scopeidempotent

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.
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 scopeidempotent

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

write scopedestructive

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 _rev when 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

write scopedestructiveidempotent

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

write scopedestructive

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

write scopeidempotent

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:

MethodWhy not
task.triage_listtasqx_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.capabilitiesthe 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.listthe 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.reverttasqx 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.eventa 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.queryan 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.addD160'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.listnothing 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.removethe 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.importit 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.refreshthe 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.statusan 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.archiveretiring 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.unarchivethe 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.useruled 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.firedaemon-internal. Its reminded event is a dedupe key and a push surface, not a thing a client asks for.
store.exportan 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.importit 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.removethe 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.recomputea 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 ref accepts and the CLI prints.
titlestringalways
The task's title, verbatim.
statusstringalways
pending, backlog, active, done or cancelled — or, on a stored row whose status text this build does not recognize, that text verbatim, with status_unrecognized: true beside it.
prioritystringnullable
H, 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 pending task reads as backlog, out of the working set, as with wait; active, done and cancelled keep their status.
waitstringnullable
Holds the task out of the working set until this instant, or null. While it is in the future a pending task reads as backlog; active, done and cancelled keep 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 due or 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.done completed it, or null. task.reopen clears it, and task.cancel does 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.remove and reminder.fire leave it alone. Send it back as expected_rev to make a change conditional.
status_unrecognizedbooleanoptional
Present, and true, only on a row whose status no writer of this build could have produced — a store an older import wrote unvalidated, or a hand-edited one. store.import now 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_since marks where that one began.
tracked_adjustmentstringalways
The net of every task.adjust_tracked correction already inside tracked, as a signed ISO duration (-PT2H25M); PT0S when there is none.
active_sincestringnullablein task.list · task.get · task.brief · task.next
When the open interval started, or null when the timer is not running.
active_sincestringoptionalin store.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_tracked corrections inside tracked_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_tokens in full (D167).
overbooleannullable
Whether fresh_tokens is past budget_tokens; null when no budget was set, because there is no verdict to give.
spawned_fromintegernullablein task.get · task.brief
The short id of the task whose completion spawned this one (a recurrence, D170); null on any other task.
spawned_fromstringoptionalin store.export
The uuid of the task whose completion spawned this one (D170). Omitted on any other task.
claimed_bystringnullablein task.list · task.next
Who holds a live claim on this task (D236), or null.
claimed_bystringnullablein task.get · task.brief
Who holds a live claim on this task (D236): the holder's actor key, user for a shell. Null when nobody does, or the lease has expired.
claimed_bystringoptionalin store.export
The claim's holder (D236), expired or not. Omitted on an unclaimed task.
claim_expiresstringnullablein task.list · task.next
When that claim's lease runs out (UTC); null with claimed_by.
claim_expiresstringnullablein task.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_expiresstringoptionalin store.export
When that claim's lease runs out (D236). Omitted with claimed_by.
urgency_breakdownobjectoptional
The terms urgency sums — present only when explain: true asked 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.reopen

backlog 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.

TermScore
priorityH 6.0 · M 3.9 · L 1.8 · none 0.0
due proximity12 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.
age0.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.
namestringalwaysin project.list
The project's name — the value project: filters on and task.add takes.
namestringalwaysin store.export
Its name — what a task's project names.
descriptionstringnullablein project.list
The project's description, or null.
descriptionstringnullablein store.export
Its description, or null.
archivedbooleanalwaysin project.list
Whether the project is archived. Archived projects are omitted unless include_archived asked for them.
archivedbooleanalwaysin store.export
Whether it is archived.
defaultbooleanalways
Whether this is the project a bare task.add lands 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.remove takes, as does a unique prefix of 8+ characters or its 1-based position on the task (D211).
bodystringalways
The note, stored verbatim (D41). task.get and task.brief given max_body_bytes return a prefix of a longer body and mark it with body_truncated and body_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[].idstringalwaysin task.get · task.brief
The scrubbed note's id.
removed.idstringalwaysin annotation.remove
The scrubbed note's id.
annotations_removed[].removedstringalwaysin task.get · task.brief
When it was scrubbed. Never the text — that is the point of the method.
removed.removedstringalwaysin annotation.remove
When it was scrubbed. Never the text — that is the point of the method.
On a task
annotationsarrayalwaysin task.get · task.brief
The notes on this task, oldest first within the page. Pages are counted back from the newest note, so annotations_offset: 0 is the most recent page.
annotationsarrayalwaysin store.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 annotations and its total.
delivered_annotation_idstringnullablein task.get · task.brief
The note task.done pinned as the delivery note — the newest one at the instant of completion — or null. task.reopen clears it (D165).
delivered_annotation_idstringoptionalin store.export
The note task.done pinned 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_offset asked 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.set and check.remove take.
bodystringalways
The criterion itself, stored verbatim. tasqx never runs it; it is a claim, not a command.
statestringalways
open, passed or failed. 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.
On a task
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: true is blocked ON.
unmet_blockers[].short_idintegeralwaysin task.get · task.brief
The blocking task's short id.
blocked_by[].short_idintegeralwaysin task.done
The blocking task's short id.
unmet_blockers[].titlestringalwaysin task.get · task.brief
The blocking task's title, so a refusal can be read without a second call.
blocked_by[].titlestringalwaysin task.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.
sourcestringnullablein memory.get · store.export
Where it came from — the path memory.import keyed on, or null.
sourcestringnullablein memory.list
Where it came from, or null.
titlestringalwaysin memory.get · store.export
The doc's title.
titlestringalwaysin memory.list
Its title.
bodystringalways
The whole body, stored and returned verbatim (D41).
createdstringalwaysin memory.get · store.export
When it was first stored.
createdstringalwaysin memory.list
When it was stored.
modifiedstringalwaysin memory.get · store.export
When it last changed.
modifiedstringalwaysin memory.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_rev to make a change conditional.
standingbooleanalwaysin memory.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.
standingbooleanalwaysin memory.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.import read 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::symbol refs the doc cites that are gone from the git toplevel of the caller's workdir; empty when none are, or none can be checked; null when no workdir was given (D235).
body_previewstringalways
The opening of the body, or all of it when the body is short (body_truncated says 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.get takes beside the uuid.
bodystringalways
The whole body, stored and returned verbatim.
projectstringnullable
The project it is filed under, or null.
statusstringalways
open or archived.
promoted_tostringnullable
Where note.promote filed it — task:<uuid> or memory:<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_rev to make a change conditional.
has_linksbooleanalways
Whether the body holds an http:// or https:// link — derived on every read, the same test note.list {links: true} filters on.
promoted_task_short_idintegernullable
The short id the task promoted_to names 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.

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-up graph.query takes as its root.
typestringalways
Which kind of node: task, memory, annotation or project.
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 wait shows backlog). Null on every other kind of node.
modifiedstringnullable
When the node last changed: modified for a task or a document, created for 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.
Edges
idstringalways
The edge's id, stable for the same edge across calls: dep:, ann:, member: or link: for a stored one, search: or tag: for a computed one.
fromstringalways
The node id the edge starts at. A depends_on edge 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, or search_match/shared_tag when include_inferred asked for them.
kindstringalways
structural for an edge read out of a table, inferred for 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 APICLIMCP
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, link or note.
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.

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.
kindstringalways
branch, pr or commit.
refstringalways
The branch name, the PR's URL (its link) or the merge commit's sha.
statestringalways
open, closed or merged. merged is final; a PR can move between open and closed.
createdstringalways
When the ref was first recorded.
modifiedstringalways
When state last changed. Between open and closed, the later change wins when two stores merge.
notedstringnullable
On a branch row, what git.event has 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.
On a task
git_refsarrayalwaysin task.get · task.brief
The branch, PR and merge commit git.event recorded on this task (D239), oldest first.
git_refsarrayoptionalin store.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.

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.remove takes.
toolstringalways
Which tool spent the tokens (claude-code, codex, …).
sourcestringalways
How the figure was obtained: self-report, log-parse or otel.
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, medium or low. A self-report may not claim high.
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).
On a task
tokensarrayalwaysin task.get · task.brief
Every token measurement banked against this task.
tokensarrayoptionalin store.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
}
Note

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.