You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Task resource carries its lifecycle dates, quadrant, and agent readiness. (#34) GET /api/v1/tasks/{id} and every task row now include created (the day from the Captured [YYYY-MM-DD]. note a capture writes — the store keeps no separate creation stamp, so a body edit that removes the note reports null), updated (the UTC instant of the record's stored last-write stamp, null when it has none), archived_on (the day an archived task's subtree was swept, inherited from the stamped subtree root), quadrant (Q1–Q4 from the same classifier and configured urgent_days as tasks quadrants, null for proposed, closed, and archived tasks), and agent_ready (the exact rule scope=agent_ready and tasks list --agent-ready apply, now one shared function). archived: true is unchanged. A client can sort by recently touched and render a detail pane without re-deriving any of these rules.
Activate a task over HTTP. (#28) POST /api/v1/tasks/{id}/activate is tasks activate for API clients: no body, mandatory If-Match, the task back with its new ETag, one undo step. It runs the same application operation as the CLI (which now goes through it too), so it keeps a past available-from date, clears a future one, and on a lead or recurring task releases just this occurrence — none of which PATCH {deferred: false, scheduled: null} could express. A web client's defer control now has all three of the TUI's choices. /meta advertises capabilities.activate.
Tag and note deltas on PATCH. (#29) add_tags, remove_tags, add_contexts, remove_contexts, and append_body send the store's own deltas — the ones tasks tag and tasks note use — instead of making a client read, edit, and resend the whole list or body. They are refused (422) alongside the fields they edit. A PATCH made only of deltas may send If-Match: *, so a note appended from a web form lands beside an agent's concurrent priority edit instead of failing 412; a real revision keeps its whole-task meaning, and * on anything else — a PATCH that replaces a field, or any other task write (activate, delete, the delegation actions) — is 428. A blank or whitespace-only append_body is refused, as tasks note refuses it. Appending is now a store operation (body_append) composed under the lock, which tasks note also uses. A PATCH that only nulls dates now runs the store's single date-clear operation, the one tasks undate uses. Each delta PATCH's undo step is labelled like the CLI verb it duplicates (note: …, tags: …, remove deadline: …), in GET /history as in tasks history. /meta advertises capabilities.patch_deltas.
Writes say what else they changed. (#33) Task write responses carry an optional meta.effects: rolled: {from, to} when completing a recurring task rolled it forward instead of closing it, and touched_ids when the write changed tasks besides the one it returns — a completion that cascaded DONE over open descendants, or a move that carried descendants with it. Both come from the store's own report of the transaction, not from comparing reads. DELETE ?cascade=true now answers 200 with {deleted: [...]} — every removed task, root first, as it stood before the delete, the shape tasks delete --json reports; a plain delete still answers 204.
The API routes the manager endpoints. (#25) A web client can now do what the TUI's u, ctrl-r and x do without shelling out. GET /api/v1/history peeks the next undo and redo labels with the store_revision they apply to, and POST /api/v1/history/undo and /redo require that revision back, refusing with 409 conflict (details.reason: stale_store_revision, with the current revision) when any surface wrote in between, so a stale browser tab can never undo someone else's edit; an exhausted journal and an out-of-band edit answer empty and journal_conflict. GET /api/v1/archive-preview lists the roots, descendants and every id a sweep would move, each blocked root with its open children, and a fingerprint; POST /api/v1/archive-sweeps moves them only while that fingerprint still holds, and every refusal carries the CLI's own reason word. The CLI gains the same two steps: tasks history [--json], undo/redo --store-revision REV, tasks archive --dry-run and archive --fingerprint FP, with documents byte-identical to the HTTP data. The CLI and OpenAPI vocabularies are reconciled on the CLI's names ({roots, records, moved_ids}, a blocked row list), undo --json adds store_revision, and a journal conflict now carries reason: journal_conflict. /meta advertises undo, redo and archive_sweep as true.
Change detection over HTTP is cheap, and can be pushed. (#26) GET /api/v1/meta answers If-None-Match with 304 and no body, deciding from a digest of the two files without parsing, validating or snapshotting the store, and is served Cache-Control: no-cache so a browser fetch() revalidates on its own. Its ETag is opaque and covers the document as well as the store — the revision joined to a digest of the process's configuration, build and capabilities — so restarting the API under a different zone, date order, urgent_days or link config never answers a stale 304; meta.store_revision stays the plain revision. GET /api/v1/events is live: a Server-Sent Events stream of store.changed frames carrying the new store_revision (also the event id, so a reconnecting EventSource is not told what it already knows), with a first frame on connect, a keep-alive comment when idle, at most eight open streams (503, reason: stream_limit, Retry-After beyond that), and a clean end on client disconnect or server shutdown. capabilities.events is true.
The TUI's tabs are readable over HTTP. (#23) GET /api/v1/views/{agenda,next,quadrants,inbox} is now routed. Each returns the full Task resource for every row (revision included) in the view's canonical order, plus a defined groups layout that references those rows by id: the agenda's overdue / today / tomorrow / later day buckets in the server's zone, one next group per @context (a two-context task in both) with (no context), all four quadrants Q1..Q4 with their headings, and the intake tab's two blocks — Approvals in triage order, then the accepted Inbox — each grouped by project in the Projects view's order with unfiled last. include_unavailable=true mirrors the TUI's Z toggle. The selection and grouping rules moved out of the TUI into internal/taskquery, so the tabs and the route are computed by the same functions and a web client no longer ports them. /meta advertises capabilities.views, an unknown view name is a 404 that lists the routed ones, and an unrouted method or path now says "No endpoint matches that method and path." instead of "No task with that id."
An outline read, and section actions on every section. (#24) GET /api/v1/views/outline returns the Outline tab as one flat DFS list of nodes — sections and tasks interleaved in file order, each with its shown parent and depth — so a client can rebuild the tree exactly, including where a project's tasks sit relative to its sub-sections. Section nodes carry the section's role, note, lifecycle and task tallies (shown, open, closed, and the · N closed count the toggle is hiding); task nodes carry full Task resources; include_closed=true mirrors the TUI's C toggle, and proposals stay out as they do in the tab. GET /api/v1/sections now carries kind, body, state and closed too. The project action routes — rename, complete, drop, reopen, archive — accept any section the Outline shows, not only what /projects lists, so a nested sub-section or an area with no open work is no longer a 404 over HTTP while the TUI can act on it; the Inbox and the Projects heading refuse rename, close and archive with a 409 instead, and reopen always works. That refusal lives in the application layer, so the TUI's outline rows now say why instead of renaming or closing either one, and a section action refused because the store is locked or invalid says so rather than reporting the project gone. /meta advertises capabilities.outline.
Friendly dates parse over HTTP without a write. (#27) GET /api/v1/dates/parse?input=fri%204pm reads date text with the same parser the TUI's date fields use — friendly date, optional wall time, optional trailing zone word (Europe/Berlin, UTC in any case, floating) and fold=later — and answers {"input","date","time":{"local","timezone","fold"}|null,"human"}, where time is exactly the object a PATCH/POST takes as deadline_time/scheduled_time. Text that is not a date is a 200 with {"input","error"}, like /recurrence/explain. /recurrence/explain gains anchor=YYYY-MM-DD to project from a task's own stamp instead of today, and GET /api/v1/lead/explain?input=3%20weeks&anchor=2026-11-01 previews a lead span (canonical, human, and the opens date) before any write. /meta publishes date_order. The CLI twins are tasks due --explain "<text>" (also schedule --explain) and tasks lead --explain "<span>" [--anchor <date>], whose --json is the HTTP data member. The zone/fold word grammar moved out of the TUI into temporal.ParseText, which the CLI's dated verbs now use as well, so tasks due <ref> "fri 4pm Europe/Berlin" works and a zone or fold named both as a flag and as a word is refused. /meta advertises capabilities.date_parse and capabilities.lead_explain. The TUI's date fields now honor the configured date_order — they read 10/2 as October 2 regardless of it before, and mistook a numeric date like 7/15 for a zone name.
Link shorthands and title-URL lifting work on every surface. (#30) Expanding a configured link.<name> shorthand (jira:OPS-1234) and lifting a trailing URL out of a capture title into a formal link used to be tasks capture-only. Both now live in the application layer, so POST /api/v1/tasks (links and title), PATCHformal_links, TUI captures, and the CLI apply them identically — a shorthand stores its URL with the token as its default label, and duplicates are judged on the expanded URL. /meta publishes the resolved link_shorthands and link_systems. Title lifting is on by default for HTTP creates, matching the CLI; PATCH never rewrites a title.
Bug Fixes
Concurrent formal_links edits no longer overwrite each other. (#31) The stored formal links were not part of a task's revision, so two clients could each PATCH formal_links under the same If-Match and the second silently replaced the first's list. Links are now in the revision's own-field digest and the stale write gets 412 stale_revision. Every stored task's revision changes once on upgrade, so an ETag a client held from before the upgrade must be re-read.
Task.depth counts only task ancestors. (#32) Section ancestors above the nearest section were counted too, so every top-level task in a project (a section under "Projects") reported depth: 1 while an Inbox task reported 0. Depth is now the number of task ancestors, matching what parent_id implies.