0089: Task statuses, review and activity

On this page

Status: accepted

Context

Tasks were a checklist for each site plus General: a title, notes, a done tick and an agent's claim (0029, 0065). Agents could list, add, claim and tick tasks, and nothing more. In practice you plan a project's work there and several coding agents (Claude Code, Codex, Short Leash's chat) work through it at once, so you need to see what each one is doing, hear when one is stuck, check finished work before it counts as done, and undo a mistake. All of it has to keep invariant 11 (every table has a retention rule) and invariant 15 (chat asks before it acts), with one person per computer and agents acting only through tools.

Decision

  • Four statuses. A task is todo, doing, review or done (shown as To do, In progress, Review and Done), and status is the only source of truth. The old done field stays in every answer, always equal to status == done, so older callers keep working. Tasks also gain a priority (high, normal, low) and up to 10 tags (lowercase, 1 to 32 characters from [a-z0-9_/-]). The description (notes) holds up to 20,000 characters of Markdown, and its - [ ] lines (numbered 1. [ ] ones too) are the task's steps. Migration 0013 adds the columns and fills status from done and live claims.
  • Review is the default. With reviewAgentWork on ([tasks] in config.toml, Agents’ finished work goes to Review in Task settings, on unless you turn it off), an agent's finished work waits in Review. task_review carries the agent's summary and, optionally, the branch workspace the work is in (ADR 0082). An agent's task_done, or an update that says the task is done, goes to Review too, with the summary "Marked done by {agent}." Its other changes apply first, in the same transaction. An agent's update can't move a task to Review or Done by itself. You then Mark done or Send back, which takes your note and puts the task at the top of To do. The review stays on a task you mark done or move back to To do or In progress, so Undo puts it back in Review with the agent's summary and workspace; it's cleared when an agent claims the task again, when you send it back or chat finishes it, and replaced when an agent hands it back again. The app and the CLI show a review only while the task is in Review. A task you move to Review yourself gets a review of your own (by: "you", an empty summary), so the app shows Needs review with no name rather than a made-up "An agent", opens it with just its status, and nothing notifies you of your own move. With the setting off, an agent's done is final and records the agent as who finished it.
  • Chat's confirmed done finishes the task. task_done from Short Leash's chat marks the task Done directly, signed "Short Leash chat", even a task already waiting in Review, whose review it clears. Chat already asked you in its confirm ("Mark this task done?"), so Review would ask twice. A blocked task is the exception, below. Chat's name is matched trimmed and without case (ai::chat::is_chat), the same in the engine and in core.
  • "Short Leash chat" is reserved. Only Short Leash's chat signs with that name. The tools layer, and the leash command, refuse it from anyone else with "“Short Leash chat” is Short Leash's own chat. Say who you are in by, e.g. "Claude Code"." Otherwise any agent could skip Review by signing as chat. Agent names are printable ASCII only (tasks::agent_name), so a lookalike (a zero-width space after "you", a Cyrillic letter) can't pass for you or chat, and no escape reaches an activity line or a notification.
  • Agents never sign as you. The MCP tools refuse a by that's missing, empty, "you" in any case, or longer than 60 characters, with "Say who you are in by, e.g. "Claude Code"; "you" is the person using Short Leash." So does the leash command when CLAUDECODE=1 says Claude Code is running it. leash task block, release and review always need an agent's --by. task_add is the one tool that takes a missing by: older agents call it without one, so the task is recorded as added by "Coding agent". The API itself stays trusting, because it's local and leashd's own: the app sends by: "you".
  • Names are kept as written. An agent's name is trimmed and kept as it signed, with no list of known agents. Names are compared without case, to match a claim and to group Agents today, so "Codex" and "codex" are one agent.
  • An agent changes status only with its claim. An agent's update may move a task to In progress only while that agent holds the live claim ("Task {id} isn't claimed by {agent}. Claim it first."), and may not change a blocked task's status at all: only your note unblocks it. No agent can move a blocked task to Review or Done either, through task_review, task_done or a finishing update, Short Leash's chat included; each gets "Task {id} is blocked: {reason} Wait for a reply." Without this, an update could put a task In progress with no claim to run out, or move a blocked task past your answer.
  • Agents can't add to a closed list. A list closed to agents refuses an agent's new task in core, as the tools layer does, so leash task add --by is refused too.
  • Blocked ends the claim. task_block needs the claim. It records who is stuck and why (up to 500 characters), ends the claim and leaves the task In progress, shown with a red lamp. Nobody can claim it while it's blocked, and it can't run out while it waits for you. A note from you clears the block and returns the task to To do for an agent to pick up. Keeping the claim instead would let it run out after 5 minutes and open the task to another agent before anyone answered the question.
  • Claims run out on time. leashd checks every 15 seconds (defaults::TASK_CLAIM_SWEEP), and again whenever it reads a list, so nothing shows a claim that has run out. A read first checks the claims index for one that's due and takes the write lock only then. A claim that runs out returns the task to To do and raises a notification.
  • Activity lives in the database only. A task_events table holds each task's trail: added, claimed, released, claim ran out, note, step (a box an agent ticked), blocked, unblocked, review, sent back, status, done and moved, each with who and any text. It references the task with ON UPDATE CASCADE ON DELETE CASCADE, so it follows a renamed site and goes with a deleted task, and it keeps at most 200 events per task (defaults::TASK_EVENTS_KEPT), dropping the oldest on insert. Moving a task to another list copies its trail there, then adds a moved event. task_get returns the last 50 events. When the database can't be opened and Short Leash falls back to task files, there's no activity: task_get returns none and task_note refuses with "Activity needs Short Leash's database, which couldn't be opened. Restart Short Leash; if it keeps failing, see Troubleshooting." A list of events kept in the task's own row or file would rewrite the whole task on every note and grow without a natural limit.
  • Delete is soft, for Undo. Deleting marks a task deleted (an internal deletedMs, never sent), hiding it everywhere, and POST /v1/tasks/restore brings it back for 10 seconds (defaults::TASK_UNDO) with its number, order, activity and Do after links. leashd removes it for good once the 10 seconds are up. The purge also runs at start and in the 10-minute upkeep, for tasks deleted just before a restart. A restore after that is refused (404) with "Couldn't restore “{title}”: it was deleted more than 10 seconds ago." The app keeping a copy and adding it again would give the task a new number and lose its trail and links. Undo of a status change sends each task's previous status back, except that a task an agent held a live claim on comes back To do, not In progress: the claim can't come back, and an In progress task with no claim would have nobody on it and nothing to run out. A bulk change (POST /v1/tasks/bulk, across lists) is one request in one transaction, applied entirely or not at all.
  • Edits name the version they were made against. Every change bumps updatedMs, always later than before. An update may send ifUpdatedMs. If the task changed since then, it's refused (409) with "This task changed while you were editing." The app's description editor uses this to keep your unsaved text when an agent changes the description at the same time, offering Keep mine or Use theirs. Guessing in the app which changes were its own echoes lost agents' edits.
  • Done tasks are archived, and kept until deleted. The upkeep archives tasks done longer than archiveDoneDays (30 by default; 0 never archives, at most 3,650). Archived tasks are hidden from the views and the tools until you choose Show archived, and are kept until you delete them, or all of a list's with Delete archived in Task settings. Reopening an archived task brings it back.
  • Archived tasks count toward a list's 1,000. A full list refuses new tasks with "{list} has 1,000 tasks, archived ones included. Delete archived tasks in Task settings, or delete some you don't need." Leaving them out of the count would let hidden tasks grow without a bound.
  • No assignment. Tasks have no assignee, due date or estimate. Agents choose work by claiming it, and there's one person per computer, so a field naming who should do a task would have no one to enforce it.
  • Tools and their classes (ADR 0062): tasks_list and task_get read; task_add, task_note, task_update and task_done act, so chat asks first; task_claim, task_block, task_release and task_review are never offered in chat, because they're for agents working through tasks. site is optional everywhere and means General when left out.
  • Notifications. A new Tasks category, In app by default, raises one when an agent asks for review ("Codex finished “…”", "Ready for review in shop.test"), gets blocked (with the reason) or lets a claim run out ("It's open to agents again"). Each opens the task (task:<list>/<id>).

Consequences

  • An agent can't mark its own work done while Review is on, and every step it reported stays on the task until you delete it.
  • Short Leash's chat can finish a task without Review, after you confirm it in chat, even one already waiting in Review.
  • A task that left Review keeps the review it had, unseen, until an agent claims it. If you reopen it and later move it to Review yourself before anyone claims it, the agent's earlier summary shows again.
  • An agent that runs leash task without --by, outside Claude Code, is recorded as you, because the command can't tell it's an agent.
  • An agent that finishes with task_done or an update gets the generic summary "Marked done by {agent}."; task_review gives a real one.
  • A task's oldest activity is dropped after 200 events, and the file fallback keeps none.
  • Undo doesn't survive a restart within its 10 seconds: the task stays hidden and is purged.
  • Undo of a status change on a task an agent was working on shows To do where you might expect In progress.
  • Archived tasks take room in a list's 1,000 until you delete them.
  • Two agents that sign with the same name, in any case, look like one.
  • Agents' notes and summaries are stored in Short Leash's database, so the Privacy policy says how long: until the task is deleted.
  • 0065 still holds: a task waits only for tasks in its own list.