---
title: Tasks (markdown outliner, in development) (part 9)
---
# Tasks (markdown outliner, in development) (part 9)

## What it is

This is part 9 of the [Tasks (markdown outliner, in development)](tasks-v2.md) 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)](tasks-v2.md) — 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.

### Define & enrich

An eighth agent **session type**, **Define & enrich**, is the **fuller sibling of Clarify & enrich**: it not only sharpens each task's wording, it also captures the context behind it. Like Clarify it is **project-launch only** — it never appears on a single-task launch (the picker filters it via `LIST_ONLY_SESSION_TYPES`), because its whole job is to walk the _list_.

Launch it from a project's **"Launch agent…"** menu (the launch popup in list mode) and pick **Define & enrich**. The spawned agent walks the project's open tasks **one at a time** and, for **each** task that needs it, prepares **two** deliverables — then, **only after you approve that task**, saves **both**: (1) a clearer **rewrite** of the task's own text in place (`POST /tasks-v2/update`), and (2) a durable **context note** capturing what a future AI would need — what the task really is, why it matters, what "done" looks like, and the specifics that live only in your head (`POST /tasks-v2/context/create`). It **skips** tasks that are already clear and well-understood, finishes one task before the next, and — where a task needs only one of the two — does just that one. Where it helps it also offers to break a task into subtasks (`POST /tasks-v2/create`). It handles an empty / already-clear list ("nothing to do") and a long, truncated list (works what it can see, offers to continue) gracefully.

Where **Clarify & enrich** treats the context note as optional (the clearer description is the point), **Define & enrich** makes the context note **first-class** — reach for it when you want every task both legible AND fully briefed for a future agent. Under the hood it reuses the same machinery as Clarify: one main-only preamble (`DEFINE_PREAMBLE`, never shipped to the renderer), the hidden/visible split, and the existing `buildListInterviewPrompt` + `tasks-v2:launch-list-agent` channels — **no new IPC channel, Zod schema, DB table, or Labs toggle** (it rides the existing Tasks flag). Like the other groomer, the project prompt's hidden background task list carries each task's **id**, and its session-type-aware footer names **both** write routes. Still behind the Tasks Labs flag; invariants (`define-groomer`, `define-list-only`) live in [`.claude/memory/contracts/tasks-v2-session-types-contract.md`](/.claude/memory/contracts/tasks-v2-session-types-contract.md).

### Context attachments

Any task can carry a small pile of **context attachments** — pointers to other sessions, projects, or tasks, plus plain URLs, free-form text/markdown, **an attached file**, and **a linked ContextDock bundle/list** — so the work and everything it refers to stay together. The **📎 count chip** that appears on any row with attachments is **clickable** — tapping it pops open a small **peek** listing those attachments, and clicking any one opens it right there (a URL in your browser; a session, project, task, or file in the app; a note or ContextDock bundle in a read-only content viewer), with a **Manage** link at the bottom that opens the full pane for adding or removing. On a focused outliner row, the **📎 Context action** (in the row's **⋯** overflow menu, or the **`i` key**) opens the full **Context pane** — and the SAME manager is now also a **Context tab** in the task's two-tab **details pane** (`Cmd/Ctrl+;`), so a task's fields and its context are editable in one window. (A task's old single free-form "Context" note was folded into this system as a text entry — nothing is lost, and the row now shows just the 📎 count chip instead of the note's raw `@…` text.) The pane / tab lets you:

- **Attach** anything via **+ Add context** → an **iconed list** of types:
  - **Session / Project / Task** — pick the entity from a searchable list (the **task picker spans every list**, each option labeled with its list, so you can link to a task in another list).
  - **URL** — `http(s)` only (it opens via the OS browser, so other schemes are rejected at the input).
  - **Text / Markdown** — a real **multi-line editor** (plain Enter is a newline; **Ctrl/⌘ + Enter** or the **Add** button saves it).
  - **File** — opens your OS file picker and attaches the chosen file by path; the launched agent reads it. (Desktop only — hidden on the mobile web app, where the native picker can't run. The link is to the file where it sits, so moving or deleting the file breaks it.)
  - **ContextDock** — a searchable list of your **cached ContextDock bundles/lists** (the same curated doc sets you add to projects). Empty / not set up shows a "set up ContextDock in Settings" hint.
- **Give it a label (optional).** Every type has an **optional Label** field when you add it — leave it blank and it names itself (the entity's name, the URL, or the note's first line, exactly as before); fill it (usually the AI does) to give the attachment a clean title, so a task can carry several clearly-labeled contexts instead of one indistinct pile.
- **Click an attachment to navigate / open** — a session opens that chat, a project switches to it, a linked **task jumps to its list** (even one in another list; a deleted target says so instead of dead-ending), a URL opens externally, a **file opens in its default app** (with a folder button to reveal it); and a **note or ContextDock item opens a read-only content viewer** (`TaskV2ContextContentModal`) showing its saved text / materialized markdown — there's no target to jump to, so the content itself is the thing you see.
- **Refresh** a ContextDock attachment from its row to re-pull the latest docs (the staleness mirror of a project doc-row refresh).
- **Edit** an attachment with the **pencil** on its row, or the **Edit** button in the read-only viewer — a **text/markdown note edits its label AND its body** (add more detail without deleting and re-adding); **every other kind renames its label**. Save with **Ctrl/⌘ + Enter** or the Save button; ContextDock stays rename-only (its content comes from the bundle).
- **Remove** an attachment with the trash button. Removal is **undoable**: a toast appears with **Undo** that re-adds the item (the lightweight Milestone 3 undo — a persisted, cross-restart history is a later milestone).

**The express path — "Attach a session".** Linking a session is the common case, so the **right-click menu** carries a dedicated **"Attach a session"** item that opens this Context pane **straight at the session picker**, skipping the five-step `⋯ → Context → + Add → Session` dig. (It used to also be an on-row icon beside Launch agent; that icon was retired to declutter the row — the express capability now lives on the right-click menu, while the longer `⋯ → Context → Session` path still works too.) Picking a session creates the same `session`-kind attachment; an empty session list shows a friendly **"No sessions yet"**. (A session you **launched for a task** via **Launch agent** / `g` is attached to that task **automatically** now, so you rarely need this for one — see [Per-task agent chat](#per-task-agent-chat).) Under the hood it calls `openContextPane(id, { addKind: 'session' })`, which seeds a `contextInitialAddKind` the pane consumes once (a later manual "+ Add context" returns to the normal type picker). Both editable views wire the right-click item via `TaskV2RowContextMenu.tsx`, so they can't diverge. (`TaskV2RowContextMenu.tsx`; the pane's optional `initialKind` lives in `TaskV2ContextAddMenu.tsx`. See the contract's `context-attach-session-icon` invariant.)

**The reverse — "Attach to task…" from the session side.** You can also start from the **session**: an **"Attach to task…"** item on a session's menus opens a small **task picker**, and picking a task creates the very same `session`-kind link. It lives in two places (both shown only when Tasks is on): **right-click a session in the sidebar** (single selection), and the **open-session header ⋯ menu** — that second one renders on mobile too, so it's the phone entry point. The picker is one searchable modal (a full-screen sheet on a phone) with a fixed **"Attach to a task"** title — the session rides in a chip below it, so a long session name never truncates the title — listing your active tasks **grouped under their list header** (so same-named tasks in different lists read distinctly). Three redesign touches: a **project filter** (a scope dropdown — a task has no project of its own, so its project = its list's `defaultProjectId`; shown only when a task actually belongs to a project, and defaults to the session's own project when that project has tasks, else "All projects"), **readable rows** (a task whose text is a bare or markdown URL shows a friendly label — "Loom recording", "Shared artifact", else the hostname — with the raw link demoted to a quiet second line, never a chopped URL), and a pinned **"New task from this session"** action that creates an unfiled task (→ Inbox) titled from the session and attaches it in one click (one Undo removes the whole thing). **Keyboard navigation** stays — arrow ↑/↓ to move the highlight over the tasks and Enter to attach, without the mouse (the New-task button sits outside that cycle); it dedups ("Already attached"), confirms with an **Undo** toast, and reuses the unchanged `TASKS_V2_CONTEXT_CREATE` (the New-task action also uses `TASKS_V2_CREATE`) — no new table, channel, or setting. This is separate from a session's `taskId` (the "launched-from-this-task" binding). See the contract's `session-attach-to-task` invariant (`AttachSessionToTaskPicker.tsx` / `attach-session-to-task.ts` / `attach-picker-labels.ts` / `attach-picker-project-filter.ts`).

A row with attachments shows a small **📎 count chip** (a quiet, clickable trailing indicator — it opens the attachment peek, see above — on **every** row that carries context — the focused/selected one included — with the hover action icons revealing to its right rather than covering it; the ⋯ menu's Context action also shows the count as a label), so you can see at a glance which tasks carry context. When a task **launches its agent**, its attachments are rendered into the launch prompt: the pointer kinds (session / project / task / url / note / **file** — a file shows its path + a "read it with your Read tool" hint) become a compact **`## Attached context`** list, and each **ContextDock** item's curated content is inlined in full under a separate **`## Reference docs`** section (capped so one bundle can't balloon the brief) — so the agent boots already aware of what the task points at AND carrying the curated docs.

Attachments persist in their own **`task_context_items`** table (one row per attachment, soft-deleted on remove, fractionally ordered per task). **It ships the `session | project | task | url | note | file | contextdock` kinds.** A **ContextDock** attachment is **materialized at add-time** — because the agent has no way to fetch a bundle on its own, Omniscio pulls the content (a bundle → its assembled docs, budget-capped; a list → a titles index) and stores it on the attachment, exactly how a project materializes a linked bundle (Refresh re-pulls it). Deferred to later milestones: the `inbox` / `image` kinds (images fold into `file`), rendering attachments into the `tasks-v2.md` mirror (Milestone 6), and a persisted activity-log + cross-restart undo history (Milestone 5). Tasks accepts the full set of `file` + `contextdock` kinds alongside the original five. Like the rest of Tasks, the whole surface stays behind the `tasks-v2` Labs gate.

Context has its own gated IPC channels (constants in `src/shared/ipc-channels/productivity.ts`, handlers in `src/main/ipc/tasks-v2-context-handlers.ts`):

| Constant                     | Channel string               | Purpose                                                                                                                                                                                                                                                   |
| ---------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TASKS_V2_CONTEXT_LIST`      | `tasks-v2-context:list`      | a task's attachments, by `sort_order`                                                                                                                                                                                                                     |
| `TASKS_V2_CONTEXT_COUNTS`    | `tasks-v2-context:counts`    | per-task attachment counts (the row chips)                                                                                                                                                                                                                |
| `TASKS_V2_CONTEXT_CREATE`    | `tasks-v2-context:create`    | attach an item; returns it                                                                                                                                                                                                                                |
| `TASKS_V2_CONTEXT_UPDATE`    | `tasks-v2-context:update`    | patch an attachment's label / payload / rel — driven by the pane's **Edit pencil** + the content viewer's **Edit** button (`TaskV2ContextEditModal` → the store's `updateContextItem`): a note edits its label + body, every other kind renames its label |
| `TASKS_V2_CONTEXT_DELETE`    | `tasks-v2-context:delete`    | soft-delete an attachment; `{deletedCount}`                                                                                                                                                                                                               |
| `TASKS_V2_CONTEXT_REORDER`   | `tasks-v2-context:reorder`   | reposition within a task (fractional); IPC-only                                                                                                                                                                                                           |
| `TASKS_V2_CONTEXT_OPEN_FILE` | `tasks-v2-context:open-file` | open / reveal a `file` item (unscoped + validated)                                                                                                                                                                                                        |
| `TASKS_V2_CONTEXT_REFRESH`   | `tasks-v2-context:refresh`   | re-materialize a `contextdock` item from its bundle/list                                                                                                                                                                                                  |
| `TASKS_V2_CONTEXT_CHANGED`   | `tasks-v2-context:changed`   | push — cache-invalidate, strict empty payload                                                                                                                                                                                                             |

Every mutation emits `TASKS_V2_CONTEXT_CHANGED` **after** the DB commit (delete only when it removed a row). Unlike the _list_ handlers, context mutations **do not** schedule a markdown-mirror write — context isn't in `tasks-v2.md` until Milestone 6.

Three of these are also reachable over the CLI (in `src/main/services/cli/cli-server-tasks-v2-routes.ts`), each **403 + `disabled: true` when the feature is off**:

| Route                           | Auth / limit                        | Notes                                                                                     |
| ------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------- |
| `GET /tasks-v2/context/list`    | bearer + 60 reads/min/token         | `?taskId=` required (400 if missing)                                                      |
| `POST /tasks-v2/context/create` | bearer (central) + 10 mutations/min | 201 + the created item; blank label → 400; **`file` + `contextdock` rejected** (app-only) |
| `POST /tasks-v2/context/delete` | bearer + 10/min                     | idempotent; `{deletedCount}`                                                              |

**Reorder and update are deliberately IPC-only** (human-curated, not exposed over CLI — matching `/tasks-v2/reorder`).

### Cascading context — list → subtree → task (M3)

Context is **layered and inherited down the tree**. Three tiers feed a task:

1. the **list's master context** (the whole-list note),
2. a **subtree** tier — context attached to an _ancestor task_ and marked **"also apply to sub-items"** cascades down to every descendant, and
3. the task's **own** attachments.

When you add context you can flip an **"Also apply to sub-items (cascade)"** toggle (it sets the item's `inherit` flag); cascading items show a **"cascades" badge** on their row. The context pane shows a read-only **"Inherited"** section listing exactly what the task inherits and _where each piece came from_ ("From list …", "from `<parent task>`") — so inheritance is always **visible**, never a surprise.

It is **bounded** end to end so it can never balloon an AI prompt: the resolver (`resolveTaskContext`) walks ancestors via a **cycle-safe, depth-capped** recursive CTE and caps inherited items (12, with a `truncated` flag); the agent brief truncates each inherited item and caps the section; and the "Right now" prioritizer is fed only a **single length-capped shared preamble** (the eligible tasks' list master contexts), not per-task inherited lines. The `TASKS_V2_CONTEXT_RESOLVE` channel (read-only, IPC-only, gated) returns the resolved context to the pane, and the **same resolver** briefs a launched agent (a new `## Inherited context` section). Editing a master context invalidates the prioritizer's cached pick — the cache key folds in a content fingerprint of the preamble. The `inherit` flag lives on `task_context_items` (migration `20260608052857-…`); existing items default to task-only.

### Attachments — paste, drop, or attach files to a task

Distinct from the context **references** above, a task also holds **stored file attachments** — real **copies** of images and any other files, shown in an **Attachments** strip inside the same Context pane. Three ways to add them: **paste** an image (Ctrl/Cmd+V while the pane is open), **drag-and-drop** files onto the strip, or the **"+" picker** (a plain file picker, so a phone offers its camera/gallery). **Any file type** is accepted, up to **50 MB** each.

Because they're **copies** (not links), a pasted screenshot — which has no file on disk to link to — sticks, and an attached file survives you moving or deleting the original. Images render as **thumbnails**; click one to view it full-size in the in-app lightbox and arrow through the image set. Every other file renders as a **name + size pill**; clicking a document opens it in your OS default app, while an unknown or program-type file is **revealed in its folder** rather than launched — so a double-click can never run an attached executable. **Remove** asks you to confirm.

It works on **mobile** (the images load over the same token-gated `/attachment/…` route the chat uses, and the picker is the phone's file/camera picker) and in both light and dark themes. Under the hood the bytes are copied into `<userData>/attachments/<taskId>/` — reusing the same hardened store the chat composer uses (the 50 MB cap + type-safety sniff live there) — and recorded in a `task_attachments` table (migration `20260724042407-…`). They are deliberately **not** fed into a launched agent's brief; that stays the job of the context references above. Full invariants + the safe-open / web-bridge / soft-delete rules: [`tasks-v2-attachments-contract.md`](../../.claude/memory/contracts/tasks-v2-attachments-contract.md).

## For agents

### IPC channels (`TASKS_V2_*`)

Defined in `src/shared/ipc-channels/productivity.ts`; handlers in `src/main/ipc/tasks-v2-handlers.ts` (all behind `requireTasksV2Enabled()`); request/response schemas in `src/shared/ipc-schemas/tasks-v2.ts`.

| Constant                      | Channel string                | Purpose                                                                                                                                                                                                                                                                                                          |
| ----------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TASKS_V2_LIST`               | `tasks-v2:list`               | list children of a parent (`parentId: null` = roots, omitted = whole tree)                                                                                                                                                                                                                                       |
| `TASKS_V2_CREATE`             | `tasks-v2:create`             | append/insert a row; returns the created `Task`                                                                                                                                                                                                                                                                  |
| `TASKS_V2_UPDATE`             | `tasks-v2:update`             | patch `{text, dueAt, snoozedUntil, dueNotifiedAt, metadata, importance, urgency, startAt}`; returns the row                                                                                                                                                                                                      |
| `TASKS_V2_COMPLETE`           | `tasks-v2:complete`           | flip `done`; cascades through undone descendants when set true                                                                                                                                                                                                                                                   |
| `TASKS_V2_DELETE`             | `tasks-v2:delete`             | soft-cascade delete; returns `{deletedCount}`                                                                                                                                                                                                                                                                    |
| `TASKS_V2_REORDER`            | `tasks-v2:reorder`            | reparent + reposition; void; **push-only, no mirror write**                                                                                                                                                                                                                                                      |
| `TASKS_V2_INDENT`             | `tasks-v2:indent`             | the v2 Tab — indent a row alone, leaving its sub-items at depth (payload `{id}`; server computes the move); void; **push-only, no mirror write**                                                                                                                                                                 |
| `TASKS_V2_MOVE_TO_LIST`       | `tasks-v2:move-to-list`       | re-file a row + its subtree into another list; emits + writes the mirror                                                                                                                                                                                                                                         |
| `TASKS_V2_GET_MARKDOWN`       | `tasks-v2:get-markdown`       | return current `tasks-v2.md` text (empty string on ENOENT)                                                                                                                                                                                                                                                       |
| `TASKS_V2_LAUNCH_AGENT`       | `tasks-v2:launch-agent`       | spawn (or reuse) a Claude agent session bound to a task; returns `{sessionId}`. The IPC variant accepts an optional `promptOverride` (the popup's edited text, used verbatim); the CLI route omits it                                                                                                            |
| `TASKS_V2_BUILD_AGENT_PROMPT` | `tasks-v2:build-agent-prompt` | preview the launch popup's **visible** message (`prompt`) AND the **hidden** appended context (`context`, steered by the optional `sessionType`) for the collapsed "what the agent will also receive" disclosure — **free, read-only, no spawn**; same shared `buildTaskAgentPrompt` as launch. Carries no token |
| `TASKS_V2_GET_SESSION`        | `tasks-v2:get-session`        | the task's linked session id (or `null`) — the free-reopen lookup                                                                                                                                                                                                                                                |
| `TASKS_V2_INTERVIEW_GET`      | `tasks-v2:interview-get`      | the task's breakdown thread (or `null`) — free read                                                                                                                                                                                                                                                              |
| `TASKS_V2_INTERVIEW_RUN`      | `tasks-v2:interview-run`      | open/generate the breakdown interview for a task — **PAID** (bounded, capped); friendly error over-cap                                                                                                                                                                                                           |
| `TASKS_V2_INTERVIEW_SEND`     | `tasks-v2:interview-send`     | send a user turn, get a bounded AI reply + refreshed proposal — **PAID**                                                                                                                                                                                                                                         |
| `TASKS_V2_INTERVIEW_ACCEPT`   | `tasks-v2:interview-accept`   | write the accepted (edited) subtasks under the task (atomic + idempotent) — free; the ONLY subtask-write path; emits `TASKS_V2_CHANGED`                                                                                                                                                                          |
| `TASKS_V2_FETCH_VIDEO_TITLE`  | `tasks-v2:fetch-video-title`  | resolve a clean YouTube/Vimeo URL to the video's title via public oEmbed (backs paste-a-link → title) — **free, read-only, no key/AI spend**; own UNgated-by-KMS channel reusing `fetchVideoTitle`; humanized failure → renderer keeps the URL (handler `ipc/tasks-v2/video-title.ts`)                           |
| `TASKS_V2_CHANGED`            | `tasks-v2:changed`            | push event — cache-invalidate fan-out, strict empty payload                                                                                                                                                                                                                                                      |

Every mutation emits `TASKS_V2_CHANGED` exactly once **after** the DB write commits. The payload is an empty object (schema in `src/shared/push-event-schemas/tasks-v2.ts`); the renderer reacts by refetching the whole tree, never by splicing a delta. Exceptions to the fan-out tail: **reorder** emits the push but schedules no mirror write; **indent** likewise schedules no mirror write and emits only when it actually moved a row (`movedCount > 0`); **delete** emits the push + schedules the mirror only when `deletedCount > 0`; **move-to-list** writes the mirror (it changes which list-heading a task lives under).

## Related

The overview, the other parts, and everything else worth reading next all sit on [Tasks (markdown outliner, in development)](tasks-v2.md).
