Skip to content

CreatorTrack CRM Expansion, Overnight 2026-07-09

What was researched

Three parallel designer proposals plus a ground-truth code map of the sales app were synthesized into one deduped spec. Strongest findings: the node-backed Pipeline (the strategic deal surface per docs/crm-overhaul-design.md) had zero activity history, notes, or staleness signal, so stalled deals hide silently in Negotiation; the Gmail ingest was working but passive (mail from unknown people at known company domains vanished, no ingested email ever attached to a deal, and "send me your rates" was logged identically to "no thanks"); deal truth was scattered across the email timeline with no summarization surface; and the chat agent could read the CRM but barely act on it. Two deal systems (legacy crm.deals and node-backed pipeline) coexist by design and were not cut over. The four overnight features were carved into four disjoint file-ownership zones (Z1-Z4) so parallel builders never touched the same file, and that partition held.

Spec

memory/plans/creatortrack-crm-expansion-2026-07-10.md (spec)

What was BUILT tonight (branch feat/crm-overnight-0709)

Single gate commit c882952 (17 files, +1942/-58, 3 new migration .sql FILES created, none executed against prod lifeos). git -C /home/justinwieb/forge-suite-wt-manual/crm-overnight-0709 log --oneline main..HEAD shows exactly that commit.

F1: Node-pipeline deal timeline + deal rot + next step (Z1). Status: BUILT, DB-verified on dev clone, UI untested in browser

  • Migration 20260710T045658_..._crm_activities_deal_node_id_for_pipeline_timeline.sql: additive idempotent crm.activities.deal_node_id FK to app.nodes + (workspace_id, deal_node_id, occurred_at DESC) index.
  • lib/crm-deals.ts: per-stage rot thresholds (dealStaleness()), Next Step / Next Step Due props seeded and lazily backfilled, stage-change auto-note in the same transaction, addDealActivity/listDealActivities with fail-loud ownership checks, listDeals returns nextStep/nextStepDue/lastActivityAt/staleness.
  • API: GET deals/[id]?activities=1 timeline; PATCH accepts nextStep/nextStepDue plus a log_activity rider.
  • PipelineView.tsx: staleness dots, next-step line (red overdue), "Needs attention" filter, right-pane timeline + composer; fixed a latent stale-defaultValue bug via per-deal keys.
  • Verified: tsc/eslint clean in owned files; migration + new SQL shapes exercised on dev clone ct_crm_overnight_0709 (all 12 pending worktree migrations applied cleanly), DML rolled back. Prod only ever read.
  • Blockers: no live-browser E2E (worktree shared with other builders); new styles inline because sales.css is another session's; "Needs attention" filter can leave columns looking empty (per-column hidden count would need shared KanbanBoard); migration must be applied via forge_workspace_migrate.py at merge time or listDeals/timeline fail loud.

F2: Gmail reply triage + email-to-company/deal auto-attribution (Z2). Status: BUILT, verified on throwaway local Postgres, live Gmail+LLM untested

  • Migration 20260709T233000_..._gmail_reply_triage_and_contact_source.sql: crm.activities.triage (CHECK interested/negotiating/declined/ooo/referral/bounce/other) + triage_detail jsonb + partial untriaged-inbound index; crm.contacts.source + email_bounced_at.
  • lib/sales-crm-jobs/triage.ts (new): one LLM call per sync over up to 50 untriaged inbound emails (bodies as untrusted data, prompt hardened), idempotent NULL->value claim, hot replies (interested/negotiating) insert crm_hot_reply notifications with the dispatcher's exact dedupe contract, ooo snoozes contact, bounce stamps first-wins.
  • lib/sales-crm-jobs/gmail.ts: domain->company map (free-mail excluded), ensureAutoContact (source='gmail_auto') so unknown senders at known companies attach immediately, single-open-deal attribution stamps deal_node_id (runtime probe guards unapplied column), duplicate-email collisions surfaced in sync_state.detail, triage called in the fail-tolerant post-commit seam.
  • lib/sales-crm.ts: Contact gains source/emailBouncedAt/hotReply (14-day lateral); Activity gains triage.
  • UI: ContactsView badges (auto-added / bounced / hot) + timeline triage chips; FollowUpsStrip Hot-replies section + duplicate-email warning.
  • Verified: full SQL path matrix on a scratch Postgres 16 (migrations applied twice, idempotency proven; dedupe replays = 0 rows; CHECK rejects junk). tsc/eslint clean vs HEAD baseline.
  • Blockers: live E2E (real Gmail + real LLM + prod schema) untested; apply the migration via forge_workspace_migrate.py BEFORE deploying (selects are unconditional); notifications write path replicated because dispatch() is module-private (refactor once exported); bell UI may need a crm_hot_reply renderer if it switches on known types; bounce detection is best-effort (mailer-daemon sweep is a follow-up).

F3: AI Deal Brief (Z3). Status: BUILT, typechecked + linted, runtime untested (needs dev slot + live LLM)

  • Migration 20260709T233500_..._ai_deal_briefs_cache.sql: optional crm.ai_deal_briefs cache (UNIQUE ws+deal, RLS, fingerprint-driven staleness). Code probes to_regclass and runs compute-on-demand when absent, so the feature works with zero schema change.
  • lib/sales-crm-ai.ts (new): generateDealBrief with cached_only / auto / force modes, mirrors lib/university-ai.ts LLM plumbing, validated {briefMd, riskFlags[], suggestedAction}, email content as untrusted data, draft-only (nothing sends), fail-loud on invalid model output.
  • API: POST op ai_deal_brief on app/api/apps/sales/route.ts.
  • UI: DealBriefSection atop the deal editor in DealsView; CompaniesView account panel reuses it with a deal selector.
  • Blockers: E2E needs WORKSPACE_DEV_AUTH=1 dev slot + configured LLM provider; until the migration is applied every generate is an uncached LLM call; inline styles pending sales.css owner.

F4: Chat-agent CRM tool surface (Z4, lib/chat.ts only). Status: BUILT, typecheck + lint + pattern parity, no runtime test

  • Six new tools delegating to existing lib/sales-crm.ts exports under caller Identity + workspaceId: list_activities (timeline by contact/deal/company id or name, snippets marked untrusted), update_company, archive_company, archive_contact (idempotent), log_touch, snooze_contact (natural-language date via parseDue, ''=clear). Workspace-scoped name resolvers with ambiguity errors instead of guessing; mutation kinds wired for client revalidation.
  • Blockers: app/api/chat/route.ts system-prompt tool enumeration (not owned) still lists only the original eight names (functional anyway, discoverability nit); triage passthrough in list_activities activates once listActivities selects the F2 column.

Gate result

PASSED and committed as c882952. tsc: 6 errors, all pre-existing baseline (LayoutProps, missing PNG modules), zero in CRM files. eslint CRM scope: 3 errors + 3 warnings, all pre-existing (verified against HEAD baselines). Integration seams intact: sales-crm-ai imported by route + DealsView, triage imported by gmail pipeline; no unwired exports. No push, no merge, no migration executed.

Deferred, awaiting Justin's approval per the feature-plan hard gate

  • F5: Payment tracking, net-terms clock, overdue queue, money dashboard (node pipeline).
  • F6: Structured deliverables tracker + Deadline Radar + calendar projection (node pipeline + calendar seam).
  • F7: Stop-on-reply outreach sequences + AI-drafted follow-ups. Highest caution: the only feature that SENDS email autonomously; Phase A (draft-only compose modal) is the safe first slice, Phase B (auto-send with stop-on-reply, daily cap) stays approval-gated.
  • Also deferred chat tools that depend on F3/outreach libs: get_deal_brief, draft_followup, enroll_in_sequence, list_hot_replies, node-pipeline deal tools.

Review commands

git -C /home/justinwieb/forge-suite-wt-manual/crm-overnight-0709 log --oneline main..HEAD
git -C /home/justinwieb/forge-suite-wt-manual/crm-overnight-0709 diff main --stat
git -C /home/justinwieb/forge-suite-wt-manual/crm-overnight-0709 show c882952 --stat

To try it live, boot a dev slot per CT dev testing: from the worktree run the dev server with WORKSPACE_DEV_AUTH=1 and next dev --webpack (worktree turbopack symlink gotcha), pointed at a parallel-dev clone DB (F1's builder already created ct_crm_overnight_0709 from ct_template_workspace with all 12 pending migrations applied). NEVER fresh-login over http; commit often, the dev-slot reaper eats uncommitted worktree work (this batch is committed).

Merge-time checklist: apply the three new migrations via forge_workspace_migrate.py (F1's deal_node_id before exercising F2's deal attribution), then deploy; F2/F1 selects are unconditional so schema ships with the branch.

Live verification addendum (2026-07-10)

All four features were exercised end-to-end over HTTP against a dev server on :3080 (WORKSPACE_DEV_AUTH=1, dev user [email protected]), against the clone DB ct_crm_overnight_0709 only. Prod lifeos was never touched. No browser/Playwright (shared profile owned by other live sessions), so all UI rendering was code-inspected, not exercised. Every fixable confirmed defect was reproduced, fixed at root cause, and re-verified green. Fixes landed in one commit 77ed9e3 (fix(crm): live-verify fixes) on feat/crm-overnight-0709; npx tsc --noEmit clean. Not pushed, not merged, no migration run against prod.

Per-feature verify result

  • F1 (deal timeline + rotting + next-step): PASS with 3 defects (2 fixed, 1 fixed). Verified: node-pipeline deal create with company/contact links and next-step props; log_activity note/call/meeting/touch with custom occurredAt; timeline newest-first by occurred_at (out-of-order inserts sorted right); stage-change auto-note written in-transaction; nextStep/nextStepDue round-trip; per-stage staleness (warm/rotting, Paid never rots, created_at fallback); ownership 404s and all validation 400s.
  • F2 (Gmail reply triage + email->company/deal auto-attribution): PASS. Exercised via a tsx harness with a synthetic 7-message fixture swapped for searchMessagesMeta (never touched live Gmail for the real test), in a throwaway workspace on the clone. Verified: domain auto-attribution with free-mail exclusion; single-open-deal attribution (two-deal and terminal-Paid stay NULL); one real LLM triage call labeled all 5 inbound rows (interested/negotiating/ooo-with-oooUntil/declined/bounce); hot-reply bell notifications; snooze + first-bounce-wins stamps; full idempotency on re-run; duplicate-address collision surfaced. Cleanup complete, workspace purged.
  • F3 (AI Deal Brief): PASS with 2 defects (both fixed). Verified: cached_only on empty cache returns brief:null with no LLM call; auto makes a live call and returns full DealBriefResult; cache row written with sha256 fingerprint; cache hit served in ~72ms; new activity flips to stale then regenerates with a changed fingerprint (upsert holds UNIQUE); force regenerates; compute-on-demand fallback verified by renaming the cache table on the clone; validation 400 for missing dealId.
  • F4 (6 new chat tools): PASS with 4 defects (all fixed) + 1 confirmed unowned-file gap. Verified via direct executeTool() harness: list_activities one-of contract + company timeline merge + snippet truncation + untrusted-email note; update_company by id and unique-prefix name; ambiguity errors never guess; snooze_contact natural-language parse + clear; log_touch advances last_touch_at; mutated=["sales"] on writes only; workspace scoping.

Defects found and fixed (commit 77ed9e3)

  • D1 (F1, correctness): lib/crm-deals.ts serialized createdAt via String(Date) (locale string) and sorted deals by localeCompare (alphabetical by weekday name). Fixed: ISO-normalize createdAt/lastActivityAt/timeline timestamps and sort chronologically. Verified live via backdated deals.
  • D2 (F1, spec deviation): PipelineView.needsAttention() excluded deals with recent activity and no next step, defeating the every-deal-has-a-next-step mechanic. Fixed: include no-next-step deals.
  • D3 (F1, non-atomicity): PATCH prop-edit + log_activity ran as two transactions; activity-insert failure left the prop change committed (retry double-applies auto-notes). Fixed: new patchDeal() runs both in one transaction; rollback proven by fault injection.
  • D7 (F3, shape): cached-path computedAt was raw pg text (YYYY-MM-DD HH:MM:SS+00), non-ISO, which Safari/WebKit new Date() rejects. Fixed: readCache returns ISO.
  • D8 (F3, HTTP semantics): unknown/other-workspace dealId returned 500. Fixed: returns 404 with clean error.
  • D11/D14 (F4, cross-workspace write + leak): log_touch's addActivity had no workspace-ownership guard, so a foreign contact_id inserted an activity row cross-workspace; a bogus id leaked raw postgres.js SQL to the model. Fixed: addActivity validates contact_id/deal_id ownership; bogus id returns clean no contact N.
  • D12 (F4, correctness): list_activities read a.triageLabel ?? a.triage_label but the column is triage, so the triage label never surfaced. Fixed: read a.triage.
  • D13 (F4, false success): archive_company/archive_contact returned {archived:true} for bad or cross-workspace ids (no rowcount check). Fixed: fail loud, idempotent on repeat.
  • D15 (F4, prompt gap, unowned file): app/api/chat/route.ts:100 system prompt listed only the old sales tools, steering the model away from the 6 new tools. Fixed: prompt now lists all 6 with usage guidance.

Not fixed (out of overnight scope)

  • D6 (F2, design defect): the real-NDR bounce path is unreachable in production. Real Gmail NDRs arrive from [email protected] / postmaster, which match no contact and whose domain is free-mail, so they are never ingested; the email_bounced_at stamp can only fire on bounce-looking TEXT from an already-matched address, which is not how bounces are delivered. The code path itself works (synthetic bounce from a matched address stamped correctly). Left as a KNOWN LIMITATION comment at lib/sales-crm-jobs/triage.ts bounce branch; a real fix needs NDR-sender queries + X-Failed-Recipients parsing (a feature build, not a bugfix).

Remaining gaps that need Justin (live)

  • Live Gmail run: real Gmail query semantics (from: suffix matching, query length caps) and the triage LLM-failure seam need a live Gmail account; only synthetic fixtures were used overnight.
  • Browser UI pass: all rendering was code-inspected only (browser tools fenced this session). Needs a real UI pass on PipelineView (RotDot colors, red overdue next-step line, Needs-attention toggle count, timeline section, log-note composer), ContactsView/FollowUpsStrip badges, DealsView brief staleness timestamp, and end-to-end chat-UI SSE with a live model actually choosing the 6 new tools.
  • Untested by design: cache-upsert concurrent-write race and RLS cross-workspace denial for the brief cache (only one authed dev identity on the server).

Note: the shared clone ct_crm_overnight_0709 was used concurrently by other overnight exercisers under the same verify0710_ prefix; the fix session switched to a per-session fx0710_ prefix and left the clone's CRM tables empty at end-state.

[Claude Code]

URL: https://mkdocs.justinsforge.com/memory/handoffs/creatortrack-crm-expansion-2026-07-09/