TASK_KANBAN_DESIGN.md29.9 KBView on GitHub # Task Kanban — Design
A kanban of **tasks**: one card per task, **one column per task group**, plus an **Upcoming**
column for tasks that aren't due yet. Lives at `/tasks/kanban` as a sibling of the agenda. Plus a
task-group accordion in the left sidepanel, task-group icons, an agent-maintained deal priority
for ordering, and an AI-cleanup affordance for the stale backlog.
> **Revised 2026-07-21** for the landed `agenda-current-future-restructure`
> ([design doc](../../docs/agenda-current-future-restructure.md), [wiki](../../docs/wiki/agenda.md)),
> which replaced per-day agendas with two documents and deleted `AgendaDateNav`. That refactor
> changes the *agenda* and a handful of file references this doc depends on — it does **not**
> change the kanban's design. The board is its own surface, reading `listUserTasks` directly.
---
## Columns
```
┌ Top deal tasks ┐ ┌ Responses ┐ ┌ Post-meeting ┐ … ┌ Misc ┐ ┌ Upcoming ┐
│ [card] │ │ [card] │ │ [card] │ │ card │ │ [card] │
│ [card] │ │ │ │ │ │ card │ │ [card] │
└────────────────┘ └───────────┘ └──────────────┘ └──────┘ └──────────┘
← task groups in `position` order, Misc last → + always Upcoming
```
- **One column per task group**, in `position` order, Misc last — straight from
`taskGroups.listGroups`, which already appends the virtual Misc lane.
- **Plus an always-present "Upcoming" column** for tasks not yet due.
- **Placement:** `dueDate <= end of today` (due or overdue) → its **group column**;
`dueDate > today` → **Upcoming**, regardless of group.
- **Ordering:** `dueDate` asc everywhere to start (most overdue on top; Upcoming chronological).
Ranking group columns by deal priority is **deferred** — see Ordering. Build and render first.
The board has **no Current/Future toggle** — it shows everything at once, split across columns.
(`Current | Future` belongs to the agenda view only.)
> The agenda's Future document buckets upcoming tasks into relative bands via
> `bandForDueDate` ([future-agenda-reconciler.ts](../../../server/src/services/agenda/future-agenda-reconciler.ts)).
> Not used here — Upcoming is one chronological column. Noted only in case we ever want to split
> it into several band columns.
---
## Decisions (locked)
- **`/tasks` is the surface; Agenda and Kanban are sibling routes.** Sidebar Tasks button → `/tasks`
(today `/agenda`).
- `/tasks/agenda` — the agenda, unchanged (keeps its own `Current | Future` toggle internally).
- `/tasks/kanban` — the board.
- `/tasks` — redirects to whichever you had open last (localStorage, default `agenda`), via
`<Navigate replace>` so it leaves no history entry.
- `/agenda` → redirect to `/tasks` preserving query.
- **One switch in the greeting row:** `Good morning, Jesse ⟷ [ Agenda | Kanban ]`. The agenda's
existing `Current | Future` toggle stays in its title row, *inside* the agenda tab — the two
never appear as competing controls because the kanban has no surface axis.
- **Card = one task.** No merge by conversation. (Also aligns with the restructure retiring
`conversationGroup` in favour of flat tasks with inline `@` chips.)
- **Drag** → `taskGroups.moveTaskToGroup({ taskId, groupId })`. ⚠️ The param is **`groupId`**, not
`taskGroupId` ([task-groups.ts:242](../../../server/src/trpc/routes/task-groups.ts)).
- **The board reads `listUserTasks` directly, unfloored** — see Backlog visibility.
---
## Backlog visibility — the board's reason to exist
`fetchActiveDailyTasks` ([documents.ts:91-152](../../../server/src/trpc/routes/documents.ts)) uses
`CURRENT_LOOKBACK_DAYS = 30`. Note the restructure's *design doc* says Current has "no lower
bound"; **the implementation kept the floor.** Measured on jesse (2026-07-21):
| Surface | Tasks | Range |
|---|---|---|
| **Invisible** — older than the 30-day floor | **198 (88%)** | 2025-11-20 → 2026-06-17 |
| Current doc | 14 | 2026-06-22 → 2026-07-21 |
| Future doc | 13 | 2026-07-22 → 2026-09-17 |
So 88% of open tasks appear in **neither** document. They aren't stale-but-shown; they're silently
excluded, and nothing ever sweeps them.
The board is unaffected by that floor — it reads `listUserTasks` directly, so it shows all 225 open
tasks. This is why the board matters: it is the only place the 198 invisible tasks are visible and
triageable, and it's what gives the overflow cleanup something to operate on.
---
## Card anatomy
One card = one task. Header is conversation context; body is the single task.
```
┌──────────────────────────────────────────┐
│ [avatar] Acme Corp │
│ last contact: 3d · next steps: Fri │
│ ☐ Send pricing follow-up [gmail] │
└──────────────────────────────────────────┘
```
Reused primitives (all verified present):
- [ConversationCompanyAvatar](../../conversationsPage/components/ConversationCompanyAvatar.tsx) — note its per-id `crm.getConversation` fetch is the N+1 the enrichment join avoids.
- [RelativeDateBadge](../../../components/ui/relative-date-badge.tsx) — `colorType='history'` / `'scheduled'`.
- [AgendaCheckbox](../../agentCanvas/components/AgendaCheckbox.tsx) — unchanged.
- `ChannelIcon` — inline at [TaskGroupDetailSidebar.tsx:13-21](components/TaskGroupDetailSidebar.tsx); extract.
- `openTask` — inline at [TaskGroupDetailSidebar.tsx:57-67](components/TaskGroupDetailSidebar.tsx); extract.
---
## `tasks-kanban.tsx` — a relic, not a foundation
There is already a task kanban in this module. **It is dead code from a previous era.** The history:
- It belonged to the old **`/tasks` route**, which was **deleted** in `2aea4965e`
*"remove /tasks route + dead nav (design: task-groups phase 6)"*.
- Its columns are **`taskType`** (response · follow-up · post-meeting · pre-meeting · reactivation)
— the categorization that **task groups replaced**. It predates task groups entirely.
- TASK_GROUPS_DESIGN §Phase 6 justified keeping these components because they were "imported by
`threads/mail.tsx` and the agenda." **That is no longer true** — `TasksKanban` and `TasksView`
are imported by *nothing*; `mail.tsx` only carries a stale comment mentioning the file.
- Last substantive change was the `[tasks]` era; since then only a mechanical store refactor.
The orphaned cluster, ~1,581 lines:
| File | Lines | Imported by |
|---|---|---|
| [tasks-kanban.tsx](components/tasks-kanban.tsx) | 345 | nothing |
| [tasks-view.tsx](components/tasks-view.tsx) | 299 | nothing |
| [sections/task-kanban-card.tsx](components/sections/task-kanban-card.tsx) | 495 | only `tasks-kanban.tsx` |
| [hooks/use-tasks-view.ts](hooks/use-tasks-view.ts) | 442 | only the two dead views |
**Decision: build fresh, delete the relic.** Its column model is conceptually superseded, its data
hook (`useTasksView`) is built around the old date-window view, and inheriting it would mean
carrying 1,581 lines of pre-task-group assumptions. Deleting also frees the natural names
(`TaskKanbanBoard` / `TaskKanbanCard`), so no awkward aliasing.
> ⚠️ **Re-creating `/tasks` reanimates a dead branch.**
> [thread-display.tsx:177](../../threads/thread/components/thread-display.tsx) still tests
> `location.pathname.startsWith('/tasks')` to set `showTaskSelector` — inert since the route was
> removed, but it will switch back on the moment `/tasks` exists. Verify it's still the behavior
> we want, or delete it.
Also reusable, discovered during validation:
- [use-task-group-data.ts](../../agentCanvas/hooks/use-task-group-data.ts) — section key → group name/color/count off the shared `listGroups` cache. Use for column headers.
- [TaskGroupCollapseContext](../../agentCanvas/context/TaskGroupCollapseContext.tsx) — existing collapse plumbing; prefer it over new store state for the accordion.
---
## Ordering — SHIPPED
Four modes, chosen from the toolbar's Ordering row and sorted by one comparator
([task-order.ts](utils/task-order.ts)) shared by the board and the list, so the two surfaces cannot
disagree about what a mode means:
| Mode | Sorts by |
|---|---|
| Due date (latest) — default | `dueDate` descending |
| Due date | `dueDate` ascending |
| Recently added | `createdAt` descending |
| Manual | the row's own `sortOrder`, set by dragging |
Three of the four are LIVE reads of a field: change a due date under "Due date (latest)" and the
card moves. `manual` is the only one that reads stored state, and therefore the only one a vertical
drag can change — see [TASK_REORDERING_DESIGN.md](TASK_REORDERING_DESIGN.md) for the schema, the
placement trigger, and how a drop resolves to a single number.
### Not built: agent-maintained `priorityScore`
An earlier plan for this section. Superseded by the manual ordering above, which gives the user the
control this was reaching for without an agent rubric or a backfill; kept for the conversation-data
finding, which stands on its own.
Rank group columns by one number on the conversation: **`priorityScore` (1–10)**, maintained by
`@crm-updater` against a rubric. Rationale: the existing `priority` enum is **null on 78% of
conversations** and dirty where set (`low/medium/high/urgent` plus casing drift), so it cannot
carry ordering.
Worth knowing: the agenda sorts by `CONVERSATION_PRIORITY_ORDER` (urgent 0, high 1, medium 2,
low 3, unknown 4) off that same enum, in both reconcilers. Migrating the agenda onto `priorityScore`
too is optional and **out of scope here** — the board owns its own sort. Noted only so the two
aren't accidentally assumed to match.
- **Field:** `crmConversations.priorityScore` int 1–10 nullable, plus
`priorityScoreSource: 'human' | 'agent'`. New numeric field rather than repurposing the enum
(still consumed by the conversation list, kanban, and `routeTaskToGroup`).
- **Rubric:** *10 = strategic must-win (big/hot company, late stage, exec interest); 7–9 = strong
active deal; 4–6 = normal open deal; 1–3 = low-fit, stalled, nurture.* Named inputs: company
size/growth/funding, stage, ACV when present, strategic fit, responsiveness. The agent folds in
the enrichment signals — no hardcoded formula.
- **Human lock:** no field provenance exists anywhere today. A human-set score stamps
`'human'`; `@crm-updater` must never overwrite it.
- **Unscored (null) sorts last** until first touched.
---
## Data model + headless surface
`listUserTasks` ([user-tasks.ts:43-156](../../../server/src/trpc/routes/user-tasks.ts)) is a bare
`findMany` returning full raw rows — so `taskGroupId`, `dueDate`, `taskChannel`, `taskActionData`
already come through. Two gaps:
1. `HydratedUserTask` doesn't declare `taskGroupId` ([userTasksSlice.ts](slice/userTasksSlice.ts)) — add it.
2. No conversation enrichment. Add a left join returning:
```
conversation: { companyName, logoUrl, lastContactedAt, nextSteps }
// priorityScore joins later, in the deferred ordering phase
```
**Headless driver — reuse, don't rebuild.** The restructure already shipped
[apps/server/src/cli/agenda.ts](../../../server/src/cli/agenda.ts): a thin tRPC-HTTP client with
`dump` / `tree` / `json` / `tasks`, whose exported `outline()` and `tally()` are the designated
structural assertion surface. The board's headless verb extends **this file** (no DB handle, per
[cli-over-http.md](../../../server/docs/cli-over-http.md)).
> 🚨 **Port trap — every headless check must pin the URL.** Several checkouts run side by side;
> this one (`cedar-mail-2`) serves **8790/8791/8792**, but `~/.cedar-cli.json` pins `baseUrl`
> machine-globally and *outranks* the repo env. A bare `pnpm cedar-cli` here silently talks to
> **another checkout's server** against the same DB — HTTP 200, real ids, wrong code. Always:
> ```
> CEDAR_API_URL=http://localhost:8790 pnpm cedar-cli …
> ```
> The dev server also runs as a built bundle (`dist/api-service/index.cjs`), so a source edit is
> only live once `dev-runtime.mjs` rebuilds.
---
## Task groups — seeding, icons, backfill
[seedDefaultTaskGroups.ts](../../../server/src/services/task-groups/seedDefaultTaskGroups.ts)
defines 4 groups `{ name, color, routingCriteria }`, idempotent by lowercased name, called on
signup and by a migration script.
**All other users' groups were deleted (580 rows, 145 users) to experiment on one account.
Only jesse has groups (4).** Rollout to everyone is the final phase.
### Seed — 4 → 6 lanes, with icons
| Group | Icon | Note |
|---|---|---|
| Top deal tasks | `Trophy` | renamed from "Top deals" |
| Responses | `Reply` | existing |
| Post-meeting followups | `CalendarCheck` | existing |
| Follow-ups | `Send` | existing |
| Reschedule | `CalendarClock` | new — needs `routingCriteria` |
| Reactivation | `Zap` | new — needs `routingCriteria` |
`task_groups` has `color` but **no `icon`** ([aop-schema.ts:1239-1280](../../../server/src/db/aop-schema.ts)) — add `icon: text('icon')` storing a lucide name, resolved by a
`TASK_GROUP_ICON_MAP` mirroring [`STATUS_ICON_MAP`](../../crm/utils/index.ts#L440) (which already
exports `Trophy` and `Zap`). Consumers: [TaskGroupsNav](components/TaskGroupsNav.tsx) (hardcodes
`ListTodo` at :42; **two** link sites at :113 and :153), the accordion, and board column headers.
> The rename duplicate-trap from the earlier draft is now **moot** — with only jesse's rows left,
> there is no fleet to duplicate against. Rename his `Top deals` row directly.
### Backfill
[`backfillRecentTasks`](../../../server/src/trpc/routes/task-groups.ts) already routes ungrouped
`todo` tasks via `routeTaskToGroup` (Haiku, concurrency 8). Changes:
- Add a **last-month date filter** (today it only has `limit: 1..100`). Jesse: ~22 created / 29 due
in the past month — ~25 Haiku calls, no batching needed.
- Replace `forceAssign: true` with **confidence gating** — for a stale backlog, forced assignment
pours junk into the good lanes. Low confidence stays in Misc.
- Tasks older than a month stay Misc — that's the overflow-cleanup population.
---
## Column overflow — AI cleanup
Aimed at the **198 invisible tasks**, which only the unfloored board surfaces. When a column
exceeds 30 tasks, render the top ~30 and collapse the tail into a pinned footer button:
```
+ 34 tasks · more than a week overdue
Ask AI to clean up — mark closed-lost or run
```
onClick → an AI overdue-sweep scoped to those task ids (mark deals closed-lost / execute / dismiss),
honoring each group's `overduePolicy`. This is the concrete UI for TASK_GROUPS_DESIGN's deferred
overdue sweep.
---
## Left sidepanel — all-groups accordion
Replace the single-group drill-in ([TaskGroupDetailSidebar.tsx](components/TaskGroupDetailSidebar.tsx))
with every group in one scrollable column, sticky headers top **and** bottom, rendering the same
card as the board. Reuse `TaskGroupCollapseContext` rather than adding store state; `?group=`
auto-expands.
---
## Scrollbar polish
**Confirmed statically — no devtools needed.** The persistent bar is the *horizontal board*
container at [KanbanConversationCanvas.tsx:451](../../crm/components/kanban-canvas/KanbanConversationCanvas.tsx),
which sets an explicitly-visible `[scrollbar-color:rgba(156,163,175,0.8)_transparent]`. The
per-column lists at [CanvasKanbanColumn.tsx:70](../../crm/components/kanban-canvas/CanvasKanbanColumn.tsx)
are already `scrollbar-none` — they were never the culprit.
Fix: a `useScrollActive` hook (add `scrolling` class on scroll, remove ~800ms after idle) applied
to that horizontal container; default transparent thumb, visible only while `.scrolling`.
---
## Critical files
| File | Change |
|---|---|
| [AgendaHome.tsx](../../agentCanvas/components/AgendaHome.tsx) | split: frame + greeting → `tasks/layout.tsx`; body (`AgendaViewToggle` + `AgendaDocument`) → `tasks/agenda/page.tsx`; lift `view` state up |
| `app/(routes)/tasks/{layout,agenda/page,kanban/page}.tsx` | new route trio |
| [app/routes.ts](../../../app/routes.ts) | `/agenda` (line 52) → `/tasks` layout + children; `/agenda` redirect. Do **not** clobber `/conversations/agenda` (:60) or `/mail/agenda` (:80) |
| [shellRoutes.ts](../../ux/layout/shellRoutes.ts) | `'agenda'` (:16) → add `'tasks'` in `SHELL_ROUTE_SEGMENTS` |
| [navigation-page-url-sync.tsx](../../ux/components/navigation-page-url-sync.tsx) | `pathToPageMap` (:27) + `pageToPathMap` (:45) → `tasks` resolves to the `agenda` NavigationPage |
| [selectEmptyChatSurface.ts](../../ux/layout/selectEmptyChatSurface.ts) | **no edit needed** — it keys off `NavigationPage` (:22), not paths; fixing the map above covers it |
| [LayoutUrlSync.tsx](../../ux/layout/LayoutUrlSync.tsx) | add `/tasks` to `CONVERSATION_LIST_PREFIXES` (:38) — this is *list scope*, not page anchoring |
| [TaskGroupsNav.tsx](components/TaskGroupsNav.tsx) | `/agenda` → `/tasks` at :113 **and** :153; `?group=` → `/tasks/agenda?group=`; active check :90; icon from `TASK_GROUP_ICON_MAP` (:42) |
| [AgentHomeTasks.tsx](components/AgentHomeTasks.tsx) | same link updates (:47, :63) |
| `components/TaskKanbanBoard.tsx` | new board (name freed by deleting the relic) |
| `components/TaskKanbanCard.tsx` | new card (name freed by deleting the relic) |
| relic cluster — `tasks-kanban.tsx`, `tasks-view.tsx`, `sections/task-kanban-card.tsx`, `hooks/use-tasks-view.ts` | **delete** (~1,581 lines, zero importers) |
| `components/TaskOverflowCleanup.tsx` | column footer + AI sweep |
| `components/ChannelIcon.tsx`, `utils/open-task.ts` | extractions from TaskGroupDetailSidebar |
| `utils/task-group-icons.ts` | `TASK_GROUP_ICON_MAP` |
| [aop-schema.ts](../../../server/src/db/aop-schema.ts) | + `icon` on `taskGroups` |
| [crm-schema.ts](../../../server/src/db/crm-schema.ts) | + `priorityScore`, `priorityScoreSource` on `crmConversations` |
| [seedDefaultTaskGroups.ts](../../../server/src/services/task-groups/seedDefaultTaskGroups.ts) | 4 → 6 groups + `icon` field + insert |
| [user-tasks.ts](../../../server/src/trpc/routes/user-tasks.ts) | `listUserTasks` conversation join; unfloored board query; AI sweep mutation |
| [task-groups.ts](../../../server/src/trpc/routes/task-groups.ts) | `backfillRecentTasks` date filter + confidence gate |
| [cli/agenda.ts](../../../server/src/cli/agenda.ts) | + `board` verb reusing `outline`/`tally` |
| [conversation-updating skill](../../../server/.claude/skills/conversation-updating) | `priorityScore` rubric + human-lock rule |
| [auth.ts:586](../../../server/src/lib/auth.ts) | (Phase 7) seeding hook bug |
---
## Phased plan
### Phase 0 — Unblock the tree
- [ ] Commit the untracked [hscroll.tsx](../../../components/ui/hscroll.tsx) — it is already imported by tracked `AgentHomeTasks.tsx:4`, so a clean checkout **fails to build** today.
- [ ] Delete the orphaned relic cluster (~1,581 lines): `tasks-kanban.tsx`, `tasks-view.tsx`, `sections/task-kanban-card.tsx`, `hooks/use-tasks-view.ts`, and the `useTasksView` barrel export — verify zero importers first.
- [ ] Confirm the concurrent `agenda-current-future-restructure` work is settled (its Phases 3/5/6/7 have unticked boxes though the code landed) before editing shared agenda files.
- **Test:** clean clone builds; `pnpm types` baseline captured.
### Phase 0a — Groups: 6 lanes + icons, **jesse only**
- [x] `icon: text('icon')` column on `task_groups` (migration `0050_task_group_icon.sql`, journaled idx 42).
- [x] `DEFAULT_TASK_GROUPS` → 6 entries with `icon`; renamed `Top deals` → `Top deal tasks`; routing criteria for Reschedule + Reactivation; `icon` added to the insert. `listGroups`' virtual Misc gains `icon: null` for shape parity.
- [x] Repair jesse's 4 rows: renamed, icons set, 2 new lanes inserted.
- [x] `TASK_GROUP_ICON_MAP` + `taskGroupIcon()` in `utils/task-group-icons.ts` (mirrors `STATUS_ICON_MAP`); `TaskGroupsNav` renders the group's own icon instead of a hardcoded `ListTodo`.
- [x] `backfillRecentTasks`: `withinDays` (default 31) + `minConfidence` (default 0.5) with `forceAssign: false`; returns a `lowConfidence` tally. Run for jesse only.
- [x] **Added, not in the original plan — `cedar-cli groups`** (`src/cli/task-groups.ts`): `list`, `backfill`, `board`. A thin tRPC-HTTP driver over the real procedures, giving the task-group surface a headless entry point it lacked. `board` projects the column model (due→group, future→Upcoming, `dueDate` asc), so board membership and ordering are assertable before any UI exists.
- **Verified** (`CEDAR_API_URL=http://localhost:8790`, as jesse): 6 lanes with icons; backfill `processed 20 / assigned 19 / lowConfidence 0` across 5 lanes incl. both new ones; 195 older tasks (oldest created 2026-01-07) stayed in Misc; `otherUsersGroups: 0`; `groups board` shows 6 group columns + Upcoming (11), `dueDate` asc.
### Phase 0b — Shared extraction
- [x] Extract `ChannelIcon` (`components/ChannelIcon.tsx`) and `openTask` (`utils/open-task.ts`); both swapped into `TaskGroupDetailSidebar`.
- [x] Add `taskGroupId` to `HydratedUserTask`.
- **Verified:** logic moved verbatim (same email-draft-vs-conversation branch); `openTask` takes its store/nuqs setters as deps rather than reaching for them, so the card and board can reuse it. Typecheck clean for the touched files (the two `AttributeOrderingValue` errors in `userTasksSlice` are pre-existing, last touched by 8d29faf0d); eslint clean; no unused imports left behind.
### Phase 1 — Card enrichment (no ordering)
- [x] `listUserTasks` gains an opt-in `withConversation` flag returning `{ name, companyName, logoUrl, lastContactedAt, nextSteps }` per task — the fields the card *renders*. No `priorityScore` here (that's Phase 7).
- [x] Opt-in rather than always-on, so existing callers keep their current cost. One extra query for the distinct conversation ids, not per-card.
- [x] `cedar-cli agenda tasks --with-conversation` drives it headlessly.
- **Verified:** enrichment attaches correctly (e.g. `conversation: { name: "Madysen", companyName: "Madysenhoward", lastContactedAt: "2026-02-02…" }`). Typecheck clean for the change; the three pre-existing `user-tasks.ts` errors (`activeConnection` null, `validFields` hoisting) sit outside the edited ranges and blame to an earlier commit.
> Join path worth remembering: `crmConversations.primaryCompanyId` points at the **per-user
> `crmCompanyRelationships` row**, not a company — the name/logo live one hop further on
> `companiesGlobal`. Two left joins, not one.
### Phase 2 — Card
- [x] `components/TaskKanbanCard.tsx` — conversation header (avatar + company), last-contact / next-step badges, then the task row (checkbox · description · channel icon). Draggable via `useDraggable({ id: task.id })`, with `draggable={false}` for sidebar/overlay use.
- [x] **Correction found while building:** the mock's `next steps: Fri` is a *date* badge, but Phase 1 enriched `nextSteps` (free text). Added `nextStepDate` to the enrichment; the text is now the badge's tooltip.
- [x] **Correction:** there is no `completeUserTask` store action — completion goes through `optimisticCompleteTask` from `useOptimisticTaskActions`.
- [x] The deal-state row only renders when there's a date to show, so a bare task doesn't carry an empty label row. `onPointerDown` is stopped on the description so clicking opens the task instead of starting a drag.
- **Verified:** eslint clean; oxlint 0 errors across 1,557 files; Vite dev server compiles and serves. Visual check is manual (pure render work).
### Phase 3 — Sidebar accordion
- [x] `components/TaskGroupsAccordion.tsx` — every lane in one scrollable column, each header `sticky top-0 bottom-0` with an ascending z-index so a scrolling header slides under the next rather than through it. `?group=` decides which lane starts expanded.
- [x] Wired into `LeftSidebarContent` in place of the single-group drill-in; **`TaskGroupDetailSidebar` deleted** as superseded (its two reusable pieces were extracted in 0b).
- [x] Used local collapse state rather than `TaskGroupCollapseContext` — that context is scoped to the agenda document's TipTap sections, not a sidebar list.
- **Verified:** eslint/oxlint clean; dev server compiles. Sticky behaviour + expand/collapse are manual visual checks.
### Phase 4a — `/tasks` route restructure
- [x] `tasks/layout.tsx` (frame + greeting + `TasksLayoutToggle`), `tasks/agenda/page.tsx` (the old body verbatim), `tasks/page.tsx` (index → last-used layout via localStorage), `tasks/kanban/page.tsx`.
- [x] `/agenda` became a redirect to `/tasks`, **preserving the query string** — `?group=` and `?threadOpen=` both ride on it.
- [x] Registrations moved in lockstep: `routes.ts`, `shellRoutes.ts` (+`'tasks'`), `navigation-page-url-sync.tsx` (`tasks → agenda` page, `agenda: '/tasks'`), `LayoutUrlSync.tsx` (`/tasks` in `CONVERSATION_LIST_PREFIXES`), `TaskGroupsNav.tsx`, `AgentHomeTasks.tsx`.
- [x] `selectEmptyChatSurface.ts` needed **no edit** — it keys off `NavigationPage`, so mapping `tasks → agenda` covers it. (The original instruction in this doc was wrong.)
- [x] Deleted `AgendaHome.tsx` and `(routes)/agenda/layout.tsx`, orphaned by the split; removed the now-unused `useLocation` in `thread-display`.
- [x] **The dormant `/tasks` branch stayed dormant.** `thread-display.tsx` tested `pathname.startsWith('/tasks')` to enable a task-selector banner *and* a `selectThreadId` side effect — dead since the old route was deleted, and re-creating `/tasks` would have silently revived it inside a routing refactor. Narrowed to `folder === 'tasks'`; re-enable deliberately if wanted.
- **Verified:** eslint clean; full mail typecheck shows **6169 errors before and after** — zero net new. The one differing signature is in `UniversalComposer.tsx`, a file I never touched (the other session's edit morphed TS2304→TS2552).
### Phase 4b — Board
- [x] `TaskKanbanBoard`: one column per task group (`position` order, Misc last) + Upcoming; due/overdue → group column, not-yet-due → Upcoming; all columns `dueDate` asc; `@dnd-kit` drag → `moveTaskToGroup({ taskId, groupId })` with optimistic invalidation.
- [x] Reuses `CanvasKanbanColumn`, extended with an optional `headerIcon` so each column shows its group's icon; the colour dot remains the default, leaving the conversation kanban untouched.
- [x] A task with **no due date** counts as due now rather than scheduled ahead, so it can't hide in Upcoming forever.
- [x] Upcoming is not a drop target — dropping there would mean rescheduling, not re-grouping.
- [x] `cedar-cli groups board` (added in 0a) is the headless mirror of this projection.
- **Verified:** `groups board` returns 6 group columns + Upcoming; counts reconcile exactly (9 grouped + 191 Misc + 11 Upcoming = 211 open).
### Phase 5 — Overflow AI-cleanup
- [x] `TaskOverflowCleanup` renders when a column exceeds `COLUMN_VISIBLE_LIMIT` (30); the tail collapses into one row.
- [x] The copy only claims "more than a week overdue" when that's true of the majority of the tail, rather than asserting it unconditionally.
- [x] Click **seeds a chat message** (`setChatInputContent`) carrying the tail's task ids and asking for a proposal first — bulk-closing deals shouldn't happen without the user reading it. Not a direct mutation.
- **Verified:** Misc holds 191 due/overdue tasks, so the button appears there over a 161-task tail.
### Phase 6 — Scrollbar polish
- [x] `hooks/use-scroll-active.ts` — adds a `scrolling` flag for ~800ms after the last scroll event.
- [x] Applied to the **confirmed culprit**: the conversation kanban's *horizontal board* container, which set an explicitly-visible `scrollbar-color`. The per-column lists were already `scrollbar-none` and were never the problem.
- [x] Paired with `scrollbar-gutter: stable` so columns don't shift sideways when the bar appears.
- **Verified:** eslint + typecheck clean. The fade itself is a manual visual check.
### Phase 7 (DEFERRED) — Order group columns by `priorityScore`
> Everything above ships with `dueDate` ordering. This phase is the only one needing a new column,
> an agent rubric, and a backfill — kept out of the build path on purpose.
- [ ] `priorityScore` int(1–10) + `priorityScoreSource` ('human'|'agent') on `crmConversations`.
- [ ] `@crm-updater` rubric; never overwrite a human-set score.
- [ ] Add `priorityScore` to the `listUserTasks` join; switch group-column sort to `priorityScore` desc, `dueDate` tiebreak.
- [ ] Batch pass to seed open deals; unscored sorts last.
- **Test:** a hot early-stage deal outranks a small stale one; a human-set score survives a `@crm-updater` run; board order changes as expected while the agenda's own sort is untouched.
### Phase 8 (LAST) — Roll out to everyone
- [ ] Fix the signup hook ([auth.ts:586](../../../server/src/lib/auth.ts)): `seedDefaultTaskGroups` sits inside the `else` of `if (existingUserRecord?.organizationId)`, so org-attached users skip **all** seeding (task groups, standalone AOPs, org templates) — 3 of 5 post-seed signups have none. Decide whether org-less users (58 solo accounts) get groups.
- [ ] Re-seed all users (groups were deleted — fresh, no duplicate hazard).
- [ ] Run the last-month backfill fleet-wide.
- **Test:** every active user has 6 iconed groups; a fresh signup (org and org-less) gets them.
---
## Verification steps
- `pnpm types` clean; clean clone builds (hscroll committed).
- All headless checks pinned to `CEDAR_API_URL=http://localhost:8790`, as <email>.
- Routing: Tasks → `/tasks` → last view; both toggles independent; `/agenda` redirects; `/conversations/agenda` and `/mail/agenda` untouched.
- Columns: task groups in `position` order, Misc last, then Upcoming.
- Placement: due/overdue in their group column; not-yet-due in Upcoming.
- Ordering: `dueDate` asc in every column (most overdue on top; Upcoming chronological). `priorityScore` ranking only after Phase 7.
- Board shows the unfloored set (jesse: 225 open) vs the agenda's floored set (27).
- Ordering identical between agenda and board.
- Overflow collapses the >1wk-overdue tail and targets the right ids.
- Sidebar sticky headers; scrollbar visible only while scrolling.