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 idempotentcrm.activities.deal_node_idFK 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/listDealActivitieswith fail-loud ownership checks,listDealsreturns nextStep/nextStepDue/lastActivityAt/staleness. - API: GET
deals/[id]?activities=1timeline; PATCH accepts nextStep/nextStepDue plus alog_activityrider. - 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_detailjsonb + 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_replynotifications 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_replyrenderer 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: optionalcrm.ai_deal_briefscache (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):
generateDealBriefwith 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_briefon app/api/apps/sales/route.ts. - UI:
DealBriefSectionatop 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);
triagepassthrough 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_activitynote/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.tsserialized createdAt viaString(Date)(locale string) and sorted deals bylocaleCompare(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
computedAtwas raw pg text (YYYY-MM-DD HH:MM:SS+00), non-ISO, which Safari/WebKitnew 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_labelbut the column istriage, so the triage label never surfaced. Fixed: reada.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:100system 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_atstamp 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 atlib/sales-crm-jobs/triage.tsbounce 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/