Tasks (markdown outliner, in development) (part 10)
Local control server (127.0.0.1:19519), in src/main/services/cli/cli-server-tasks-v2-routes.ts. Every handler runs the shared feature gate (tasksReadAuth / tasksMutationGate → isCliFeatureVisible('tasks-v2')) first and returns 403 with disabled: true when the feature is off — deliberately not a 404, so a caller can tell "feature off" from "id not found". On these routes a 404 means only that the id is missing.
What it is
This is part 10 of the Tasks (markdown outliner, in development) page. It carries the next stretch of the material on that page, moved here because a single page is capped at 40,000 characters.
Where to find it
Reach this part through Tasks (markdown outliner, in development) — it lists every part and explains where the feature lives in the product. Everything below is reached from the same place.
How it behaves
Everything below is the behaviour, detail and edge cases that belong to this stretch of the Tasks (markdown outliner, in development) page.
The markdown mirror (output only)
src/main/services/tasks/tasks-v2-mirror-writer.ts writes <userData>/tasks-v2.md on every change (debounced 500 ms, atomic via write-tmp-then-rename). The pure formatter is src/main/services/tasks/tasks-v2-mirror-format.ts (formatTasksV2Mirror): a nested markdown checklist, [x] for done, [ ] for actionable, no checkbox for a row snoozed into the future, with an HTML-comment metadata tail (<!--id=… due=… snooze=… ctx=…-->, with --> inside values escaped via a zero-width space).
The mirror is never read back to reconstruct task state. The DB is the single source of truth; the file is for AI agents / human eyes only. The only reader is tasks-v2:get-markdown / GET /tasks-v2/markdown, which return the file's text verbatim to a UI/agent. Mirror writes are fire-and-forget — a write failure is recorded as getLastMirrorError() for a "Mirror file out of sync" UI chip and never blocks a DB write. Writes (and the orphan-tmp cleanup in ensureValid()) are skipped entirely while the feature is off, so disabling Tasks never clobbers an existing file.
Gating mechanism
Tasks is registered as an in-development feature in the unreleased-feature registry — the 'tasks-v2' entry in src/shared/unreleased-features.ts (settingKey: 'tasksV2Enabled', envVar: AMC_SHOW_TASKS_V2, status: 'in-development'). Visibility is decided ONLY through isUnreleasedFeatureVisible (main) / isUnreleasedFeatureVisibleInRenderer (renderer):
visible = status === 'shipped' || env AMC_SHOW_TASKS_V2 === '1' || settings.tasksV2Enabled === true
No code reads settings.tasksV2Enabled directly to decide visibility — doing so would bypass the env-reveal and shipped paths and fail the gating lint. The gates: requireTasksV2Enabled() (feature-flag-gates.ts) for IPC, tasksV2Visible() for CLI, isTasksV2Visible() for the mirror writer, and the UNRELEASED_PROJECT_GATES entry in src/renderer/src/stores/project-visibility.ts for the sidebar row. To ship the feature, flip the registry status to 'shipped' (then the Lab toggle disappears and everyone sees it). Full mechanism: the unreleased-feature gating page.
For agents
When Tasks V2 is enabled, every local session receives a tasks-awareness prompt fragment (engine prompt bundle) that teaches the agent to route "add to tasks" requests to the Tasks CLI endpoints below instead of Claude Code's built-in TaskCreate/TaskUpdate tools. The fragment fires for local sessions only — SSH and Ask helpers are excluded. It uses the full-trust CLI token (not the scoped $AMC_CLI_TOKEN), matching the privacy guard on these routes.
CLI routes (/tasks-v2/*)
Local control server (127.0.0.1:19519), in src/main/services/cli/cli-server-tasks-v2-routes.ts. Every handler runs the shared feature gate (tasksReadAuth / tasksMutationGate → isCliFeatureVisible('tasks-v2')) first and returns 403 with disabled: true when the feature is off — deliberately not a 404, so a caller can tell "feature off" from "id not found". On these routes a 404 means only that the id is missing.
| Route | Auth / limit | Notes |
|---|---|---|
GET /tasks-v2/list |
bearer + 60 reads/min/token | mirrors TASKS_V2_LIST |
POST /tasks-v2/create |
bearer (central) + 10 mutations/min | 201 on success; 409 if parentId is deleted |
POST /tasks-v2/update |
bearer + 10/min | 404 if id missing |
POST /tasks-v2/complete |
bearer + 10/min | 404 if id missing |
POST /tasks-v2/delete |
bearer + 10/min | idempotent; {deletedCount} |
GET /tasks-v2/markdown |
bearer + 60 reads/min/token | text/markdown, no-store, nosniff; empty body if file absent |
GET /tasks-v2-lists/list |
bearer + 60 reads/min/token | all lists + per-list counts |
POST /tasks-v2-lists/create |
bearer + 10/min | 201; returns the new list |
POST /tasks-v2-lists/update |
bearer + 10/min | 404 if id missing; patch name / color / master context |
POST /tasks-v2-lists/delete |
bearer + 10/min | cascade soft-delete; {deletedListCount, deletedTaskCount} |
Setting a time on a task (dueAt, snoozedUntil, startAt, waitingSince, followUpAt)
Send the plain local time. Do not convert to UTC yourself. All three shapes are accepted and normalized to a UTC instant before storage:
| You send | Read as | Stored |
|---|---|---|
2026-08-18T15:00 |
3 PM in the user's own time zone ← prefer this | 2026-08-18T19:00:00.000Z |
2026-08-18T15:00:00-04:00 |
the offset you stated | 2026-08-18T19:00:00.000Z |
2026-08-18T15:00:00.000Z |
a genuine UTC instant | 2026-08-18T15:00:00.000Z |
The zoneless form is the one to reach for — it is the only shape that cannot be got wrong. The app resolves it against the user's live zone with the same wall-clock resolver alarms, scheduled messages, and cron jobs use (shared/datetime/wall-clock-zone.ts), so it stays correct across DST and across a change of machine zone.
Never append Z to a wall-clock time you have not actually converted. That is indistinguishable from a real UTC instant, so nothing downstream can catch it — the deadline is simply stored wrong and the reminder fires at the wrong hour. That is precisely how task "Thai — Tue 8/18 3:00 session" came to hold due_at = 2026-08-18T15:00:00.000Z and alert the owner at 11:00 AM Eastern, four hours early (2026-08-18).
A bare date (2026-08-18), a bare clock (15:00), and prose (tomorrow) are all rejected — these routes take a datetime, never a phrase. A client running in a different time zone from the desktop app (a phone, another machine) should send the explicit-offset form: a zoneless time is always resolved in the desktop's zone, because that is where the payload is validated.
Both /tasks-v2/reorder AND /tasks-v2-lists/reorder are deliberately not exposed — reorder is a human-curated operation, IPC-only (matches the v1 CLI surface). The Break it down (M4) interview has no CLI route at all — interview-run / interview-send spend tokens and interview-accept writes tasks, so all of it is IPC-only by design, ensuring only an in-app human action can trigger spend or create subtasks. Every list mutation route schedules a mirror write. For POST routes the central bearer-auth check in cli-server.ts's handleRequest runs before the per-handler feature gate, so an unauthenticated POST returns 401 even when the feature is on.
Related
The overview, the other parts, and everything else worth reading next all sit on Tasks (markdown outliner, in development).
Last verified 2026-09-30