index.tsfix(mail): clear 309 typecheck errors and 5 live ReferenceErrors apps/mail 792 -> 483. Jest stays green at 111 suites / 1071 tests. The single biggest cluster was one missing line: tsconfig pins `types` to ["node","vite/client","jest"] and jest.setup.js is a .js file outside `include`, so the @testing-library/jest-dom matcher augmentation never entered the program — 170 errors from `expect(...).toBeInTheDocument()` and friends. A 4-line jest-dom.d.ts clears all of them. Stripping @jest/globals from 22 test files was a prerequisite, not a tidy-up: only the globally-injected `expect` picks up that augmentation. The other 145 test files already used injected globals, so this also converges on the dominant convention. Do not install @jest/globals. The rest: CRM store test fixtures still extending state that moved to conversationsSlice in Dec 2025 and importing a deleted CRMTypes module; barrel files re-exporting modules that no longer exist (each one an import-time throw); a locally-declared SelectorItem whose module was deleted; sanitizeJson taking `object` when its body explicitly handles primitives and both callers already cast. Five of these were live runtime bugs, not type noise: - setForcePropagate(false) in the AOP-save onSuccess threw on EVERY successful playbook save (cedarAdmin/page.tsx, admin/page.tsx) - backendResponsiveness referenced in a useMemo dependency array, evaluated every render, while the memo body no longer mentions it - "Force re-sync" called handleForceSync() with no argument against a z.object() input that rejects undefined, so it always failed validation - an empty-string icon key produced `Icon === ''`, i.e. <'' /> - showOverlay leaked an unknown attribute onto a Radix DOM node Worth knowing separately: the 792 were invisible. Plain tsc reported 3, because a syntax error elsewhere in the tree makes TypeScript skip ALL semantic checking. One stray comma blinds the entire typecheck. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 2:11 PMStageBatchApprovalCard.tsxfix(types): close 205 typecheck errors by repairing real client/server drift apps/mail typechecked against a stale apps/server/dist d.ts (the server's package.json points "types" at dist), so a chunk of the reported drift was phantom. Rebuilt it and fixed the four script errors that blocked `tsc -b` in server, which is what regenerates it. The rest was genuine contract drift, fixed at the source rather than cast away — routes that dropped fields their callers still read (agentExecutions, getUpcomingCalendarEvents' conversationId, createCanvas' description), schema splits the UI never followed (conversationUsers vs conversationContacts), and enums that grew server-side but not client-side (TASK_TYPES, ActionStatus, DateFilterOperator). Also deletes code that was already dead: an unreferenced sort popover whose store API is gone, a panel importing a deleted module, the system-skill metadata UI whose mutation was removed deliberately, and a test asserting a store method that no longer exists. apps/server: 4 -> 0. apps/mail: 228 -> 23, all remaining errors being dependency resolution (zod v3/v4, react-router dev/runtime skew, tiptap v2 via novel) rather than code. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 7:45 PMTaskEmailItem.tsxrefactor(tasks): retire task_channel onto task_output.kind (design: task-cleanup phase 5) `multi-action` was never a channel. It meant "this task has several possible actions", which is not a place a message is sent — and TASK_OUTPUT_KINDS omits it deliberately for that reason. Only two sites wrote it, both CRM approvals that already pass an explicit task_output, so the value was legacy filler. Removing it forced the question the column had been dodging: task_channel was NOT NULL DEFAULT 'email', so those rows had to become either a lie or nothing. A CRM approval sends nothing. Migration 0059 makes the column nullable, drops the default, narrows the CHECK to real channels, and nulls the 7,200 multi-action rows. Relaxing the constraint is itself a step toward the drop — constraints come off before columns do, so a newer server can stop writing it first. Moving the readers is NOT a rename. The two axes disagree on ~12,000 rows and the output axis is the correct one, so each site is a decision: does it want the declared channel, or what the task actually produces? It is the artifact, and that changes which rows match. On open tasks, 49 stop matching the email filter and 9 start — a calendar task or a pure reminder no longer auto-completes because an email thread synced, and a task whose channel says slack but whose payload holds an email draft now does. Two traps worth recording: - The migration ordered `UPDATE ... SET NULL` before `DROP NOT NULL` and was rejected by the constraint it had not yet dropped. - createUserTaskWithExecutionUpdate had `taskChannel || 'email'`. Left alone it would have silently converted every "no channel" back into "email" on write, undoing the migration for new rows while backfilled ones stayed correct. Two latent bugs surfaced by the sweep: - The crm/conversations.ts task aggregates omitted task_output entirely despite HydratedConversation.userTasks being typed as the full row, so nothing downstream could read the output axis off that path. - The task board's `keyOf` defaulted a null output to 'email', so an undecided task would have vanished from the board when grouping by channel. There is now an explicit Undecided bucket. Frontend gets a shared task-output module mirroring the server kinds; the Multi-action column, badge and icon are replaced by real output kinds. Co-Authored-By: Claude Opus 5 <<email>>Aug 15, 2026, 6:28 PM