Built in public · Changelog

How Supernova learned the job.

The product story, newest first: what actually became possible in each stretch, not a list of tweaks. Generated live from the engineering journal; the log is the changelog.

99

improvements shipped

9

days of building

11

shipped per day, avg

9
6 Jul
8
7 Jul
1
8 Jul
2
10 Jul
5
11 Jul
25
12 Jul
6
13 Jul
peak37
14 Jul
6
15 Jul

Product panorama

One deal, one continuous operating loop.

Six agents live in the product today, propose-only, a named human at every gate. Everything below ships; nothing is a mock.

01every desk, daily

Brief

The Daily Brief opens each cockpit: meetings with exact times, deals needing a touch, the one move.

02critic-checked

Draft

The Email Drafter investigates the thread through scoped tools, then a fresh-context critic verifies every claim.

038 dimensions

Qualify

Deal Check scores MEDDPICC with the evidence that earned each score, never vibes.

04pre-call

Prep

Meeting Prep briefs every call: who is in the room, where it stands, what to ask.

05with provenance

Track

The Promise Tracker extracts what each side committed to, source attached, resolved in place.

06propose-only

Audit

Every run costed and receipted: model, tokens, verdict, the audit trail behind every artifact.

Latest15-Jul-2026Agents

The Rohit walk: the org chart stops decorating, the briefing stops wrapping names

Walking the admin suite as Rohit (Command / Users / Audit / AI usage / Tech leads / Org chart), two real design-language breaches:

  • The org chart was the last page decorating with colour. Its L1-L4 depth ramp was neon indigo / blue / emerald / purple on bordered boxes, the exact thing Daylight bans (hierarchy comes from scale and weight, never a rainbow; the entity hues are warm and muted). Depth is now a ladder of prominence: clay at the root, fading down the ink ramp, elevation carrying the box, borders gone. The legend reads from the same LEVEL_STYLES, so it re-tuned itself.
  • The briefing's lead column is a MEASURE column (times "12:30", amounts, ages) and aligns beautifully at w-16 with tabular-nums. The Command Board's rep names are not measures: "Nitish Kumar Rana" wrapped to three lines. Names and company labels now get w-28 + truncate, and drop tabular.

Everything else on the admin suite held up: the team briefing reads with real intelligence ("Himanshu carries the biggest load: Rs 19.9 Cr with 20 slipping deals and 58 overdue tasks"), the audit trail shows agent runs with actor glyphs, the AI ledger costs every call.

15

Jul 2026

Wednesday

6 shipped

Product

Bulk blasts are not conversations (the SDR inbox was inventing threads)

Walking the SDR inbox as Priyadarshini, the top "conversation" was Anand Singh at Probus: 15 emails, newest a tender notice from nprocure@ncode.in, labelled "with this lead". The label was…

Walking the SDR inbox as Priyadarshini, the top "conversation" was Anand Singh at Probus: 15 emails, newest a tender notice from nprocure@ncode.in, labelled "with this lead". The label was technically true and semantically garbage: anand@probus.io is on that tender blast, along with 387 other addresses. He is on the same mass mailing list as our mailbox; he has never corresponded with us at all. The mirror holds 1,260 such blasts from that one sender.

The damage was real: fake threads at the top of the inbox (ranked by "last email"), inflated conversation counts and thread lengths, a "with this lead" claim that never happened, and the same pollution feeding the Briefer's waiting on your reply.

Fix, central so every surface inherits it (BULK_RECIPIENT_LIMIT = 12 in lib/graph/match.ts): the RECIPIENT-side match branches now exclude messages with more than 12 recipients. A real thread has a handful of people on it; a 400-address blast is a broadcast. The FROM side is untouched: if the lead personally sent it, it is real no matter how many were copied. Same guard in getInbox's ranking CTE.

Verified: Anand's thread 31 matched -> 0 (the entire "conversation" was fake); the inbox now opens on a genuine thread (Deepak Mishra at Chakr, 9 emails + 2 meetings + a recorded call). The filter is also faster than no filter (49ms vs 250ms) because it prunes rows before the join, so the 29ms deal-timeline budget is safe.

Also this pass: SDR nav order put My Day 5th, below the lists; the cockpit comes first for every role now (the AE's Desk sets the shape).

Product

The uniformity pass: one component per job, every pulse a doorway, no width caps

The all-pages walk turned up the same disease three times: a shared idea reimplemented per page until the design language drifts. Fixed at the root, then codified as DESIGN principles 19-21.

The all-pages walk turned up the same disease three times: a shared idea reimplemented per page until the design language drifts. Fixed at the root, then codified as DESIGN principles 19-21.

  • AttnItem existed 8 times with drifting shapes (some with counts, one with children, three prop signatures). Now one src/components/attn-item.tsx, and it takes an `href`: every attention item is a link to the surface where the problem gets fixed (past-due -> pipeline, no-next-step -> tasks, overdue -> follow-ups, unlinked call -> the recording), with the arrow affordance on hover. Plain text only where there is genuinely nowhere to go (read-only banners). A count you cannot act on is noise.
  • Settings was five near-identical pages that had drifted (two eyebrow treatments, two card classes, a stray em dash, full-width password fields). Now one AccountSettings; admin composes it and passes Integrations as aside. admin 108->61 lines, the other four ~78 -> 10 each.
  • Width caps killed: max-w-3xl/2xl/xl/[64ch] on cards, headlines, and page copy. The app is a cockpit, not an article: a cap strands the viewport and breaks alignment (one card stops at 48rem, the next runs full, nothing shares an edge). Control widths (dialogs, the password form) stay.
  • Em-dash sweep across every rendered string (page copy, toasts, pulse descriptions, metadata).
  • Also: the meeting-event page strips Teams-invite boilerplate from Notes; the email reader got its universe rail; enrich-panel imported Linkedin from lucide-react (which dropped brand glyphs) and broke tsc tree-wide, swapped to Link2.

Verified: all 13 AE + public routes return 200 with no runtime errors; pipeline/lead-hub pulses confirmed linking in the browser.

Data

Enrich closes the loop: the FIRST live Zoho write-back, plus company channels

Two upgrades the same day, from the user's asks ("a way to update Zoho with it"; "email phone linkedin needs to be figured out"):

Two upgrades the same day, from the user's asks ("a way to update Zoho with it"; "email phone linkedin needs to be figured out"):

  • Zoho write-back, live: the dossier's proposed field updates now carry an Update Zoho button. applyZohoUpdates (src/lib/enrichment/actions.ts) PUTs the fields to Zoho v8 (the first real Zoho write in the product — zohoPut had never been used), then updates our mirror, clears the applied proposals from the stored dossier, and audits ZOHO_FIELDS_UPDATED with full before/after. Field map is deliberate: leads get Industry / No_of_Employees / Website / City / State / Country; accounts add Phone + billing address fields. A lead's Email/Phone/LinkedIn are the person's — company research never writes them. Verified live on the ANSCER lead: 6 empty fields (industry, employees, website, city, state, country) filled in Zoho + mirror, audit row before all null / after all researched values, proposals section cleared itself.
  • Company channels in the dossier: company gained website / linkedin (company page URL, validated) / email / phone, rendered as actionable chips (site ↗, LinkedIn ↗, mailto:, tel:) under the identity row. Panel redesigned: ICP read as a banded row (emerald/amber/zinc wash by score), news as a mono-dated timeline, key people with initial avatars, updates as field-shaped diff rows ("empty → Bengaluru"). The model prompt now demands field-shaped values (city = the city name alone) and channels it actually found, never guessed.
  • Fix en route: drizzle expands a JS array param into N placeholders, so = any(${arr}::text[]) broke — string_to_array on a joined param instead.
Agents

Enrich (agent 7): live web research on any company, including ones not in the CRM yet

The seventh agent, Enrich, answers "who is this company, really?" with the live web instead of the CRM's stale fields. Built on Anthropic's server-side web_search tool (verified on the…

The seventh agent, Enrich, answers "who is this company, really?" with the live web instead of the CRM's stale fields. Built on Anthropic's server-side web_search tool (verified on the user's key; ≤5 searches per run, each metered at $0.01 into llm_calls.web_searches and the cost ledger):

  • `src/lib/ai/agent-enrich.ts`: one Haiku call with web search → structured dossier: what they do / size / HQ, recent news (every item must carry its source URL — uncited news is dropped deterministically), key people, an honest ICP-fit read (strong/medium/weak) against Antino's ICP, ≤3 talking angles, and proposed CRM updates (current → proposed, only where the web contradicts or fills a listed field — propose-not-act, they land in Zoho with write-back). Verdict PASS/THIN/NOT_FOUND; run summary + trace-true totals; upsert into new enrichments table (unique per entity_kind + entity_id).
  • Three entity kinds: lead (company or corporate-email domain), account (name + website), and `prospect` — research-from-nothing: the company name typed into accounts search, stored lowercase. The /ae/accounts?q=LifeWise empty state now offers "Research LifeWise on the web"; the dossier renders right there, with a Propose as account button that audits ACCOUNT_ADD_PROPOSED (PREVIEW) with the dossier fields in after.
  • Surfaces (src/components/enrich-panel.tsx): accounts-search empty state, account hub, AE lead hub, SDR lead hub. Toast on start + finish (principle 13), receipt line (verdict · N web searches · ₹). Server actions in src/lib/enrichment/actions.ts audit AGENT_ENRICHED.
  • Eval added (5th agent eval): /api/evals/run case + run-evals.mjs asserts company identified, all news cited, valid ICP score, totals == ledger, cost < $0.15 (search-metered ceiling).
  • Verified live: Bajaj Finserv account (PASS, 4 cited news, ICP strong, $0.088); LifeWise prospect (found LifeWise Academy, honestly scored ICP weak — the openers even warn the name is ambiguous across companies); ANSCER Robotics lead (ICP strong, the real $5.4M Series A + US expansion in news, key people matching the actual email thread, 4 proposed field updates: industry, 73 employees, anscer.com, Bengaluru).
Product

The email reader gets its universe

A message is never just a message. /ae/emails/[id] now carries a universe rail resolved from the CRM graph (src/lib/graph/email-universe.ts): Part of (the deals the participants belong to,…

A message is never just a message. /ae/emails/[id] now carries a universe rail resolved from the CRM graph (src/lib/graph/email-universe.ts): Part of (the deals the participants belong to, with stage dot + amount, the account, and each sender/recipient resolved to their contact or lead, all linked), Conversation (the rest of the Graph conversation_id thread, each message opening in the reader), and With these people (recorded calls + calendar meetings matched by participant email). Honest empty state when nobody on the message is in the book (newsletters). Verified on the TruBoard thread: deal, account, contact, 2 prior messages, and 5 meetings/calls, every node a link. Also from the page-by-page loop: the meeting-event page strips Teams-invite boilerplate from Notes.

14

Jul 2026

Tuesday

37 shipped

Data

Tech lead, from messy Zoho text to a resolved person on every deal

Zoho's Tech_Lead_Name_Involved is free text and a mess: first names, spelling variants (Radhakant / Radhakanth), multi-person strings ("Arka and Gorish"), and notes ("NA", "Through Prem,…

Zoho's Tech_Lead_Name_Involved is free text and a mess: first names, spelling variants (Radhakant / Radhakanth), multi-person strings ("Arka and Gorish"), and notes ("NA", "Through Prem, hence no tech lead"). 205 deals tagged, 71 distinct values, only 14 linking to an app user. Fixed it as a resolution layer, not a blind rewrite:

  • `tech_lead_map` (new table + Drizzle def): one row per distinct raw value, mapping it to a clean_name (the canonical spelling we write back to Zoho) and, where the person is in the system, a user_id. Seeded by scripts/tech-lead/resolve.mjs, which auto-resolved 36 names / 143 of 205 deals (70%) by first-name match (TECH_LEAD/DEV preferred, spelling variants folded).
  • `/admin/tech-leads` review surface: the residue (unmatched names, multi-person, notes) is a human's call, so the page lists every value by status with a one-click suggestion, a teammate picker, a "just a name" option, "no tech lead", and an inline add-a-teammate flow for people not yet in the system (Arka = 24 deals, Divyansh, Sudhanshu…). Every resolution is audited (TECH_LEAD_RESOLVED) and re-arms the Zoho write-back.
  • Shown everywhere a deal is (user: "super super important"): one TECH_LEAD_VALUE SQL expression resolves the display name (clean when mapped, raw until reviewed, null when cleared), threaded through the deal hub (clickable to the teammate), pipeline kanban cards, the Desk slipping list + deal hover-card, the Accounts table top-deal, the Inbox deal threads, the Follow-ups list, account + contact hubs, the team page, and the deal peek pane, all via the shared DEAL_SELECT / TechLeadChip / deal-row components.
  • Write-back of the cleaned names to the Zoho source is next (write scope now granted); the map is the reviewed outbox.
Product

The highlight pair + material upgrades: vanilla × clay-hot, alpha hairlines, lit card edges

Three token-level upgrades from the day's design studies, all propagating system-wide:

Three token-level upgrades from the day's design studies, all propagating system-wide:

  • The highlight pair (burnt-orange × vanilla study): new --vanilla (warm surface for the FEW hot moments; espresso-warm equivalent in dark) and --clay-hot (the interaction step above clay). Applied: the briefing's "Your one move" band sits on vanilla with a rest shadow; the primary button goes clay → clay-hot on hover/active. Never ambient — the pair works because it is scarce.
  • Alpha hairlines (dealflow mechanic): --border/--border-strong are now translucent ink (rgb(33 30 24 / .12) light, cream-alpha dark), so the same divider token reads correctly on any surface it crosses.
  • Lit top edge: dark --elev-rest/raised gain an inset cream hairline (4-5%), the "physically lit" card top that espresso surfaces were missing.
Product

Changelog v3: the story claim + the Product Panorama (revq.io study)

Studied revq.io's marketing pages in detail (home, changelog, how-it-works) and applied the two strongest devices to /changelog. The headline became a story claim, "How Supernova learned…

Studied revq.io's marketing pages in detail (home, changelog, how-it-works) and applied the two strongest devices to /changelog. The headline became a story claim, "How Supernova learned the job", with the editorial bar stated in the subcopy: what became POSSIBLE in each stretch, not a list of tweaks. And before the timeline, a Product Panorama: six cells, one operating loop (Brief / Draft / Qualify / Prep / Track / Audit), each with a mono number, a real status tag (every-desk-daily, critic-checked, 8-dimensions, pre-call, with-provenance, propose-only) and a one-liner. The reader sees what the product IS today before reading how it got here. Earlier from the same study: the landing hero's tightened display register (leading 1.02, tracking -0.03em) and the honesty tag on the walkthrough card.

Design

Every page polished for espresso: two systemic sweeps + borderless chips

Getting every app page to the landing's bar, by fixing the SYSTEM rather than 30 screens:

Getting every app page to the landing's bar, by fixing the SYSTEM rather than 30 screens:

  • Shadow sweep: 111 hardcoded shadow literals (93× the same raised string) across 50+ files replaced with the theme-aware shadow-rest / raised / float tokens, so every card now has real depth on espresso and the warm tint on cream. Only deliberate one-offs remain (sidebar ring, the changelog glow, kbd inset).
  • Central dark tint layer (globals.css): the ~110 light-assuming status tints (bg-amber-100 text-amber-700 chips, icon tiles, banners) are remapped once, in one place, for .dark/.section-dark — pale washes become translucent hue washes, emphasis text brightens to shades tuned to the dark Daylight palette. The shared status maps (leadStatus/leadTemp/dealStage) ride it with zero call-site changes.
  • Chips lose their borders (Daylight rule): Badge outline → tone wash; Button outline → raised tone step + shadow (no hairline); Kbd and Alert borderless. Badge grew semantic tones (success/warning/danger/info/clay/neutral) so pages converge on ONE chip language instead of hand-rolled spans.

Verified on the espresso app: Pipeline (kanban chips as hue washes), the deal hub (stage gate, MEDDPICC, timeline), Accounts. Also fixed a banned em dash in the pipeline briefing sentence.

Product

The app goes espresso

The whole signed-in app now runs the dark espresso theme, the landing's register: AppShell carries .app-theme dark text-foreground, the sonner toaster pins dark, and a tiny DarkBody client…

The whole signed-in app now runs the dark espresso theme, the landing's register: AppShell carries .app-theme dark text-foreground, the sonner toaster pins dark, and a tiny DarkBody client helper mirrors the scope onto <body> so Radix PORTALS (dialogs, dropdowns, tooltips, which mount outside the shell) resolve dark tokens too, portals only open after interaction so there is never a flash. The token system carried the surfaces: Desk, briefing, tables, sidebar all read correctly on first flip thanks to the /design contrast pass landing in the reusables first.

Fix

/design goes espresso: the dark-mode contrast pass, fixed in the reusables

The user's read on "the app feels light but not the landing's way" became a direction: the design page now runs the full espresso dark theme as the proving ground. /design wears .app-theme…

The user's read on "the app feels light but not the landing's way" became a direction: the design page now runs the full espresso dark theme as the proving ground. /design wears .app-theme dark, an espresso rail (.section-dark sidebar with the landing wordmark register), the landing's ruled eyebrow on the header, and a dark-aware canvas (faint cream dots, warm glow, never a white haze). Global radius knob turned down 14px→10px (crisper at app density).

The contrast pass then went component by component, every fix in the REUSABLE layer so the whole app inherits:

  • Root cause found: .dark only remaps variables, so any text without an explicit color class inherited body's light-resolved ink — dark-on-dark across the page. Fix: the dark scope carries text-foreground (documented in the page).
  • Table rewritten (ui/table.tsx): mono uppercase header BAND on a tone step with a stronger rule, real cell padding (px-4 py-2.5), theme-real hover (row-hover token, not invisible muted/50), selected rows in the warm clay wash, footer on the strong rule. Now reads as a data grid in both themes; every table in the app upgrades for free.
  • Elevation became theme-aware: --shadow-rest/raised/float now route through --elev-* vars (warm-tinted on cream, real black weight on espresso — a 5% warm shadow is invisible on dark).
  • Skeleton / Progress / Slider tracks: bg-muted (a near-zero tone step on espresso) → foreground-relative washes, visible in both themes.
  • Scrims (dialog/sheet/drawer/alert-dialog): bg-black/10 gets dark:bg-black/45.
  • Sonner on the dark page pins theme="dark" (it otherwise follows the OS, not the page scope).
  • Canvas glow fix shipped to the light app too: the near-white radial that washed every app page pale at the top is now warm cream.
Product

Accounts sorted by recency

/ae/accounts now orders by last activity — the account's own last_activity_time or the freshest of its deals', whichever is later — instead of open pipeline, with a visible Last activity…

/ae/accounts now orders by last activity — the account's own last_activity_time or the freshest of its deals', whichever is later — instead of open pipeline, with a visible Last activity column so the ordering is legible (a sort you can't see is a sort you don't trust). Header sentence updated to say so; pipeline stays as the tiebreak. Also gave the list page its missing PAGES.md row.

Agents

The Chaser: promises tracked, kept, chased (agent #6) + the lead thread gets the matcher

People commit to things on threads and calls — "we'll send the proposal by Friday", "Vikrant will share it with the US team" — and those promises evaporate. Two ships close #13 completely:

People commit to things on threads and calls — "we'll send the proposal by Friday", "Vikrant will share it with the US team" — and those promises evaporate. Two ships close #13 completely:

The Chaser (src/lib/ai/agent-chaser.ts): reads an entity's deterministic sources — the matched email thread (full bodies) + the recorded calls' action items — and extracts each concrete commitment: crisp text, who made it, which side (WE/THEY), the due date only when one was actually stated (resolved against that source's date, never invented), and the exact source it came from. A fresh-context critic drops anything the sources don't support (extraction filters, it never repairs). Rows land in `commitments` with full provenance (source email/recording + agent run id; a dedupe index makes re-scans idempotent). The Promises panel (promises-panel.tsx) sits on the AE deal hub and both lead hubs: WE/THEY typeset rows, due chips (amber → rose when overdue), source links into the recording page, and in-place kept / dismiss — REAL writes to our table, audited as COMMITMENT_FULFILLED/COMMITMENT_DISMISSED (not previews). Verified live on centaurihs: 7 promises from 3 emails + 2 calls (6 ours, 1 theirs — Vikrant taking Antino to his US counterparts), all due dates correctly resolved to the Jul-14 proposal deadline, critic verdict FILTERED (it dropped an unsupported one), ₹0.79. 6 of 8 agents live.

The matcher reaches the lead thread (#13's leftover): getLeadTimeline now uses the same 4-branch corporate-domain matcher as the deal thread — exact + company-domain mail in ONE indexed pass, with a per-row exact flag driving "with this lead" vs "team thread @domain" (the old code only fell back to domain when exact matched nothing, hiding team threads entirely — SwiffyLabs' finance-team invoices now show on Ashish's lead). And a hazard found before it shipped: a lead at microsoft.com would have dragged 400+ dmarcreport@/no-reply@ notifications into their timeline — new NOISE_LOCALPARTS filter on the from-domain branch (431→15 real rows), exact-address matches untouched, deal side inherits the fix.

Product

The deep test tier: correctness, not rendering

"Very detailed test cases, not surface-level smoke" — the new tests/e2e/deep.spec.ts (17 tests) asserts three things smoke tests can't:

"Very detailed test cases, not surface-level smoke" — the new tests/e2e/deep.spec.ts (17 tests) asserts three things smoke tests can't:

  • UI == DB, exactly. Every checked value is computed from the database through the app's own pure formatters and asserted verbatim in the UI: the deal hub's ₹ amount/account/contact; the pipeline board's card count AND header total vs the owner's SQL aggregate; a lead search returning the exact DB row; the MEDDPICC panel mirroring `deal_qualifications` dimension-by-dimension (all 8 scores + the "N/8 read" chip); the Promises "N open" chip; the recording Signals tab badge vs the insights count; meeting attendee counts; the admin AI ledger's per-agent cost vs the SQL sum; the audit trail showing the actual newest event's summary; the Deal Check receipt ₹ vs agent_run_summary.
  • Permission boundaries probed. SDR bounced out of /admin and /ae (URL asserted); an AE opening ANOTHER rep's deal sees the read-only banner and no action buttons; a nonexistent deal 404s; a garbage ?peek= token can't break the page; unauthenticated hits redirect to sign-in.
  • Business rules. Briefing storage idempotency (two desk visits → still exactly one row per user/day).

One real locator lesson: "Competition"/"Champion" also appear inside timeline email previews (also lis), so panel assertions must scope to the section — the kind of false-positive smoke tests never notice. Suite total: 72 tests (37 smoke + 13 journeys + 5 mutations + 17 deep), 71 passed + 1 transient-flaky-passed-on-retry, ~52s.

Product

Mutation journeys: the flows that change state, tested + restored

The missing half of journey coverage. tests/e2e/mutations.spec.ts (serial) drives the five state-changing flows end-to-end and asserts both the UI acknowledgement (the toast — principle 13)…

The missing half of journey coverage. tests/e2e/mutations.spec.ts (serial) drives the five state-changing flows end-to-end and asserts both the UI acknowledgement (the toast — principle 13) and the actual write, then deletes exactly what it created (surgical predicates: action + entity + after-timestamp — tests/e2e/db.ts with waitForRow/cleanAudit helpers):

  1. 01Log a call on a dealCALL_LOGGED PREVIEW audit + close-on-activity chain completion → completions + audit restored.
  2. 02SAL handoff → synthetic SAL_HANDOFF_SENT seeded → appears in /ae/handoffs → Accept → SAL_ACCEPTED → both events + notifications removed.
  3. 03Mark a promise kept → REAL commitments status flip + audit → status restored. (First version leaked: it pre-picked a commitment by id but the panel's display order differs, so a different row flipped — caught by the timeout, leak repaired, test now tracks whichever row actually changed.)
  4. 04Link an unlinked recording to its suggested deal → real proshort_recording_links row → deleted.
  5. 05SDR logs activity on a lead → PREVIEW audit + chain completion → restored.

Full suite: 37 route smokes + 13 read journeys + 5 mutation journeys = 55 tests, ~45s, zero pollution (verified: no e2e audit rows, flips, completions, or links left behind). One retry configured — the live dev server can 500 transiently while recompiling under 4-worker load (2 flaky-passed-on-retry, 0 hard failures). Still open for full story coverage: admin user-management mutations (invite/role/suspend — need a disposable user fixture), settings/change-password (would rotate the shared dev password — deliberately excluded), and the write-back journeys (blocked on grants).

Agents

Test infrastructure: route smoke suite + agent evals + CI

The honest audit said it: agents were verified live but had no eval suite, and the early Playwright pass was a one-off, not maintained. Built the first two tiers:

The honest audit said it: agents were verified live but had no eval suite, and the early Playwright pass was a one-off, not maintained. Built the first two tiers:

  • Route smoke suite (tests/e2e/, npm run test:e2e) — Playwright signs in one seeded user per role (global-setup samples live entity ids from the DB), then hits 37 routes across admin/AE/SDR/public — every static page plus real dynamic instances (deal hub, lead hubs, account, contact, recording, meeting event, a peek URL). Each must return <500, render a landmark, show no Next error state (the dev overlay text is checked through the shadow DOM — <nextjs-portal> itself always exists in dev), and log no serious console errors. 37/37 green in ~39s — and the suite caught a real bug on its maiden run: /admin/ai 500'd because as w(window) used a reserved SQL word. Fixed.
  • Agent evals (scripts/evals/run-evals.mjs, npm run test:evals) — a token-guarded eval endpoint (/api/evals/run) runs each live agent against a real fixture entity; the runner asserts hard invariants: valid artifact structure (8 MEDDPICC dims, scores from the enum, every non-unknown score carries evidence; brief sections within bounds; extraction caps), sane verdict, run totals == the llm_calls ledger exactly, and a per-run cost ceiling ($0.10). All 4 evals passed — Meeting Prep even exercised the REPAIRED path live.
  • CI (.github/workflows/ci.yml) — typecheck + lint on every push; e2e/evals run locally (need the dev DB + key + server).

Next tiers (planned): golden-fixture grounding snapshots per agent (deterministic, no-API), the 8–10 money-flow tests (peek, link-recording, run-agent with a mocked client, close-on-activity, handoff accept), nightly evals with trend tracking off agent_run_summary.

Product

LLM cost + telemetry infrastructure: the ledger

Cost was estimated with hardcoded constants duplicated in five files, and tool-loop runs undercounted tokens (toolRunner's final.usage is the LAST call's usage, not the loop's sum — proven…

Cost was estimated with hardcoded constants duplicated in five files, and tool-loop runs undercounted tokens (toolRunner's final.usage is the LAST call's usage, not the loop's sum — proven live: a Deal Check run reported 10.9k tokens; the ledger counted 15.7k). Built it properly:

  • `llm_pricing` — rates per model (input/output/cache-write/cache-read per Mtok); cost computed in ONE place, re-computable.
  • `llm_calls` — the ground-truth ledger: one row per API call (trace_id, agent, purpose loop|critic|repair|generate|extract|reemit, model, all four token kinds incl. prompt-cache, latency, stop reason, status, cost). Every rollup — per run/agent/user/entity/day/total — is a query over this.
  • Auto-instrumentation (src/lib/ai/telemetry.ts) — instrument() patches the Anthropic client once in getAnthropic() so every messages.create (incl. the ones toolRunner makes internally) self-logs; withLlm() carries agent/entity context via AsyncLocalStorage, t.mark() tags phases, t.totals() is the real sum. Fixed a race: ctx updates now complete before create() returns (the critic call was missing from totals).
  • All 5 agents rewired (Deal Check, Meeting Prep, Email Drafter, Promise Tracker, Daily Brief): hardcoded COST consts deleted; run summaries now write ledger-true tokens/cost + trace_id (runs join to their calls).
  • `/admin/ai` — the spend surface: today/7d/all-time in the briefing sentence, per-agent table (runs, verdicts, calls, tokens, avg latency, cost), the raw recent-calls ledger. Nav item "AI usage".

Verified: one Deal Check run = 5 ledger rows (4 loop turns + critic, 1.9s→13.3s each, per-call cost); regenerated all 17 briefings — 17/17 runs' totals equal the ledger exactly. Ledger stands at 28 calls ≈ ₹10.74.

Product

Goldenflitch staff are app users (decision: yes)

Executed the standing decision. scripts/add-goldenflitch-users.mjs (idempotent, every mutation audited as SYSTEM): sourced the GF tenant's own directory (53 mailboxes), skipped 9 shared…

Executed the standing decision. scripts/add-goldenflitch-users.mjs (idempotent, every mutation audited as SYSTEM): sourced the GF tenant's own directory (53 mailboxes), skipped 9 shared service inboxes + 2 already-existing people (Vinay = the CEO; Shraddha Sharma name-match flagged, not duplicated), created 39 users (role DEV, department "Goldenflitch", no password — invite-link to activate), and repaired the 3 placeholder identities: Harshad → harshad@goldenflitch.com (Harshad Bhardwaj), Dhruv Raj → dhruv@, Abhishek Rana → abhishek@. Then re-matched mappings: graph 50 unmatched → 10 (all service inboxes), zoho +2.

The repair is also a data-quality win: mailbox-scoped surfaces (briefing, meetings, inbox) key off user.email, and Harshad's real mailbox holds 7,444 messages vs the 585 his guessed @antino.com address saw. Nuance parked in TASKS: he genuinely has both mailboxes — multi-mailbox users would need a user_email_aliases concept someday.

Agents

The agent team gets plain-English names

"Witness" and "Qualifier" mean nothing to a salesperson — the name should say what the agent does. Renamed the whole team and propagated everywhere: Daily Brief (Briefer) · Email Drafter…

"Witness" and "Qualifier" mean nothing to a salesperson — the name should say what the agent does. Renamed the whole team and propagated everywhere: Daily Brief (Briefer) · Email Drafter (Writer) · Call Notes (Witness) · Deal Check (Qualifier) · Meeting Prep (Researcher) · Promise Tracker (Chaser); planned: Meeting Minutes (MoM), What's Next (Sorter). Single source of truth: src/lib/ai/agents.ts. Propagation: every agent writes the new name into agent_run_summary.agent_name + audit actorLabel; all UI copy (panel toasts, buttons, receipts, StubButton hints, "Copilot draft" → "Draft email"); agent system prompts; 115 telemetry rows migrated to the new names. One deliberate exception: historical audit_events rows keep the labels they were written with — the audit trail is append-only and history is never rewritten.

Agents

The Researcher: the pre-call brief (agent #5)

Walk into every meeting prepared. On the meeting-event page, "Brief me" → the Researcher gathers deterministic grounding (who the external attendees are in the CRM — lead/contact → their…

Walk into every meeting prepared. On the meeting-event page, "Brief me" → the Researcher gathers deterministic grounding (who the external attendees are in the CRM — lead/contact → their deal + account; the recent email thread both directions; the last recorded calls' recap + action items/risks) and writes the brief: headline goal · who's in the room · where it stands · still open · lead with · ask them. Same guarantees as the rest of the team: a fresh-context critic flags any fact not in the grounding (one repair round), meeting_briefs stores one row per (mailbox, meeting), agent_run_summary + AGENT_RESEARCHED audit event, receipt on the panel.

Verified live on the Right Horizons negotiation meeting — the brief nailed the deal's actual shape from 5 recorded calls + the thread: "Bridge the 150% budget gap… reworked two-phase proposal that fits their 25-30L constraint"; who: "Sunil Kawariya — decision-maker; firm on 25-30L, rejected three-phase model"; still open: Anil's internal greenlight; ask: "Has Anil signed off on the Phase 1 scope?". Files: src/lib/ai/agent-researcher.ts, scripts/agents/create-researcher-table.mjs, event-page actions.ts + researcher-panel.tsx. 5 of 8 agents live.

Agents

The Qualifier: MEDDPICC scored from real evidence (agent #4)

The deal hub's empty MEDDPICC scorecard is now filled by a real agent. Same harness as the agentic Writer (tool loop → fresh-context critic → repair), new job: score the 8 MEDDPICC…

The deal hub's empty MEDDPICC scorecard is now filled by a real agent. Same harness as the agentic Writer (tool loop → fresh-context critic → repair), new job: score the 8 MEDDPICC dimensions with the evidence that earned each score.

  • `src/lib/ai/agent-qualifier.ts` — INVESTIGATE via the deal tool belt, then emit strict JSON (per-dimension score unknown|weak|partial|strong + evidence, headline, gap, suggestedFocus). Guardrails: a non-unknown score with empty evidence demotes to unknown; JSON extracted substring-wise; a re-emit round recovers truncated output (the first live run failed exactly here — too-small max_tokens + a strict parse; fixed). The critic audits that scores are EARNED ("strong needs explicit confirmation, not a passing mention"), one repair round.
  • Stored in deal_qualifications (one row per deal, latest wins; scripts/agents/create-qualifier-table.mjs) + agent_run_summary + an AGENT_QUALIFIED audit event; RACI-gated runQualifier action.
  • `qualifier-panel.tsx` — headline read, Biggest gap, 8 score rows (dot-colored, evidence expands per row), a clay Next move, Run/Re-qualify, and the auditable-AI receipt (verdict · tokens · ₹).

Verified live on the centaurihs deal (the very deal linked via the new in-place connect — full circle): 14 tool calls (bundle×1, email×3, call-intel×2, transcript-search×8), verdict PASS, ₹1.47. The read is blunt and grounded: "Early-stage prospect with implicit need but no urgency, incumbent entanglement, and US-based economic buyers — poor fit for short-cycle deal"; Competition scored strong quoting the Jul-1 call (two incumbent EOR vendors, 3 offers extended); next move: "Before sending the proposal due Jul-14, confirm who at Centauri HQ will evaluate it and secure a warm introduction — otherwise proposal dies in inbox."

Docs

/changelog: the journal becomes the public changelog

Built the build-in-public marketing page: /changelog is generated live from this very file. src/lib/changelog.ts parses the ## YYYY-MM-DD — Title entries (72 across 8 days at launch),…

Built the build-in-public marketing page: `/changelog` is generated live from this very file. src/lib/changelog.ts parses the ## YYYY-MM-DD — Title entries (72 across 8 days at launch), groups by day, and derives one tag per entry from its title (Agents / Fix / Performance / Design / Data / Docs / Product). The page (landing register, Daylight tokens, SiteNav/SiteFooter): an espresso hero — "Every day, shipped." — with the progress measured as big numerals (shipped / days / per-day average), a per-day velocity bar chart with the peak called out and today's bar glowing, and tag filters (?tag=agents, server-rendered). Below: the latest entry featured in full, then a date-grouped timeline — a monument day rail (sticky), a colored spine with a node per entry, tag mark + title + teaser collapsed, full markdown on expand. force-dynamic so every journal edit appears immediately. The journal IS the changelog — no second copy to maintain.

Data

Corporate-domain email matcher (#13): the whole company thread, not one contact

A user question — "why am I not seeing the latest emails on this deal?" — turned out to be two things. First, a diagnosis that cleared the air: nothing was stale. Both mirrors are current…

A user question — "why am I not seeing the latest emails on this deal?" — turned out to be two things. First, a diagnosis that cleared the air: nothing was stale. Both mirrors are current (Zoho to 07-13/14, Graph email to today), and on the TruBoard Cleantech deal Jul 10 genuinely IS the newest email — confirmed by scanning the whole mirror for any TruBoard mail after it (zero). Anisha's mailbox is synced (anisha@antino.io.io not .com; her login/CRM identity vs her mailbox differ, bridged by the mapping).

But the diagnosis surfaced the real limitation: the deal's email thread matched only its mapped Zoho contacts — here a single contact, S. Kadhirvel — while the person actually driving the thread, Sohail Usmani (@truboardpartners.com), isn't a Zoho contact at all. His mail showed only incidentally (he cc'd Kadhirvel); drop that cc and it'd vanish. The whole TruBoard account has a team in Zoho (Srickant, Vipul, Rakesh…), none attached to this deal.

The fix (#13): match a deal's emails by the company domain, not just its contacts. src/lib/graph/match.ts — a reusable matcher: corporateDomains() (derive domains from the deal + account contacts, dropping a personal/internal exclusion list — gmail/yahoo/… and antino.com/antino.io/goldenflitch.com, so we never over-match free providers or internal mail) + matchedMessageIds(emails, domains) (a UNION of four branches: from-email, from-domain, recipient-email, recipient-domain). Wired into getDealTimeline (the thread — full 4-way match) and getDealInbox (the left-pane ranking — exact-email + from-domain only). TruBoard now surfaces 10 of Sohail's emails directly, plus the rest of the account's conversation.

The performance was the hard part (the whole reason it's worth logging): the natural shapes are traps. A top-level OR … EXISTS predicate → 8,305 ms (seq-scans all 170k messages). IN (select … from cte)1,078 ms (a CTE defeats the functional index — Postgres hash-semi-joins with a full seq scan; confirmed via EXPLAIN). The fix: pass emails/domains as array parameters and use = any($arr) in a UNION of index-backed branches29 ms. Added one index (idx_gmr_domain_lower on lower(split_part(email,'@',2)) for the recipient side) to scripts/perf/add-indexes.mjs. The inbox ranking stayed lean (exact-email + from-domain, recipient-domain dropped — it cost +480ms for barely any ranking change): 162 ms → 54 ms. Lesson banked: never match email sets against a CTE with `IN`/`EXISTS`; materialize to an array and `= any` the functional index. Second gotcha (caught in the browser after the first commit): ` sql${jsArray}::text[] renders as a **row constructor** ($1,$2,…)::text[] — a cast error — because drizzle expands a JS array into a paren-list. Build the literal explicitly: sqlarray[${sql.join(vals.map(v => sql${v}), sql, )}]::text[] array[$1,$2,…]::text[]. Verified via PgDialect().sqlToQuery + a live getDealTimeline run, not just a raw-pg` scratch (which had hidden the bug — raw pg accepts a single array param, drizzle doesn't render one).

Product

"Part of": explicit call→CRM linkage + in-place connect (a real write)

The recording page never said whether a call was part of a deal — mkccuiq0 was auto-linked to its contact while the matching centaurihs deal sat unlinked, silently. Now:

The recording page never said whether a call was part of a deal — mkccuiq0 was auto-linked to its contact while the matching centaurihs deal sat unlinked, silently. Now:

  • "Part of" states linkage per kind — Deal / Lead·Contact / Account each show "✓ name" (clay tile, click-through, "· Linked by you" when manual) or an honest "no deal" with an attention Pulse (the deal is the one that matters for pipeline). Header pulse: "not part of a deal — connect it →".
  • In-place connectsuggestLinks (consolidated with the parallel session's version into one superset query) proposes candidates from the call's external attendees (email → lead/contact → their deals/account, company domain) and the prospect company name, each with the reason shown ("attendee is the deal contact"). One click links it; a debounced search over deals/leads/accounts covers everything else.
  • A REAL write, todayproshort_recording_links is our own table (source='manual', method='user'), no Zoho scope needed; every link logs RECORDING_LINKED (SUCCESS, not preview) to the audit spine; toast + instant refresh (principle 13).

Verified end-to-end on mkccuiq0: suggestions surfaced the centaurihs deal + account → one click → "Part of: centaurihs · Deal · Qualification · Linked by you", pulse cleared, suggestion consumed.

Agents

#10: the Writer is AGENTIC — tool loop + critic, live on the AE inbox

The Copilot draft is no longer a pre-stuffed prompt: it's a real agent. Built on the official SDK's Tool Runner (client.beta.messages.toolRunner, betaTool) — deliberately NOT the "Claude…

The Copilot draft is no longer a pre-stuffed prompt: it's a real agent. Built on the official SDK's Tool Runner (client.beta.messages.toolRunner, betaTool) — deliberately NOT the "Claude Agent SDK" package, which is Claude-Code-as-a-library (subprocess + filesystem tools; wrong fit inside a Next.js server action). The Tool Runner is the SDK's model-driven tool loop over tools we define — genuinely agentic, in-process.

  • `src/lib/ai/tools.ts` — the Writer's tool belt, READ-ONLY and deal-scoped by construction (ids resolved at build time; get_email refuses ids off this thread): get_deal_bundle (facts + thread INDEX), get_email (full body + attachments), get_call_intelligence (recap/action-items/risks), search_transcripts (FTS over the deal's linked call transcripts — the GIN index earning its keep). Every call is logged for the receipt.
  • `src/lib/ai/agent-writer.ts` — INVESTIGATE→DRAFT (toolRunner, Haiku per the model dial, max 8 iterations) → CRITIC (fresh context, judges the draft against the evidence the tools actually returned: no invented facts/numbers/commitments, exactly one ask) → one repair round on failure. Telemetry row per run in agent_run_summary (model/tokens/cost/verdict).
  • Receipt in the UI — the toast now says what the agent did: "Writer investigated (deal bundle×1 · email×3) · critic flagged & repaired".
  • Verified live on the GasTech deal: agent chose to open 3 emails, critic flagged, repair fixed it; grounded draft with one clear ask. ~9.9k tokens in / 344 out ≈ ₹1 per draft on Haiku.

Also this stretch: grounding.ts (deep thread grounding — full email bodies, meetings, call intel, attachments + receipt) now powers the SDR lead Writer; the deal Writer's fetchers became the agent's tools, as planned.

Agents

Markdown renderer for user/AI-authored text (+ transcript backfill complete)

"Markdown to html is not working" — traced to two real homes: (1) lead descriptions carry AI-enriched markdown ( bullets, bold — e.g. the Target/Bandhan leads), and (2) one transcript turn…

"Markdown to html is not working" — traced to two real homes: (1) lead descriptions carry AI-enriched markdown (* bullets, **bold** — e.g. the Target/Bandhan leads), and (2) one transcript turn where the prospect pasted formatted requirements into the meeting chat and Proshort merged it into the transcript. (The Proshort long fields/insights have zero literal markdown — checked all 548.) Built src/components/markdown.tsx: a tiny dependency-free renderer (bold/italic/code/links/headings/bullet+numbered lists) that outputs React nodes, never injected HTML (XSS-safe by construction), styled on the DESIGN text scale (clay-dot bullets, mono numerals, semibold ink emphasis). Wired into: transcript turns (recording page), AE + SDR lead descriptions, and the deal "What they want" panel. Verified on the Target lead + the InfraKeys transcript.

Also: the transcript backfill finished — 498/548 recordings now have speaker/timestamped transcripts (50 genuinely empty, 0 failed).

Data

Recording page v4: chat transcript, deadline chips, "not in your CRM" pulse + Add to CRM

Four upgrades on top of the tabbed rebuild, plus three new design principles:

Four upgrades on top of the tabbed rebuild, plus three new design principles:

  • Chat transcript — buyer left (white cards) / seller right (clay wash), full-width, with the buyer/seller labels anchoring each side. The join is proshort_recording_parties.speaker_name → affiliation. You can read the negotiation's rhythm at a glance.
  • Parser coverage — action items without an "(Org)" ("Mahak - Share the framework… - No deadline specified") now parse: owner matched to the call's external parties for the right avatar tint, and the trailing deadline becomes a chip (amber when real, muted for "no deadline"). Checked: there is NO literal markdown (**/##) anywhere in the 548 recordings — the "raw text" smell was unparsed structure, not un-rendered markdown.
  • "Not in your CRM" pulse + actiongetRecordingParties now carries crmKind (contact | lead | null by email). An external participant who exists nowhere in Zoho gets an attention Pulse + an Add to CRM button → proposeContact server action logs CONTACT_ADD_PROPOSED (PREVIEW) with the full proposed record to the audit spine; button flips to "✓ proposed" + toast. Captured externals show a Lead/Contact chip.
  • Tabs look like tabs — the line variant: bold active label + clay underline.

DESIGN.md principles 13–15 (from this session's gaps): 13 Every action is acknowledged (toast naming what happened + what happens next, or the control flips to its done-state — never silence); 14 The text color scale (ink → body /85 → meta muted → mono eyebrow; tabular-nums; one clay number per surface; never bold whole sentences); 15 Parse structure out of strings (owner/title/deadline embedded in a string is structure wearing a costume — split and typeset it, never dump the bullet).

Design

Recording page v3: three jobs, three tabs (10/10 pass)

The single-scroll rebuild still read poorly (lopsided hero cards, a wall of 9 uniform risk bullets, a starved rail, no visual anchor). The insight that fixed it: a recorded call has three…

The single-scroll rebuild still read poorly (lopsided hero cards, a wall of 9 uniform risk bullets, a starved rail, no visual anchor). The insight that fixed it: a recorded call has three different jobs — act on it, mine it, read it — so the page is now Overview / Signals / Transcript tabs with a persistent context rail. What made it sing:

  • Parse the structure inside insight strings instead of dumping flat bullets. "Owner (Org) — task" → numbered checklist with initials avatars (clay = Antino, sky = prospect); "Title — detail" → semibold lead-in + muted detail. Same InsightText parser across Signals.
  • Talk time / share of voice — computed from the word-level timestamps (proshort_transcript_segments): a stacked bar + % per speaker (e.g. prospect 55% — a healthy discovery signal). Duration ("24-min call") now in the header sentence and on the video card.
  • Turns, not fragments — 272 utterances merged into 69 speaker turns, per-speaker stable colors, timestamps; transcript flows on the surface in its own tab (no boxed inner scroll).
  • The video is the anchor — the rail leads with a real thumbnail card (play overlay + duration badge → recording URL), then Linked in CRM, then participants with avatars + external count.

getRecordingTranscript gained end_sec. Register: product (earned familiarity — Gong-class patterns, Daylight language).

Fix

Proshort: incremental 10-min sync + a false-match fix

The mirror had gone stale (525 recordings, last one 12-Jul) because the only tooling was a full backfill (~10 min of throttled calls — too heavy to run often). Re-ran it (525 → 548, now…

The mirror had gone stale (525 recordings, last one 12-Jul) because the only tooling was a full backfill (~10 min of throttled calls — too heavy to run often). Re-ran it (525 → 548, now current through today 14-Jul 11:30), then built the missing incremental path:

  • `src/lib/proshort/sync.ts`syncProshortIncremental(): walks the recordings list newest-first and stops as soon as a page holds nothing new, so a routine run is 1 list call + N details (usually N=0). Also refreshes the last 2 days of recordings, whose insights Proshort finishes generating after the call. Writes a DELTA batch row.
  • `POST/GET /api/proshort/sync` — token-guarded (shared token or Vercel Cron's Bearer $CRON_SECRET).
  • Every 10 minutes: vercel.json cron (*/10 * * * *) for deploy, + scripts/proshort/watch.mjs for local (a loop that hits the route; node scripts/proshort/watch.mjs).
  • Bug fix — false recording matches: getMeetingEvent's matcher joined on any shared party email, so a recording could link to a meeting merely because a colleague was in both (only the 30-min window prevented it). Now requires a genuine external party via the shared nonCorporateDomainsSql (which already excludes our own tenants + free providers).

Finding: the "Negotiation Discussion — Right Horizon x Antino" (13-Jul) has no recording because that call was never recorded — the mirror is current and 16 other calls exist from that day; Right Horizons' last recorded call is 03-Jul.

Fix

Two calendar-data bugs found via the briefing: recurring subjects + a 5:30 timezone error

The briefing surfaced two real mirror bugs (both now fixed in the shared DB + at the source):

The briefing surfaced two real mirror bugs (both now fixed in the shared DB + at the source):

  1. 01Recurring occurrences had no subject. 12,813 of 23,523 graph_events (every event_type='occurrence') had subject/organizer null — Graph puts those on the series master, and occurrences only reference it. Backfilled all 12,813 from their masters (subject, organizer, is_online_meeting, location, body_preview). "(no subject)" is gone.
  2. 02Every meeting was 5:30 early (UTC parsed as IST). graph/importer.ts's dt() did new Date(String(v)). Graph event start/end (dateTimeTimeZone) is a naive timestamp (zone in a separate field, always UTC here); JS parses a bare timestamp as local, so on the IST host every event stored 5:30 early — breaking times AND which-day bucketing (evening meetings fell into the wrong day, so "today's meetings" mis-timed and mis-counted). Fix: dt() now appends Z when there's no zone marker (message receivedDateTime/sentDateTime already carry Z, so they're untouched and were always correct). Backfilled graph_events.start_at/end_at +5:30 (one-time; verified against Anisha's real Outlook: 12:30 / 16:00 / 19:00 / 20:00 — exact match). Blast radius confirmed to be events only (the 4 dt() call sites: event start/end broken, message received/sent fine).

Regenerated all 17 briefings — times, day, and counts now correct.

Product

Accounts table: from a number ledger to a state view

The accounts list was a flat ledger (Deals / Open / Pipeline / Won / Last activity), numbers you had to decode, not a signal you could scan. Reframed it around the one question that…

The accounts list was a flat ledger (Deals / Open / Pipeline / Won / Last activity), numbers you had to decode, not a signal you could scan. Reframed it around the one question that matters: "which accounts should I act on, and why?" Every account now carries a State derived from its deals + activity, computed in code: Live (open deal moving, recent), At risk (deals past close or no next step), Cooling (no touch 60d+), Won (closed, expansion target), Dormant (nothing open). Columns are now Account · State · In flight · Open pipeline · Last touch · Won, where "In flight" shows the actual biggest open deal + its stage ("centaurihs · Qualification") instead of a meaningless "Open: 1", the State chip has a hover reason, and a stale Last-touch goes rose. getAccounts gained the top-open-deal (lateral join) + slipping / no-next-step signals. Still ordered by recency. Next candidate: a State filter so an AE can jump straight to At-risk.

Product

AE Follow-ups page (the missing backlog home)

AEs had no follow-ups surface, so Anisha's 174 overdue had nowhere to live. Honest finding first: her tasks are recent (12d), contact-linked lead follow-ups with NO done-evidence (1 of 174…

AEs had no follow-ups surface, so Anisha's 174 overdue had nowhere to live. Honest finding first: her tasks are recent (12d), contact-linked lead follow-ups with NO done-evidence (1 of 174 detectably clearable, 0 stale), so an evidence-based cleanup panel would be empty and useless for AEs. The right surface is a place to SEE and WORK them, not a magic cleaner. Built /ae/tasks (getAeOverdueTasks resolves each to its linked deal/contact; AeTaskList client): the overdue list with per-task Done (writes the completion overlay), a Clear all bulk, and each row links to its contact/deal to go work it. The cleanup panel still shows on top when it does find safe-to-clear buckets (AEs with closed-deal tasks). Nav item added. Close-on-activity remains the real fix, working the lead clears the chain.

Product

Title-case names (initcap at the data layer)

Zoho names are entered any which way ("bhargav krishna", "NAGA KUMAR"). Wrapped the 12 lead/contact display-name expressions across sdr/ae/peek queries in Postgres initcap(), so every…

Zoho names are entered any which way ("bhargav krishna", "NAGA KUMAR"). Wrapped the 12 lead/contact display-name expressions across sdr/ae/peek queries in Postgres initcap(), so every render (lists, hub header, breadcrumb, hover cards, timeline senders, briefing) shows title case at once. Idempotent for already-correct names; only intentional mid-word caps (McDonald) flatten, which is rare and the source is usually lowercase there anyway. Verified: "bhargav krishna" → "Bhargav Krishna" in header + breadcrumb + timeline.

Product

Timeline email previews: strip the chrome, keep the message

The lead/deal/inbox timelines dumped raw body_preview: Outlook's "You don't often get email from…" safety banner, "Get Outlook for iOS" footers, long ____ separator runs, and forwarded…

The lead/deal/inbox timelines dumped raw body_preview: Outlook's "You don't often get email from…" safety banner, "Get Outlook for iOS" footers, long ____ separator runs, and forwarded "From: … Sent: …" / "On <date> … wrote:" tails. Added cleanEmailPreview (src/lib/format/text.ts) that runs after htmlToText and removes all of that, leaving just the human sentence, and wired it into getLeadTimeline + getDealTimeline. E.g. a preview that read "You don't often get email from bhargav… Learn why this is important Running 5 minutes late… Get Outlook for iOS ___ From: Naga…" now reads "Running 5 minutes late. Joining soon. Regards, Bhargav." The full email reader/peek keeps the whole body (a deliberate read, not a preview).

Design

App-wide em-dash sweep (design copy rule 17)

Replaced 402 prose em dashes ( — inside rendered strings) with commas across 148 files, enforcing the no-em-dash copy rule everywhere, not just the Briefer. The sweep protected code…

Replaced 402 prose em dashes ( inside rendered strings) with commas across 148 files, enforcing the no-em-dash copy rule everywhere, not just the Briefer. The sweep protected code comments (skipped lines starting //, *, /*, and em dashes sitting in trailing line comments) and left the empty-value '—' table glyphs alone (a legit convention, not prose). Builds clean (tsc 0). A few start-of-text-node cases ({' '}— …) the line sweep missed are being caught by hand as spotted.

Design

Lead-hub polish + the company→account reality

Fixes from a live QA pass on the AE lead hub:

Fixes from a live QA pass on the AE lead hub:

  • Company is clickable, and never dead-ends. A lead's company is free text; only 44% map to a real Account (measured: 8,528 of 19,476; 56% don't). So the company links to the account hub when one exists (getLead now returns companyAccountId), otherwise to that company's leads (/ae/leads?q=). The accounts search also got a real empty state: "No account named X, see leads at X →" instead of a blank.
  • Details shows only fields that have a value (the Field primitive returns null when empty, and renders internal hrefs via next/link same-tab).
  • Right-rail cards are equal widthLeadActions had lg:self-start, which shrank it to content inside the flex column; now w-full.
  • Design-system exemplar: the daily briefing elevated (hero headline, ladder body tones, tone-wash separation not border-b), a toast standard (src/lib/ui/toast.ts), and a deterministic em-dash sanitizer on the Briefer. See DESIGN.md 16-18.
Agents

The Witness: notes write themselves from the call recording

"Add note" was a hollow placeholder string. Replaced it with the Witness — on a deal, ✨ Draft from last call composes a real minute-of-meeting from the deal's most recent Proshort…

"Add note" was a hollow placeholder string. Replaced it with the Witness — on a deal, ✨ Draft from last call composes a real minute-of-meeting from the deal's most recent Proshort recording: recap + action items + risks + pain points + competition. Deterministic (src/lib/notes/recording-note.ts composes from the already-extracted proshort_recording_insights — no LLM, so it never invents; every line traces to the transcript). Matched to the deal via its buying-committee contacts or the account name. draftDealNote action + a proper note-compose panel in the deal rail (editable textarea, toast acknowledgement, Save). Preview until Zoho write-back. Verified live on centaurihs (Jul 10 call → recap + 4 action items + 3 risks + competitor). Next: same for leads + auto-fill on "Log call".

Product

Close-on-activity closes the CHAIN, not one task

Follow-ups come as chains ("Followup 1 → 2 → …"); one touch should clear the whole pending chain, not a single reminder. Reworked completeChainForLead/completeChainForDeal (was…

Follow-ups come as chains ("Followup 1 → 2 → …"); one touch should clear the whole pending chain, not a single reminder. Reworked completeChainForLead/completeChainForDeal (was completeNext*) to complete every open matched task due on/before today in one action — one audit event, closedCount reported to the UI ("N follow-ups marked done ✓"). Strictly-future follow-ups (a genuine planned next step) are deliberately left open. Tested on a real 4-task chain (Siddharth / Godfrey Phillips): 2 due + 2 future → an activity closes the 2 due, leaves the 2 future. All four callers (SDR/AE inbox, lead hub, deal hub) show the count.

Product

Close-on-activity everywhere + overlay-consistent counts

Extended close-on-activity across every work surface + made the completion overlay consistent app-wide:

Extended close-on-activity across every work surface + made the completion overlay consistent app-wide:

  • Inbox — SDR inbox (lead) and AE inbox (deal) Send / Log call / Book now log the touch AND complete the record's next follow-up (completeNextForLead / completeNextForDeal), toasting the closed step + refreshing.
  • AE lead hub — added the (now role-flexible) LeadActions rail (actions use getCurrentUser + role-aware links, still RACI-gated), so AEs get the same close-on-activity + manual close their leads' follow-ups.
  • Deal hublogCall + new logDealEmail/logDealMeeting are close-on-activity; the rail's manual close now persists.
  • Overlay consistency — every overdue-task COUNT now excludes task_completions: SDR summary + nav pulse, AE desk, the SLA board, and the briefing (ic overdue + topOverdue + team per-rep), plus getLeadTasks/getDealTasks. Clear a task once and it drops out of every count, not just the list. Verified lead + deal close paths against the DB.
Fix

Close-on-activity: doing the work closes the follow-up (the durable fix)

The backlog rebuilds because "mark done" is a separate step reps skip. So make the work itself close the task. On the SDR lead hub, logging real activity — Send email / Log a call / Book a…

The backlog rebuilds because "mark done" is a separate step reps skip. So make the work itself close the task. On the SDR lead hub, logging real activity — Send email / Log a call / Book a meeting — now completes the lead's next open follow-up in the same action (completeNextForLead, subject-matched to the lead), toasts "Follow-up marked done ✓ — you just did it", and refreshes so it vanishes. The manual Done on the lead's task list now persists too (was toast-only). Both write the task_completions overlay + an audit event (reversible, PREVIEW).

  • Extracted a shared, role-flexible src/lib/tasks/actions.ts (completeTasks bulk + completeNextForLead); the cleanup panel re-exports from it. getLeadTasks now excludes cleared tasks (overlay), so the hub reflects it instantly. Verified end-to-end (lead "Sagar/Anaplan" → "Followup 2" auto-completes + hides).
  • Not yet wired: the inbox (parallel session's /ae/inbox) and the AE lead hub (inline, no rail) — same pattern applies when those are touched.
Agents

Follow-up cleanup: clear what's already done (+ briefing below the greeting)

The overdue-task mountain (19.5k team-wide) is mostly uncleared, not undone — reps did the work and never ticked the box. Built an evidence-based cleanup that recognises done/moot…

The overdue-task mountain (19.5k team-wide) is mostly uncleared, not undone — reps did the work and never ticked the box. Built an evidence-based cleanup that recognises done/moot follow-ups and clears them for the rep, reversibly.

  • `task_completions` overlay — Zoho write scope isn't granted, so we don't fake the mirror; a completion lives app-side and every open-task query LEFT-excludes it (cleared tasks vanish immediately). Becomes the write-back outbox when scope lands.
  • `getTaskCleanup(zid)` (deterministic — no LLM decides a close): matches each overdue task to its lead (subject-based, like getTasks) AND deal (what_id), then buckets: Moot (deal closed / lead lost/converted), Looks already done (lead or deal moved after the task was due, incl. an email off the CRM — the exact "did it, never logged it" case), Stale (>90d — suggest, don't auto). Each row carries its evidence.
  • `completeTasks` action — bulk clear → overlay + ONE audit event (reversible, PREVIEW).
  • `CleanupPanel` on /sdr/tasks: three bucket cards with evidence + "Clear N". Honest by design — it clears only what it can prove (~4-5% here; the CRM lacks evidence for the rest, which correctly stay). The durable fix remains close-on-activity (prevention), noted for next.
  • Layout: the daily briefing now sits below each cockpit's greeting/KPI header (AE Desk, SDR My Day, Command) — greeting first, then the brief.
Agents

The Briefer: the daily briefing agent (first of the agent team) is LIVE

The API key landed (valid — verified), so the agent program is unblocked. Built the Briefer end-to-end and put it as the first thing on the Desk / My Day / Command board.

The API key landed (valid — verified), so the agent program is unblocked. Built the Briefer end-to-end and put it as the first thing on the Desk / My Day / Command board.

  • Storage — two app-owned tables (src/db/schema.ts + scripts/briefings/create-tables.mjs): agent_run_summary (per-run telemetry every agent writes — model/tokens/cost/verdict; the audit agent_run_id points here) and briefings (one row per user per day; sections JSONB; unique(user_id, briefing_date)).
  • Section ownership done rightsrc/lib/briefing/facts.ts computes every NUMBER in SQL, role-scoped (AE/SDR = their book; HEAD_OF_SALES/ADMIN = team roll-up). briefer.ts (Haiku) writes prose only; the schedule (today's meetings, exact IST times), at-risk deals, and rep lists are rendered deterministically from facts — the model never touches a time or amount. This fixed the first-cut bug where the model narrated meetings (wrong/empty bullets).
  • SurfaceDailyBriefing component: headline + aligned mono lists (time/amount lead) + prose sections + a highlighted "Your one move" + an auditable-AI receipt (agent · model · "numbers computed from your live CRM"). Lazy-generates on first view; getAnthropic() now prefers process.env.ANTHROPIC_API_KEY.
  • BatchPOST /api/briefings/generate (token-guarded, the cron target). Generated 17/17 live briefings: Rohit (team: "₹42.3 Cr slipping… urgency starts with Himanshu and Harshad") + every mapped SDR/AE. Reads like a real chief-of-staff.

Format: bullets everywhere. Every section is now short bullet points (the model writes 2-4 one-sentence bullets per section, never paragraphs); data lists lead with a mono time/amount, narrative bullets with a clay dot. Regenerated all 17.

Enrichment + clickable links. Three data sources added to the facts layer (all numbers/names still code-computed): (1) meeting prep — each today's meeting is matched to a deal via an external attendee → shows "— FedX · Qualification · no next step"; (2) "Waiting on your reply" — the actual unanswered threads (lead + subject + days-waiting), not just a count; (3) "Follow up from your recent calls" — Proshort call intelligence (open action items + ⚠ risks from the last 10 days of recorded calls the user was on). Every list item now carries a ref and renders as a link (role-aware): meetings → the event page, deals → the deal hub, threads → the lead, calls → the recording. Verified: clicking a meeting opens its detail (correct 16:00 IST, real subject).

Still to do: the pre-dawn cron trigger + the rest of the agent team (MoM, Sorter, Qualifier, Researcher, Chaser) + the critic pass. #11 is now in progress (Briefer shipped).

13

Jul 2026

Monday

6 shipped

Fix

AE deal inbox (Outlook-style workspace) + timeline HTML sanitization

"the AE also needs to have an inbox similar to what we built for SDR — without inbox none of this has any value." Built /ae/inbox, the SDR inbox one level up the ontology (deals, not…

"the AE also needs to have an inbox similar to what we built for SDR — without inbox none of this has any value." Built `/ae/inbox`, the SDR inbox one level up the ontology (deals, not leads). Same two-pane, mail-client shape: left = the AE's deals split into "In conversation" (open deals whose buying committee — primary contact + contact-role members — has a live email thread in the Graph mirror, ranked by most-recent message across the whole pipeline) and "Open · quiet" (open deals with no live thread — the ones going dark, paginated); right = the deal header (stage chip, ₹, account, contact, Deal hub ↗) + the unified thread (emails + meetings + calls + notes + tasks via getDealTimeline) + a Copilot-draft compose box (preview — nothing sends). New data layer getDealInbox (verified 162ms / 12 real conversations for Anisha) and draftReplyForDeal (Haiku, grounded in the deal's real thread). Inbox added as the AE's #2 nav item. Note: the Copilot draft currently 401s — the ANTHROPIC_API_KEY in .env is expired (wiring is correct; SDR copilot fails identically) — a key rotation, not a code fix.

HTML sanitization — the thread was leaking raw markup (<div style="display:flex…">…) from Zoho notes pasted out of Proshort and Outlook body fragments, since we render detail as text. New htmlToText helper (src/lib/format/text.ts: strips tags, keeps block breaks as newlines, decodes the common entities incl. emoji &#…;) wired into `getDealTimeline`, `getLeadTimeline`, and the peek pane (email body + note content). Deal hub, both inboxes, and every peek now render clean prose. Also promoted the deal description to a clay-accented "What they want" brief at the top of the Deal hub Details card (was a muted grey footnote under the field grid) — narrative leads, facts follow.

Perf note (not a bug): /ae/pipeline?peek=… logged 3.5s in dev. Traced it — getPipeline 29ms + peekDeal 24ms = ~60ms of real work; the rest is Next.js dev-mode render overhead (Turbopack + React dev renderer, amplified by concurrent session load). Production build serves that route in ~100ms. No code change; the peek is a full server round-trip by design (URL-driven master-detail, not a client Sheet).

Product

#14 (start): StubButton — honest "SOON" states, no fake buttons

antino3's rule: no button that toasts a lie. Built src/components/stub-button.tsx — a visibly-disabled control with a "SOON" pill + a tooltip saying what it WILL do and why it's off (an…

antino3's rule: no button that toasts a lie. Built src/components/stub-button.tsx — a visibly-disabled control with a "SOON" pill + a tooltip saying what it WILL do and why it's off (an unbuilt agent or an ungranted write scope). First applied to the deal-rail Draft follow-up button (that's the blocked Writer agent — it was faking a "Follow-up drafted" toast); now it reads honestly with the reason (needs a valid ANTHROPIC_API_KEY). More fake buttons to convert as the audit surfaces them.

Data

#17: Proshort tables in the Drizzle schema (db:push no longer drops them)

src/db/proshort-schema.ts already mirrored the 5 Proshort tables — but it was never registered in drizzle.config.ts, so drizzle-kit didn't read it and db:push still generated DROP…

src/db/proshort-schema.ts already mirrored the 5 Proshort tables — but it was never registered in `drizzle.config.ts`, so drizzle-kit didn't read it and db:push still generated DROP statements for proshort_* (525 recordings + 22k insights = data loss). Fix: added it to the config's schema array, plus the one missing index def (proshort_parties_email_idx on lower(email)) so the schema matches the live DB exactly (columns, PKs, indexes all verified against information_schema). Did not run push to "prove" it — that's the hazard; the fix is that drizzle-kit now knows the tables. Updated the memory (never-drizzle-push → "cautious": the drop hazard is resolved, but push still applies all diffs at once, so prefer targeted ALTER TABLE) + DATA-REALITY.md.

Data

Breadcrumbs show entity names, not zoho PKs

The breadcrumb rendered the raw id for [id] routes (e.g. AE / Deals / 4350976000054900540) because AppBreadcrumbs is a client component with no way to resolve a name. Fixed with a tiny…

The breadcrumb rendered the raw id for [id] routes (e.g. AE / Deals / 4350976000054900540) because AppBreadcrumbs is a client component with no way to resolve a name. Fixed with a tiny context (src/components/breadcrumb-title.tsx): BreadcrumbTitleProvider wraps the shell content, and each detail page renders an invisible <BreadcrumbTitle name={entity.name} /> that registers its name via effect; AppBreadcrumbs reads it and swaps the final crumb's label. Wired into all 9 detail pages (deal, account, contact, teammate, email, recording, meeting-event, AE + SDR lead). Now it reads AE / Deals / Defect Detection on Steel Sheets.

Product

Meeting detail page (every meeting is clickable; the "unlinked" pulse now leads somewhere)

/ae/meetings flagged "unlinked" meetings (external attendee not matched to a lead) with a pulse that asked for an action — but only recorded meetings were clickable (to the recording…

/ae/meetings flagged "unlinked" meetings (external attendee not matched to a lead) with a pulse that asked for an action — but only recorded meetings were clickable (to the recording detail); the rest were dead ends with no detail page. Built /ae/meetings/event/[id] for calendar meetings (graph events): getMeetingEvent(graphId, mailbox) → title, time, online/location, organizer, attendees (response dots), notes, and a link to the recording detail if one lines up. Crucially, an External guests panel lists the non-Antino attendees each with a Find link (/ae/leads?q=<name>) — the actionable path the unlinked pulse promised. Wired the AE meetings chips so every meeting links: recorded → recording detail, else → this event page. Verified getMeetingEvent on "Expat Orbit<>Antino" (3 attendees).

Product

Process spine #8 complete: the SAL handoff seam (receive side)

Closed the loop on "the most important seam." An SDR qualifying a lead already logs SAL_HANDOFF_SENT (#9); built the receive side — /ae/handoffs, a team-wide SAL inbox. getPendingHandoffs…

Closed the loop on "the most important seam." An SDR qualifying a lead already logs SAL_HANDOFF_SENT (#9); built the receive side/ae/handoffs, a team-wide SAL inbox. getPendingHandoffs derives the queue straight from the audit spine: SAL_HANDOFF_SENT events with no later SAL_ACCEPTED/SAL_REJECTED for that lead, joined to the lead + the SDR who sent it. Each row → Accept (logs SAL_ACCEPTED, notifies the SDR "picked up") or Reject with a reason (logs SAL_REJECTED, warning-severity notify back). Both are RACI-safe AE server actions through recordAuditAndNotify (PREVIEW), and accepting/rejecting drops the lead out of the queue via the NOT EXISTS. Added a Handoffs nav item (inbox icon) + breadcrumb. Verified the full flow against the DB with a temp handoff event (queue populated → cleaned up). With the SLA engine + Command Board hygiene row (below), #8 is done — the process spine now has stage-gates, the lead status machine, the SLA engine, and the SDR→AE handoff seam end to end. (Optional Command Board polish remains: per-rep SLA columns, forecast categories.)

12

Jul 2026

Sunday

25 shipped

Design

The page philosophy: whole app re-skinned to the landing (ultracode)

The app didn't read like the marketing landing — garish chart colors, flat surfaces, KPI-tile scoreboards, no motion, espresso buttons. Codified a page philosophy and rolled it across every…

The app didn't read like the marketing landing — garish chart colors, flat surfaces, KPI-tile scoreboards, no motion, espresso buttons. Codified a page philosophy and rolled it across every app page via ultracode workflows.

The philosophy (now the app-wide rule): a page header is a mono eyebrow (clay dot) → a big font-light title → one full-width briefing sentence weaving the key numbers as prose (clay for the key metric) — not a grid of KPI stat-tiles (DESIGN principle 10, "briefing > scoreboard"). Attention is a Pulse only on what's wrong/slipping — never an alarm-number board. Cards lift, never border; sections carry mono eyebrows; numbers are font-mono tabular-nums; entrance/hover motion is reduced-motion-safe (animate-in, never the buggy Reveal wrapper).

Color: the 8-hue entity palette was bright/saturated/cool (#008300 green, #2a78d6 blue, #e34948 red…), clashing on warm cream. Retuned all three sources — the --color-entity-* CSS vars, the JS base/text hexes in entity-colors.ts, and the ENTITY_RAMPS pie ramps in charts.tsx — to muted, cohesive Daylight hues (deal=clay, lead=sage, account=slate-blue, task=amber + muted violet/teal/rose/rust). Buttons: the Button default variant is now the landing's clay pill, so auth + app primary CTAs match the site.

Rollout (workflows): the command page as the exemplar (briefing header + pulses + warm charts + a real horizontal-stacked-bar RepScoreboard) → batch 1 (10 home/admin pages) → batch 2/3 (17 SDR+AE pages) → settings (5 role pages). Every batch preserved functionality — verified in-browser as Anisha (AE): the AE Desk, Pipeline (deal peek still opens), the Deal hub (peek timeline + stage-gate + action rail all intact, "Advance stage" now clay), and settings (change-password form intact). ~40 pages, TSC clean throughout. Note: keep concurrent workflow count modest — running several at once alongside the sibling session starved the dev server once.

Design

Landing rebuilt as the founding-story arc (Company Brain), 10/10 polish

The landing sold a narrow slice ("agentic sales OS"). Grounded in the two source docs the user supplied — antino-supernova-deck.pdf (the founding story: services→outcomes, the 11:47 PM…

The landing sold a narrow slice ("agentic sales OS"). Grounded in the two source docs the user supplied — antino-supernova-deck.pdf (the founding story: services→outcomes, the 11:47 PM resignation, four fault lines, the harness, the Company Brain, "priced like bread not oven-hours") and artifacts-library.html (the real product: a contract-to-production platform, Stages 0→6, maker→gate→checker, artifacts pinned to real standards) — rebuilt / as the full narrative arc: Hero → 11:47 email → four fault lines → the harness (translation chain) → the Company Brain reveal → the living pipeline (0→6 + HITL evidence gate) → the Rosetta-Stone moat → proof (85%+, ontology beats model) → new-world finale + CTA → Apple-style FAQ. Retired the sales-only sections; retitled "Sales OS" → "the Company Brain". Built + polished via two ultracode workflows (5-section build, then a 9-agent 10/10 polish pass against a shared design rubric). Hard lesson: the Reveal scroll-wrapper left sections invisible — all landing sections are now plain server components. Verified end-to-end: zero inter-section gaps (measured via DOM), TSC clean, every section renders. New ember starburst brand mark + favicon (clay→amber on espresso, the deck's collapse→burst motif) replaces the violet nova. Design language unified — app now shares the landing's warm Daylight tokens (.app-theme = :root).

Design

One design language: app adopts the landing's Daylight tokens

The logged-in app and the marketing site had diverged — landing warm Daylight (cream #edeae0, espresso ink, clay accent), app cool shadcn-zinc via the .app-theme scope. User: "why is the…

The logged-in app and the marketing site had diverged — landing warm Daylight (cream #edeae0, espresso ink, clay accent), app cool shadcn-zinc via the .app-theme scope. User: "why is the design language of app pages and landing page different — merge this; I want the landing's design language in all the pages." So .app-theme in globals.css now mirrors the :root Daylight layer: cream page, warm #fbfaf4 cards, espresso ink, clay `#c2613f` on primary/ring/links. One softening — --accent (dense shadcn hover surface) stays a warm neutral #efece2, not clay, so dropdown/ghost hovers don't flare orange. .app-canvas warmed (espresso dots + cream wash). Landing look unchanged (merge, not restyle). Verified on /design: raw tokens render the full warm Daylight palette + clay. Prior "app = shadcn zinc" decision reversed (memory + design-page copy updated); DESIGN.md "Daylight" now governs the app too.

Performance

Performance: /ae/meetings was 24.8s → sub-second (query + indexes)

/ae/meetings took 24.8s in application code. Timed the DAL: getMeetingSignals's "unlinked" query was 8.6s — its not exists correlated-joined all 19k zoho_leads per attendee per meeting with…

/ae/meetings took 24.8s in application code. Timed the DAL: getMeetingSignals's "unlinked" query was 8.6s — its not exists correlated-joined all 19k `zoho_leads` per attendee per meeting with an OR domain-match (split_part(email,'@',2)), non-sargable, re-scanning the whole leads table repeatedly. Rewrote it to materialize the lead-email + lead-domain sets once as hashable CTEs, then check membership: 8,588ms → 240ms (~35×), identical 44 rows. Then the root cause app-wide: no functional indexes existed on the email/mailbox JOIN keys, but "email is the universal join key" and every join uses lower(...), so the PK/FK indexes were unusable — seq scans across recipients (753k), messages (148k), contacts (140k), attendees (44k). Added 7 functional indexes (lower(email) / lower(mailbox_email) / lower(from_email) on graph_events/attendees/messages/recipients + zoho_leads/contacts) via CREATE INDEX CONCURRENTLY + ANALYZE. getMeetings events query: 408ms → 172ms. Net: the meetings page drops from ~9s of DB time to <1s. Indexes are version-controlled in scripts/perf/add-indexes.mjs (idempotent; not via db:push).

Then the app-wide audit (task #19, a subagent that timed every DAL query against the live DB): one genuine outlier remained — `getTasks` (SDR follow-ups) at 2.6s, a per-task left join lateral scanning all 3,298 of the owner's leads filtering on unindexed lower(company). Added a composite functional index zoho_leads (owner, lower(company))2,655ms → 196ms warm (also speeds getLeadTasks + the lead-timeline task block). Added the cheap graph_messages (lower(split_part(from_email,'@',2))) for the rare lead-timeline domain fallback. The audit confirmed everything else (AE, metrics, peek, SLA, attachment DALs) is already FK/PK-indexed and sits at the ~150ms remote-DB network floor — no more indexes needed. 9 perf indexes total, all in scripts/perf/add-indexes.mjs.

Product

Process spine #8: the SLA engine + Command Board hygiene row

Started #8 (finish the process spine). Built the SLA / hygiene engine (src/lib/sla/queries.ts, getSlaBoard) — the manager's early-warning signals computed live from the mirror: untouched…

Started #8 (finish the process spine). Built the SLA / hygiene engine (src/lib/sla/queries.ts, getSlaBoard) — the manager's early-warning signals computed live from the mirror: untouched leads (open/new with no activity ever — a speed-to-lead breach; the data has 1,606, oldest 123 days), no-next-step deals (40), stale deals (30d+ inactive, 19), overdue tasks (19,193), and the SAL handoff funnel from the audit spine (SAL_HANDOFF_SENTSAL_ACCEPTED/REJECTED, with an acceptance rate). Surfaced as an "SLA & hygiene" row at the top of the manager Command Board (/admin/command), tone-colored (rose = breach, amber = attention). Verified the counts directly against the DB. Remaining #8: the AE-side SAL accept/reject queue (an SDR qualifying already logs the handoff; the receive side is next) and the per-rep SLA scoreboard columns.

Product

Deal hub: surface all the data we hold (#18)

The deal hub showed ~8 fields; the mirror holds far more. Enriched getDeal + DealFull to fetch every meaningful column and surfaced them on the hub: a prominent Next step callout (the field…

The deal hub showed ~8 fields; the mirror holds far more. Enriched getDeal + DealFull to fetch every meaningful column and surfaced them on the hub: a prominent Next step callout (the field we just added to the mirror), Expected revenue, Pipeline, Lead source, Investment type, Domain, Tech lead, Company location, Sales cycle (shown only when populated — honest empty states, since per DATA-REALITY.md many custom fields are sparse), and the Estimation sheet as a real link. Also added contactId so the primary contact can link to its hub. Contact hub enriched next: getContact/ContactFull now fetch + surface a Details card — department, reports-to (linked to that contact's hub), source, sourced-by, company type, company industry, projected 1-year revenue, mobile — all shown only when populated. Deal + contact done; account/lead/teammate follow in the #18 sweep. (Next_Step/Expected_Revenue are empty until the next admin-gated Zoho sync populates them from the new manifest fields.)

Agents

Audit + notify spine v2: actor-kind (agents!), outcome, severity, the wrapper

Diagnosed the "fucked up" audit/notify: the code was well-built but the spine couldn't log agents (it required a User actor; null just meant "system"), had no outcome/severity/summary, and…

Diagnosed the "fucked up" audit/notify: the code was well-built but the spine couldn't log agents (it required a User actor; null just meant "system"), had no outcome/severity/summary, and covered only the identity/admin half — every sales + agent action wrote nothing. Rebuilt the foundation:

  • Schema (audit_events + notifications, via ALTER TABLE, never db:push): actor_kind (USER|AGENT|SYSTEM), actor_label (person or agent name), outcome (SUCCESS|FAILURE|PARTIAL|PREVIEW — for propose-not-act), summary (a rendered human sentence — what the trail shows), agent_run_id; notifications gain severity (info|warning|critical) + cta_label/cta_href.
  • `recordAudit` now takes actorKind/label/outcome/summary/agentRunId and is fire-and-forget (swallows errors, returns number | null) so a logging failure never breaks the action. notify gains severity + CTA and is fire-and-forget too.
  • `recordAuditAndNotify()` — THE spine wrapper (antino3's SyncAudit pattern): one call = one audit row + notifications with a shared vocabulary (outcome→severity). `recordAgentRun()` — logs an AGENT action attributed to the user it ran for.
  • Wired the Writer agent: every draft (success/empty/error) now writes an AGENT_DRAFTED_REPLY audit row (actorKind AGENT, label "Writer", outcome, char-count). Registry (audit-actions.ts) gained the sales + agent action codes + ActorKind/Outcome/Severity enums. Verified end-to-end (agent event + severity/CTA notification insert). Then made it visible + gated: /admin/audit now surfaces the new detail — an actor-kind glyph (agent = violet sparkle, system = cpu, user = person), a colored outcome chip (shown when not SUCCESS), and the summary sentence on its own line. Built the RACI access helper (src/lib/auth/access.ts): canActOnOwner/requireActOnOwner gate write-actions by admin-tier · ownership (Zoho mapping) · manager hierarchy (recursive report tree) — unauthorized → notFound(); reading stays open. Then wired the deal action rail to log every user action: src/app/ae/deals/[id]/actions.ts (logStageAdvance / logDealNote / logCall) — each requireRole('AE') → looks up the deal owner → requireActOnOwner (404 if not owner/manager/admin) → recordAuditAndNotify({ outcome: 'PREVIEW', … }) (audit row + a manager notification with a CTA). The deal-hub Stage-gate Advance button + the Advance / Log call / Add note rail buttons now call these server actions (client toast reflects the logged result) instead of a bare client toast. Typecheck-verified; the spine itself was DB-smoke-tested. Live click-through pending a fresh sign-in (I can't authenticate). Then wired the SDR lead action rail the same way (src/app/sdr/leads/[id]/actions.ts): Send email / Log call / Book meeting / Qualify / Add note now call RACI-gated server actions logging PREVIEW events + notifying the SDR's manager. Qualifying fires the SAL handoff (SAL_HANDOFF_SENT) — the seam antino3 calls the most important — and Unqualified logs LEAD_DISQUALIFIED with the reason code. #9 complete: every user action (deal + lead) and every agent run is now logged in detail, RACI-gated, with severity/CTA notifications. (SAL accept/reject on the AE side + the SLA engine belong to #8.)
Data

Zoho coverage: audit + field fill (Next_Step, Mobile, Reports_To…)

Audited whether the Zoho mirror pulls everything (a subagent, task #15). Finding: coverage is selective by construction — src/lib/zoho/manifest.json is a hand-picked field list, no raw…

Audited whether the Zoho mirror pulls everything (a subagent, task #15). Finding: coverage is selective by constructionsrc/lib/zoho/manifest.json is a hand-picked field list, no raw overflow (deliberate "fully-typed 1:1 mirror"). The forecasting spine is dense (probability/forecast/amount/close/stage all ~100%), but several workflow fields are structurally absent. Filled the real gaps (task #16): added zoho_deals.next_step + expected_revenue, zoho_contacts.reports_to + department, zoho_leads.mobile to the schema + manifest; the self-healing importer populates them on the next sync. Discovered a live hazard: drizzle-kit push wants to DROP the Proshort tables (525 recordings + 22k insights — they live outside the Drizzle schema, created by scripts/proshort/). The no-TTY guard blocked it; applied the columns via surgical ALTER TABLE ... ADD COLUMN IF NOT EXISTS instead. Recorded the never-push rule (memory + docs/DATA-REALITY.md, ported from antino3 with live 2026-07-12 fill rates — most account attributes are <2% filled, converted_s is 0%). Remaining #16: mirror CRM Attachments + deal line items (bigger builds); the raw jsonb idea is deferred — it contradicts the deliberate no-jsonb schema convention (flagged for a decision). Follow-up task #17: add Proshort tables to the Drizzle schema so push is safe.

Product

Process spine v1: deal stage-gates (from the antino3 study)

Studied the predecessor project C:\Users\vaibh\projects\antino3 (same product, Java stack, further along on process/agents/ontology) with four parallel researchers and turned it into a gap…

Studied the predecessor project C:\Users\vaibh\projects\antino3 (same product, Java stack, further along on process/agents/ontology) with four parallel researchers and turned it into a gap program (session tasks #7-14). First fill: the deal process spine. antino3's lesson — "we built artifacts, not process." Zoho has a flat 3-stage picklist; src/lib/process/deal-stages.ts overlays the enterprise funnel S1 Qualify → S2 Solution & Validation → S3 Proposal → S4 Negotiate and inserts the stage the data is missing — S2, the Tech Lead / pre-sales home. Each stage has exit criteria evaluated against the deal's real signals (has contact, next step, meetings, emails, amount, close date, probability); a deal can't credibly advance until its gate is met. StageGate (src/components/ae/stage-gate.tsx) renders the funnel (done/current/future, S2 marked app-native with a sparkle) + the criteria checklist + an Advance button that's enabled only when the gate passes (preview toast until write-back). Wired into the deal hub above MEDDPICC. Verified on "Defect Detection on Steel Sheets" (Proposal): 3/3 met, Qualify+S2 passed, "Advance to Negotiate" live.

Lead status machine (src/lib/process/lead-stages.ts + LeadStatusGate): the symmetric piece for leads. Normalizes Zoho's free-text lead_status mess (Open/New/Working/Qualified-Hot·Warm·Cool/Nurturing/Recycled/Lost·Junk·Not-Qualified/Disqualified) into clean phases — a forward funnel New → Working → Qualified plus off-ramps (Nurturing/Recycled/Lost). Exit criteria per forward step (first touch, discovery, temperature, next step) computed from the lead's timeline signals; advance gated + preview. Wired into the AE lead hub. Verified on Pramod Sao (Working, 2/3 — discovery + interest met, next step missing → locked). Hygiene (open deal, no next step = red) already lives on the pipeline/desk. Process spine core done (task #7).

Product

Detail pane (peek) — v3: connect it across the list surfaces

"Connect it everywhere." Turned the main list pages into master-detail: clicking an entity opens its peek pane beside the list (?peek=<kind>:<id>, scroll-preserving) instead of navigating,…

"Connect it everywhere." Turned the main list pages into master-detail: clicking an entity opens its peek pane beside the list (?peek=<kind>:<id>, scroll-preserving) instead of navigating, with the ↗ affordance on every openable name/card and a selected-row/card highlight; the pane's own ↗ still opens the full page, and sort/filter/paginate drop ?peek so they close the pane. Done on: AE leads (lead pane), pipeline board (deal pane — cards reflow to 1fr + 360px, emerald ring on the selected card), accounts (account pane, portfolio stats), SDR leads People view (lead pane). Each reuses the shared PeekPane + getPeek data layer, so a page is ~15 lines: parse ?peek, a peekHref/closeHref, wrap the list in a grid, and point row links at the peek. Verified as Himanshu: AE leads → Anurag Rudra pane; pipeline → AI Use Cases deal pane; accounts → Ministry of Cooperation portfolio pane — all in-place, no navigation. Remaining surfaces (SDR tasks chains, inbox, meetings calendar, Desk lists, hover-card "Open") are follow-ups — the framework makes each a quick add. Tracked in the session task list.

Product

Detail pane (peek) — v2: lead timelines (SDR + AE)

Extended the peek pane to the lead 360s. Every getLeadTimeline item now carries a ref (note/task/call/email/meeting → their stable ids; Proshort → recording:…). Tasks use the…

Extended the peek pane to the lead 360s. Every getLeadTimeline item now carries a ref (note/task/call/email/meeting → their stable ids; Proshort → recording:…). Tasks use the subject-matched follow-up rows (since task who_id never points at leads); meetings/emails use the graph id; recordings the document id. Both /sdr/leads/[id] and /ae/leads/[id] render the PeekPane in the right column when ?peek is present (SDR: replaces the action rail; a read-only lead now gets a 2-col layout so the pane has a home; AE: replaces the follow-ups rail), with the ↗ affordance + selected-row highlight on clickable rows. Verified as Himanshu on Ishika's Collinson lead (read-only): clicking "Followup 1 with Lead Saurabh from Collinson" opened the task pane (Not Started · Normal · Due 13-Jul · Owner Ishika) in-place, no navigation.

Product

Universal detail pane (peek) — v1 on the deal timeline

The goal: never navigate away to see an element's detail. Built a reusable, URL-driven detail pane (a real master-detail split — not a Sheet/overlay): ?peek=<kind>:<id> on a detail page…

The goal: never navigate away to see an element's detail. Built a reusable, URL-driven detail pane (a real master-detail split — not a Sheet/overlay): ?peek=<kind>:<id> on a detail page swaps the right column from the rail (Actions/MEDDPICC) to a pane rendering that element, deep-linkable and server-rendered.

  • Data layer src/lib/peek/queries.ts — one fetcher per ontological element: recording (reuses the Proshort intelligence: recap, signals, insights, participants, CRM links, watch URL), lead, deal, contact (+ its deals), account (+ portfolio), email (full body + recipients), meeting (attendees, external/Antino split), task, note, call. getPeek("<kind>:<id>") parses + dispatches.
  • Component src/components/peek/peek-pane.tsx — an async server component; header with entity icon + kind + "open full page" ↗ + close X, then a per-kind body. Sticky, scrolls internally.
  • Timeline refs — every getDealTimeline item now carries ref ("recording:…", "email:…", "meeting:…", "task:…", "note:…", "call:…"). Rows with a ref become peek links with a ↗ affordance and a selected-row highlight.
  • Deal page wired: ?peek renders the pane in place of the rail; close returns. Verified as Himanshu on the Hitachi deal: clicked the "Hitachi <> Antino : CV Solution" recording → pane shows recap + signals + Watch; clicked the calendar meeting → pane swaps in-place to attendees (external/Antino) + Open-in-calendar. No navigation, content swaps live.
  • Next: lead timelines (getLeadTimeline refs + SDR/AE lead pages), then connect peek everywhere entities appear (hover cards' "Open" → peek, list rows) with the ↗ affordance. Tracked in the session task list.
Product

Email reader + attachments, and Inter as the app font

Email reader (/ae/emails/[id]) — the last ontological object. The peek pane already previews an email's stripped-plain body; this is the full read: src/lib/graph/attachments.ts…

Email reader (/ae/emails/[id]) — the last ontological object. The peek pane already previews an email's stripped-plain body; this is the full read: src/lib/graph/attachments.ts (getEmailFull → subject, from, to/cc, the real body_content + type, and attachments) renders the HTML body in a sandbox="" iframe (no scripts/forms/same-origin) or plain text, plus an attachments grid. Attachments download through GET /api/graph/attachment?k=<token> — auth-gated (getCurrentUser), streaming the graph_message_attachments.content bytea, addressed by an opaque base64url token (encodeAttKey) so no mailbox address or raw id sits readable in the URL. peekFullHref now maps email → the reader (AE). Verified on the "GITEX 2024" email: full HTML body + 39 real attachments (names, sizes, download icons). Inline attachments filtered out; cloud/reference attachments shown non-downloadable.

Font: switched the app sans from IBM Plex Sans to Inter (next/font/google, variable font) — --font-sans + --font-heading now point at --font-inter. IBM Plex Mono stays (the "supernova" wordmark + tabular numbers). Verified across the account hub.

Product

Teammate hub (the user as a first-class object)

Built /ae/team/[id] — a rep 360, completing the ontological page set (Lead, Deal, Meeting, Account, Contact, User all now have full pages). getTeammate (identity from users: role,…

Built /ae/team/[id] — a rep 360, completing the ontological page set (Lead, Deal, Meeting, Account, Contact, User all now have full pages). getTeammate (identity from users: role, designation, department, reports-to) + the rep's sales snapshot by reusing getAeDesk/getPipeline on their Zoho id (getAeZohoId(userId)), so it works for any mapped rep — SDR or AE. Shows open pipeline / won FY / open leads / overdue + their open deals (shared OpenDealRow). Graceful empty state when a user has no CRM mapping. Linked from the Team scoreboard (rep names). Verified on Shambhavi Srivastava (SDR): identity + ₹10.7 Cr pipeline, 8 open deals, 99 overdue. (Loosened the shared deal-row prop types to structural subsets so both DealRow and AccountDeal render through them.)

Product

Contact hub (the person as a first-class object)

Built /ae/contacts/[id] — the contact 360 the app was missing (contacts existed only as timeline names and peek previews). getContact + getContactDeals (deals where zoho_deals.contact_name…

Built /ae/contacts/[id] — the contact 360 the app was missing (contacts existed only as timeline names and peek previews). getContact + getContactDeals (deals where zoho_deals.contact_name = the contact, plus zoho_deal_contact_roles). Shows identity (title, account link, city), the deals the person is on (open + full Won/Lost history), a reach panel (email/phone + open/won stats), and notes. Extracted the deal-row rendering into a shared src/components/ae/deal-rows.tsx used by both the account and contact hubs. Linked in: account-hub contacts now click through, and peekFullHref maps contact → the page (and account → the new hub, fixing its old link to the list). Verified on Swapnil Randive (Hitachi India): 6-deal history (1 Won ₹6.5L, 5 Lost) + reach + notes.

Product

Rebrand to Supernova + Account hub (the company as a first-class object)

Rebrand: the product is now Supernova (Antino is the company that runs it). New brand mark — an 8-ray violet nova burst (#7C5CFF core, gradient rays, glow) in src/components/logo.tsx +…

Rebrand: the product is now Supernova (Antino is the company that runs it). New brand mark — an 8-ray violet nova burst (#7C5CFF core, gradient rays, glow) in src/components/logo.tsx + src/app/icon.svg; wordmark "supernova". Renamed all product branding (28 page title: strings across SDR/AE/Admin/auth, root layout "Supernova — Sales OS", marketing wordmarks, the design-system page). Deliberately kept the company references — meeting-participant affiliation ("External vs Antino"), the pilot copy ("Antino's own 15 reps"), @antino.com domains — since those name the company, not the product (renaming them would corrupt what the data means).

Account hub (/ae/accounts/[id]) — the missing ontological object. Two user questions ("why isn't the company clickable", "why can't I see past deals") were the same gap: no account 360. Built it: getAccount/getAccountDeals/getAccountContacts (deals keyed by zoho_deals.account_name = the account FK, despite the column name). Shows header + stats, open deals and full deal history (Closed Won/Lost — 458 won + 1,052 lost org-wide were previously invisible everywhere), and the account's contacts. Wired the link in across the app: accounts list rows, deal-hub account name (via accountId), recording linked-account, and the /ae/leads company column (→ ?q= search, since a lead's company is free text). Verified on MOD-MISO (2 open + 2 past Won deals render).

Product

AE Meetings: Day/Week/Month switcher + click-through to recording detail

/ae/meetings was month-only, and its meeting chips were hover-only (no way in). Brought it up to the SDR calendar's level: a Day / Week / Month view switcher driven by ?view=&date= (IST,…

/ae/meetings was month-only, and its meeting chips were hover-only (no way in). Brought it up to the SDR calendar's level: a Day / Week / Month view switcher driven by ?view=&date= (IST, Monday-start weeks, prev/next + Today), the same getMeetings + getMeetingSignals data, rendered through the shared MeetingItem (hover card + recording/unlinked pulses). MeetingItem already grew an href prop (parallel session), so recorded meetings now link straight through to their call intelligence instead of nesting an anchor in a button. Built /ae/meetings/[id] as the AE-side recording detail — read-only, reusing getRecording/getRecordingParties/getRecordingInsights/getRecordingLinks: recap, budget/timing/decision scalars, call intelligence grouped by kind, a Linked in CRM panel (deal → /ae/deals, lead → /ae/leads, read-only) and participants split external vs Antino. No connect/write flow — that stays on the SDR side. Verified as Himanshu across all three views (June 2026) and clicked through the "Discovery Call: Goldenflitch & FabHotels" recording (recap + Sahil Malhan linked lead + participants).

Data

Lead timeline: Zoho tasks (who_id is a dead link for leads)

The lead timeline queried tasks by who_id=leadId — but a DB check shows Zoho task who_id points at contacts (539,625) and never leads (0), so that block returned nothing on every lead.…

The lead timeline queried tasks by who_id=leadId — but a DB check shows Zoho task who_id points at contacts (539,625) and never leads (0), so that block returned nothing on every lead. Leads are linked to their follow-ups by the subject convention instead ("Followup N with Lead {FirstName} from {Company}"), the same match getLeadTasks uses. Rewired getLeadTimeline's task block to match by the lead's owner + company + first name across all statuses (so the timeline shows the full done+pending chain, not just open). Added ownerId to LeadFull/getLead to scope it to the lead's actual owner (works for read-only cross-rep views too). Verified as Himanshu on Ishika's Collinson lead (read-only): "Followup 1/2 with Lead Saurabh from Collinson" now render in the timeline, while the owner-scoped "Open follow-ups" rail stays empty (correctly — they're not his to action). Both SDR and AE lead pages inherit this.

Data

Deal timeline: real-calendar meetings (the missing category)

The deal 360 was missing meetings. Grounded it in the data: for Himanshu's Tata Steel deal, zoho_events (CRM events, what_id) = 0, but the real Outlook calendar (graph_events) has meetings…

The deal 360 was missing meetings. Grounded it in the data: for Himanshu's Tata Steel deal, zoho_events (CRM events, what_id) = 0, but the real Outlook calendar (graph_events) has meetings with the buying committee — the deal timeline only queried zoho_events, so they never showed. (Org-wide only 197 CRM events link via what_id at all; the real meeting record lives in graph_events.) Added a graph_events block to getDealTimeline, matched to the same buying-committee email set the email block uses (primary contact + contact-roles), deduped across mailboxes. Now the deal timeline is genuinely unified — CRM tasks/notes/calls + committee emails + real-calendar meetings + Proshort calls. Verified as Himanshu on Tata Steel: "TATA Steel <> Antino" meetings render (teal calendar icon, "· with the buying committee") interleaved with the MOM email, follow-up tasks, and the Proshort discovery call. Subtitle updated to "CRM + email + meetings + calls, unified". (Lead timeline already carried all three — tasks via who_id, meetings via graph_events, emails exact/domain.)

Product

AE lead surface (their book, read-only 360)

AEs are full-cycle — they own leads too — but the AE nav gave them no way in. Added the lead surface, reusing the SDR lead data layer (getLeadBook / getLead / getLeadTimeline / getLeadTasks…

AEs are full-cycle — they own leads too — but the AE nav gave them no way in. Added the lead surface, reusing the SDR lead data layer (getLeadBook / getLead / getLeadTimeline / getLeadTasks are keyed by a Zoho owner id, so they're role-agnostic):

  • `/ae/leads` — the AE's own lead book: data table with search + status filter + sortable columns + server-side pagination (25/page), mirroring the SDR People view. The company lens already lives on /ae/accounts, so this stays focused on people.
  • `/ae/leads/[id]` — a read-only lead 360: Details, the lead's open follow-ups (read-only), and the unified CRM + email + meetings timeline. This completes the user's "email of a lead/deal in the detail page" ask on the AE side (the deal 360 already interleaves the buying committee's Graph emails via getDealTimeline).
  • Nav: Leads added to the AE role (after Pipeline). Verified as Himanshu: leads table renders his book with the status tabs + sortable columns; the Atul Sharma hub shows Details + a 7-email unified timeline (real HBS India thread, IST timestamps).
Product

AE universe v1: the deal layer (Pipeline board + Deal hub)

Started the AE role, grounded in a deep DB read first. Key findings: AEs are full-cycle — they own 13,440 open leads (more than SDRs' 3,896) and the deal pipeline. The active pipeline is…

Started the AE role, grounded in a deep DB read first. Key findings: AEs are full-cycle — they own 13,440 open leads (more than SDRs' 3,896) and the deal pipeline. The active pipeline is small (~151 open deals org-wide) in 3 real stages (Qualification → Proposal/Price Quote → Negotiation/Review) — the process-doc's Discovery/Solution stages don't exist. 115/151 open deals are past-due (₹42 Cr slipping) and 40 have no next step — the AE's "overdue chains." Deals are single-threaded (1 contact, roles null → MEDDPICC empty), but their timelines are rich (7,367 tasks + 878 notes via what_id). 246/525 Proshort recordings link to the CRM — the AE's call-intelligence unlock. Himanshu is the closer (38% win, ₹19.7 Cr); deals never come from leads (0 conversions); all INR.

So the AE universe = the SDR workspace (reusable — AEs own leads) + a deal layer. Built the deal layer read-only:

  • `src/lib/ae/queries.ts` — deal DAL scoped to the AE owner id: getPipeline (grouped by open stage), getAeDesk (blended counts: open pipeline, slipping, no-next-step, won FY, open leads, overdue tasks), getDeal, getDealTimeline (tasks/notes/calls/meetings via what_id). src/lib/ae/stage.ts for stage styling.
  • `/ae/pipeline` — Kanban board: 3 stage columns, deal cards with ₹/account/close, past-due + no-next-step flags.
  • `/ae/deals/[id]` — deal hub: Details + rich timeline + an app-native MEDDPICC scorecard (the data is empty; the Qualifier agent fills it from calls) + a buying-committee multi-threading prompt; read-only "owned by X" when not yours.
  • Nav: added Pipeline to the AE role. Verified as Himanshu (AE): board shows 20 open deals / ₹19.7 Cr incl. the ₹10 Cr Ministry of Cooperation deal, all past-due; deal hub renders real Details + timeline + MEDDPICC.
Product

Recording detail + connect records, link resolver, the Pulse system

Meeting/recording detail page (/sdr/meetings/[id]) — full Proshort intelligence for a call (recap, budget/timing/contract/decision-process, and the call-intelligence lists: pain points,…

  • Meeting/recording detail page (/sdr/meetings/[id]) — full Proshort intelligence for a call (recap, budget/timing/contract/decision-process, and the call-intelligence lists: pain points, action items, risks, competitors, questions…), participants split internal/external, Watch-recording link. Query layer in src/lib/proshort/queries.ts.
  • Connect records — the meeting↔lead/deal linking UI, all cases: auto-linked (with method + confidence), suggested candidates, manual search of leads/deals, and orphan → create-lead. Link/unlink are real writes to proshort_recording_links (src/lib/proshort/actions.ts); create-lead is preview (Zoho write-back). Link table migrated to a composite PK (multi-link) + source (auto|manual).
  • Layered link resolver (scripts/proshort/resolve-links.mjs) — best-confidence-wins matcher (exact email→lead/contact, name+company, company domain, account website, fuzzy company name): 152/191 external calls auto-linked. The residual 40 are honest gaps — 28 personal-email individuals/candidates (no company) and ~15 real companies genuinely absent from Zoho; matching can't place them, only create-lead can. Verified with a why-unmatched inspection.
  • The Pulse system (src/components/pulse.tsx) — the app's universal "needs attention" dot (animated ping, tone-colored new/attention/warn/ok), each with a hover tooltip that explains its meaning (self-teaching). First wired on the calendar: a blue recording-available + amber not-linked-to-a-lead pulse per meeting, scoped to the user's own meetings (getMeetingSignals). To spread across nav counts, going-cold leads, no-next-action, orphan calls.
Data

Follow-up chains, unified calls (Proshort), user-scoped timeline

Follow-ups grouped into chains. DB study confirmed the shape: 763 leads carry a "Followup 1 → Followup 2" chain (1636/1639 tasks resolve to a lead). So /sdr/tasks now groups by lead — one…

  • Follow-ups grouped into chains. DB study confirmed the shape: 763 leads carry a "Followup 1 → Followup 2" chain (1636/1639 tasks resolve to a lead). So /sdr/tasks now groups by lead — one card per lead with its chain and a Clear chain action — instead of a flat list. Ordered by lead recency (getTasks now returns leadRecency, cap lifted 300→2000 so the full 1,639 shows across 875 leads).
  • Proshort integration (conversation intelligence) — the call layer. New proshort_* staging tables (scripts/proshort/create-tables.mjs, typed in src/db/proshort-schema.ts) + backfill (scripts/proshort/backfill.mjs, v3 list→detail, throttled). Recorded calls (recap + overview: pain points, action items, risks, competitors, decision process) now surface as call items in the lead timeline. See docs/PROSHORT.md. Key in .env (rotate it).
  • User-scoped timeline (privacy). getLeadTimeline(lead, userEmail) now scopes meetings to the signed-in SDR's own mailbox and Proshort calls to recordings they were a party on — a user sees only their meetings/calls, never everyone's. Emails + meetings + calls interleave by date; the Inbox/lead labels count each.
  • Threaded user.email through the timeline callers (inbox, lead detail) and the Writer draft path.
Agents

First AI: the "Writer" agent + task accordion + hover cards

First real Claude call in the app. @anthropic-ai/sdk installed; src/lib/ai/anthropic.ts builds the client from the encrypted ANTHROPIC_API_KEY setting (cached until the key changes);…

  • First real Claude call in the app. @anthropic-ai/sdk installed; src/lib/ai/anthropic.ts builds the client from the encrypted ANTHROPIC_API_KEY setting (cached until the key changes); src/lib/ai/draft.ts = Writer, which drafts a reply grounded in the lead's real email thread + Zoho record (Haiku 4.5, read-only). Server action draftReply(leadId) in src/app/sdr/ai-actions.ts; the Inbox "Copilot draft" button now calls it (loading state, toast, graceful error) instead of a template.
  • Verified the path is real: clicking it fires a genuine API call and returns a proper Anthropic response. The stored key is currently an invalid 24-char placeholder (sk-ant-tes…, updated 2026-07-07) so it 401s — a valid key in Admin → Settings unblocks it, no code change.
  • Agent framing: the SDR gets a team of simple-named agents — Writer (live), Briefer / Sorter / Qualifier / Researcher / Chaser (planned) — all Haiku, all propose-not-act, user in the center. Write-back (send via Graph, CRM update via Zoho) is a separate, deliberate switch.
  • Follow-ups → accordion. getTasks now resolves each task to its lead via a lateral join (subject parse, since who_id is null), so tasks carry a hoverable/linkable lead. New src/lib/sdr/task-types.ts classifies each task (follow-up / status / future / notes / data / linkedin — grounded in a scan of the real subjects, ~8k are "Priority Action" bot tasks); the row expands to the right "how to finish it" actions per type. All preview + toast.
  • Universal `LeadHoverCard` (src/components/sdr/lead-hover-card.tsx) — hover any lead name to preview contact/status in the lead entity hue + jump to the record; the pattern extends to deal/contact on the AE surface.
Product

SDR workspace: a day-in-the-life command center

Built the SDR role from a couple of dashboard stubs into a real workspace, all on live Zoho + Graph data (only write-back actions are preview). New DAL: src/lib/sdr/queries.ts (everything…

Built the SDR role from a couple of dashboard stubs into a real workspace, all on live Zoho + Graph data (only write-back actions are preview). New DAL: src/lib/sdr/queries.ts (everything scoped to the signed-in SDR's Zoho owner id) + src/lib/sdr/status.ts (lead status/temperature styling + SALES-PROCESS dispositions/reason codes).

  • My Day (`/sdr`) — replaced the stats dashboard with a command center: follow-ups due today (+ overdue), meetings today, leads never worked (+ new this week), active conversations. Each card links into the work. getMyDay().
  • Inbox (`/sdr/inbox`) — Outlook two-pane. Left: leads split into Conversations (whole-book, ranked by most-recent email via an exact/recipient match against graph_messages) + Not yet contacted. Right: the selected lead's unified communication thread + a reply/log/book compose box. getInbox(), getLeadTimeline() (stitches email mirror + Zoho notes/calls/events; exact email then company-domain fallback).
  • Leads (`/sdr/leads`) — merged the old Leads + Companies into one page with a People ⇄ By-company toggle (SDRs don't own zoho_accounts, so "company" is the lead book grouped by the company field — 831 companies). Dropped the separate Companies route/nav.
  • Lead hub (`/sdr/leads/[id]`) — the cross-entity view the model needed: a Next action strip + the lead's own open follow-up tasks, closeable inline with toasts. Tasks have who_id = null, so the task↔lead link is reconstructed from the subject ("…with Lead X from Company") — 1636/1639 matched. Right rail actions qualify via the process dispositions + reason codes.
  • Follow-ups (`/sdr/tasks`) — the task backlog as a triage list, bucketed Overdue/Today/Upcoming, each parsed into lead · company.
  • Meetings (`/sdr/meetings`) — real calendar with Day / Week / Month views (IST), color-coded by online/in-person/cancelled; a Radix hover card shows full details incl. attendees + external company (from attendee email domain). getMeetings() joins graph_events + graph_event_attendees.
  • Foundations: src/lib/format/datetime.ts — canonical 12-Mar-2029 16:30 IST everywhere (+ istDayKey/formatTimeIST for the calendar); global Toaster (sonner) mounted in the shell; sticky header.
  • Fixes along the way: pg SSL deprecation warning (explicit ssl config in db/index.ts, strip sslmode); command-board crash (formatCount(undefined) — realigned /admin/command to the redesigned getRepScoreboard schema + hardened formatCount); audit trail & integrations now show IST (were raw UTC).
  • Verified in Chromium signed in as an SDR (Ishika): My Day (87 due today / 1,372 overdue / 805 unworked), Inbox (Bhaskara's real Exide thread), Leads (878 / 831 companies), Follow-ups (300 shown), Meetings (Bechtel discovery call, week grid), Lead hub (Neha's next action + 2 tasks + 6 matched emails). Zero render errors.

11

Jul 2026

Saturday

5 shipped

Product

Command board: donut breakdowns + SDR/AE rep scoreboard

"Open pipeline by stage" and "Lead book by status" are now donuts (user request): new BreakdownPieChart in dashboard/charts.tsx — one entity hue per chart (deal-greens / lead-teals, darkest…

  • "Open pipeline by stage" and "Lead book by status" are now donuts (user request): new BreakdownPieChart in dashboard/charts.tsx — one entity hue per chart (deal-greens / lead-teals, darkest = largest slice, dataviz one-hue rule), slices sorted desc with tail folded into "Other", and the legend doubles as a data table (name + share % + formatted value) so identity never rides on color alone.
  • Rep scoreboard on /admin/command: every active SDR/AE with a Zoho mapping — open leads (unconverted, not Lost/Junk/Not Qualified), new leads 30d, tasks/calls/meetings 30d (getRepScoreboard() in metrics/queries.ts). Entity dots in the column headers.
  • getWeeklyActivity week bucket now returns a deterministic to_char(..., 'YYYY-MM-DD') string (belt-and-braces with the earlier Date coercion for the Intl RangeError).
  • Verified in Chromium signed in as admin: 4 chart SVGs render, zero page/asset errors; scoreboard shows the real book (e.g. Mahak Rai: 3,163 open leads / 4,451 tasks-30d). Data note: calls/meetings are near-zero for most reps — real Zoho behavior (activity logged as tasks), not a query bug.
Product

Role home dashboards (SDR + AE), live per-person data

SDR home (/sdr) and AE Desk (/ae) upgraded from welcome-blurb stubs to real dashboards, each scoped to the signed-in person via the metrics lib's rootUserId (a user with no reports resolves…

  • SDR home (`/sdr`) and AE Desk (`/ae`) upgraded from welcome-blurb stubs to real dashboards, each scoped to the signed-in person via the metrics lib's rootUserId (a user with no reports resolves to just their own owned records). SDR home leads with the lead book (leads owned, new-leads/30d, activity/7d tiles + lead-status breakdown + 12-week activity); AE Desk leads with pipeline (open pipeline ₹, won FY, new leads, activity tiles + pipeline-by-stage + 12-week activity), plus a link to the Team view. Honest empty state when no CRM records map to the user.
  • Reuses the existing getSalesOverview/getLeadStatusBreakdown/getPipelineByStage/getWeeklyActivity (all rootUserId-scoped) and the committed dashboard primitives (StatTile, ChartCard, CategoryBarChart, WeeklyActivityChart). Applied the same weekStart Date-coercion guard as the other dashboards.
  • Verified in Chromium: signed in as an SDR (Zubair) and an AE (Anisha) — both homes render real numbers and charts (Anisha's Desk: ₹6.8 Cr open pipeline / 24 deals), zero console errors.
Fix

HEAD_OF_SALES role (admin-tier) + fixed the KPI dashboards

New HEAD_OF_SALES role, admin-tier: it shares the /admin area and sees everything the CXO admins see (Command Board, users, org, audit, integrations). Rohit Naidu (id 163) promoted to it…

  • New `HEAD_OF_SALES` role, admin-tier: it shares the /admin area and sees everything the CXO admins see (Command Board, users, org, audit, integrations). Rohit Naidu (id 163) promoted to it (audited USER_ROLE_CHANGED; scripts/add-head-of-sales-role.ts, which also runs the ALTER TYPE user_role ADD VALUE 'HEAD_OF_SALES' AFTER 'ADMIN').
  • Access refactor so a role can share another's area: ADMIN_ROLES = [ADMIN, HEAD_OF_SALES], new requireAdmin() in dal.ts (replaces requireRole('ADMIN') across all 11 admin pages/actions), SEGMENT_ALLOWED_ROLES in roles.ts drives proxy gating (was 1:1 SEGMENT_ROLES), roleHome(HEAD_OF_SALES) = /admin, ROLE_NAV[HEAD_OF_SALES] = the admin nav. "Head of Sales" is now an assignable role in /admin/users.
  • Fixed two real crashes on the KPI dashboards (they were listed "working" but actually threw on render): (1) server components passed formatter functions (valueFormatter={formatInr}) to the client chart component — forbidden across the RSC boundary; replaced with a valueFormat="inr"|"count" string the client resolves. (2) weekStart from Postgres wasn't always a Date → Intl.DateTimeFormat threw "Invalid time value"; now coerced with a NaN guard. Both /admin/command and /ae/team render clean now.
  • Verified in Chromium: signed in as admin@glue.tools and as rohit.naidu@antino.com — both land on /admin, reach /admin/command, see ₹ figures + 16 chart SVGs, zero console/render errors.
Data

First metrics dashboards on live Zoho data (docs catch-up)

Shipped inside a7fe7fe alongside the pool-retry work, documented here: /admin/command (company-wide command board: ₹-pipeline/lead/deal stat tiles, pipeline by stage, lead-status breakdown,…

  • Shipped inside a7fe7fe alongside the pool-retry work, documented here: `/admin/command` (company-wide command board: ₹-pipeline/lead/deal stat tiles, pipeline by stage, lead-status breakdown, 12-week activity, top owners) and `/ae/team` (the same lens scoped to the signed-in AE's reporting subtree with per-member performance).
  • Foundations: src/lib/metrics/queries.ts (SQL over the zoho_* staging layer joined to our org tree) + format.ts (₹ INR + count formatting), and dashboard primitives StatTile / ChartCard / CategoryBarChart / WeeklyActivityChart — all charts on the entity color tokens, fixed order, per the color language.
Product

DB pool: retry-once on transient network errors

A momentary DNS blip (getaddrinfo ENOTFOUND nuvio.postgres.database.azure.com) surfaced as a user-facing 500 on /ae. The host resolved fine seconds later — purely transient.

  • A momentary DNS blip (getaddrinfo ENOTFOUND nuvio.postgres.database.azure.com) surfaced as a user-facing 500 on /ae. The host resolved fine seconds later — purely transient.
  • src/db/index.ts now wraps pool.query with a single retry (300ms backoff) on ENOTFOUND | EAI_AGAIN | ECONNREFUSED | ETIMEDOUT | ECONNRESET. Pre-connect errors are always safe to retry; ECONNRESET could in theory retry an in-flight write, accepted trade-off (low write volume, rare duplicate beats guaranteed 500). Callback-style queries bypass the wrapper (drizzle is promise-only); double-wrap guarded via a symbol on the hot-reload-shared pool.
  • Verified live: normal drizzle query works through the wrapper (234 users); against a bogus host the wrapper logs one [pg pool] transient network error, retrying once and fails only after the second attempt (~360ms) — so a blip that heals within the window succeeds silently.

10

Jul 2026

Friday

2 shipped

Data

Microsoft Graph pipeline: multi-tenant email + calendar + attachment bytes

New graph_ staging layer (src/db/graph-schema.ts) — fully-typed, jsonb-free mirror of M365 email + calendar. Tenant-aware: every table keyed by an org discriminator (ANTINO | GOLDENFLITH),…

  • *New `graph_ staging layer** (src/db/graph-schema.ts) — fully-typed, jsonb-free mirror of M365 email + calendar. **Tenant-aware**: every table keyed by an org discriminator (ANTINO | GOLDENFLITH), because Antino and Goldenflith are separate Microsoft tenants with separate app registrations. Creds per-org env vars; graph_tenants` mirrors them (AES-encrypted secret) for a future admin UI.
  • Attachment bytes live in Postgres (graph_message_attachments.content bytea) — the explicit requirement. Inline path for ≤3 MB, /$value fetch for larger; verified byte-perfect (a 25 MB PDF stored intact, %PDF%%EOF). One heavy mailbox ≈ 900 MB of attachments.
  • Engine (src/lib/graph/importer.ts + client.ts, CLI scripts/graph/run.ts): client-credentials auth per tenant; full pull (delta-primed) + every-15-min delta via stored deltaLink; messages (body, recipients, threading), events (attendees), attachments. Hardened against transient Azure Postgres/network errors — per-mailbox retry-then-skip, pool keepAlive + timeouts, so one blip can't kill a multi-hour pull. Added a targeted mailbox <emails> command for re-syncing specific inboxes.
  • Access model: User.Read.All lets us enumerate the directory, but mail read is gated by an Exchange application access policy scoped to the sales team per tenant. Inaccessible + hard-excluded (CEO/leadership: vinay@, ceo@goldenflitch.com) mailboxes are marked and skipped, never fatal.
  • Antino: app access policy grants the sales team. 13 sales mailboxes pulling (Anisha, Nitish + the 10 SDR/closers, plus Harshad/Ishika/Priyadarshini once added to the policy mid-session). Vedant excluded (disabled account); Dhruv Raj + Abhishek Rana turned out to be Goldenflith, not Antino (their org-chart placeholder @antino.com emails were wrong).
  • Goldenflith: separate tenant; seeded from its own directory (53 goldenflitch.com mailboxes), access policy gates to the sales set. Pulling now.
  • 15-min delta cron added to src/instrumentation.ts alongside the hourly Zoho sync — per-tenant, skips an org until its FULL pull completes, guards overlaps.
  • Creds for both tenants in .env (gitignored).
Fix

Incremental sync fixed for activity modules; first real delta pulled

First real-world incremental sync surfaced two COQL blind spots: activity modules (Tasks/Calls/Events) reject What_Id — the polymorphic link we must have — and Deals.Stage_Modified_Time /…

  • First real-world incremental sync surfaced two COQL blind spots: activity modules (Tasks/Calls/Events) reject What_Id — the polymorphic link we must have — and Deals.Stage_Modified_Time / Calls.CTI_Entry / Events.Participants are unqueryable. Fix: Tasks/Calls/Events now sync via the same Modified_Time-desc REST walk as Notes (REST returns What_Id fine, deletions still tracked via the deleted API); the COQL path self-heals by dropping any column Zoho names in an INVALID_QUERY error (learned per process, mirroring the bulk-import behavior).
  • Sync then pulled two days of real CRM activity cleanly: Leads +650 upserts/2 deletes, Tasks +1,548/4, Notes +81, Calls/Accounts/Contacts small. Post-sync counts verified against live Zoho: Leads 19,034 / Tasks 588,206 / Contacts 140,500 all exact.
  • Known limitation confirmed in the wild: Zoho exposes no deleted-records API for Notes, so notes deleted in Zoho persist here until the next full import (currently 1 such row).

08

Jul 2026

Wednesday

1 shipped

Data

Zoho pipeline: full CRM in Postgres, Integrations page, hourly sync, webhook

All Zoho data is in our DB — 866k+ rows across a fully-typed, jsonb-free staging layer (zoho_): Tasks 587,297 · Contacts 140,499 · Accounts 50,904 · Notes 47,874 · Calls 19,384 · Leads…

  • All Zoho data is in our DB — 866k+ rows across a fully-typed, jsonb-free staging layer (zoho_*): Tasks 587,297 · Contacts 140,499 · Accounts 50,904 · Notes 47,874 · Calls 19,384 · Leads 18,610 · Deals 1,660 · Events 891 + campaigns/products/vendors, plus related lists (stage history 3,642 · contact roles 1,615 · campaign members 559) and zoho_users (70) with the zoho_user_mappings bridge to our users (email/local-part/name matching; catches anisha@antino.comanisha@antino.io).
  • Schema is generated from Zoho's own field metadata (scripts/zoho/generate-staging-schema.tssrc/db/zoho-schema.ts): one typed column per field, bigint lookup ids (indexed), multiselects as child tables. No hard FKs on staging by design (Zoho doesn't enforce integrity; truncate-reload + out-of-order sync) — orphan tallies instead (currently 0 dangling refs); the future domain layer gets hard FKs.
  • Engine (src/lib/zoho/importer.ts, ultracode-built + live-tested): Bulk Read v8 full imports (200k-row CSV pages, chunked inserts, self-healing drop-and-retry for bulk-unsupported fields), hourly incremental sync (per-module Modified_Time checkpoints via COQL, deleted-records tracking, Notes REST walk), targeted webhook sync, Zoho watch registration. Usable from server actions, instrumentation, and CLI (scripts/zoho/run.ts full|sync|users|related|module <Name>).
  • `/admin/integrations` — connection status, per-module batch table, Import everything / Sync now / Enable push (audited: ZOHO_IMPORT_TRIGGERED/ZOHO_SYNC_TRIGGERED/ZOHO_PUSH_ENABLED; admins notified), auto-refresh while running; in the admin sidebar.
  • Hourly cron in src/instrumentation.ts (activates on server boot; skips until a FULL import exists; renews the Zoho watch when APP_PUBLIC_URL is set). Webhook /api/zoho/webhook (token-checked; proven live — a pushed vendor id round-tripped through targeted sync).
  • Verified: all 11 module counts match live Zoho exactly; lead-status distribution reproduces the audit (77.3% Open); the "Anisha's active leads" question answers identically from pure SQL (Open 1,272 / Working 2: MILIND + Srishti, stale since March) — and SQL surfaced a status (Disqualified, 3 leads) the per-picklist API counting had missed.
  • Zoho credentials + webhook token live in .env (gitignored).

07

Jul 2026

Tuesday

8 shipped

Product

Entity color language

One permanent hue per domain entity, built on a dataviz-validated 8-slot categorical palette (all-pairs CVD on white surface; chip ink ≥ 4.5:1): Account blue · Lead aqua · Task yellow ·…

  • One permanent hue per domain entity, built on a dataviz-validated 8-slot categorical palette (all-pairs CVD on white surface; chip ink ≥ 4.5:1): Account blue · Lead aqua · Task yellow · Deal green · Contact violet · Meeting red · Employee magenta · Email orange.
  • Tokens in globals.css @theme (--color-entity-<kind> base + -text ink) → utilities like bg-entity-lead/10, text-entity-deal-text. Registry + helper in src/lib/entity-colors.ts (labels, lucide icons, literal class strings, chartVar); <EntityBadge kind variant={chip|dot|icon}> in src/components/entity-badge.tsx.
  • Rules: color follows the entity everywhere (chips, dots, icon tiles, chart marks), fixed, never cycled, never reassigned by rank; never color-alone (icon/label always rides along); charts about entities use entityColor(kind).chartVar in the fixed slot order (the CVD-safety mechanism).
  • Showcased on /design#entity-colors (chips/dots/tiles, token table, chart-order strip), linked in the sidebar under Foundations.
Fix

Sales leadership designations fixed from the org chart

scripts/fix-sales-designations.ts (idempotent, audited USER_UPDATED): the HR sheet's generic VP/AVP titles were overriding the chart's role labels. Rohit Naidu → Head of Sales (per user;…

  • scripts/fix-sales-designations.ts (idempotent, audited USER_UPDATED): the HR sheet's generic VP/AVP titles were overriding the chart's role labels. Rohit Naidu → Head of Sales (per user; chart label was "Director-Business & Strategy"), Anisha Ratnam → Head of Startup, Nitish Kumar → Head of Growth, Himanshu Gupta → Head of Government. Harshad/Vinay already correct.
  • Reporting tree needed no changes — all 17 chart edges already matched the image.
  • L4 "Key Accounts" people keep their specific HR designations (Business Consultant / Trainee / Intern / Tender Executive); the chart's "Key Accounts" reads as the team grouping, not individual titles.
Data

Zoho CRM current-state audit (docs/ZOHO-CRM.md)

Mapped the live Zoho CRM (read-only) and wrote it up in docs/ZOHO-CRM.md, linked from the docs index: module inventory + record counts, the full entity-relationship graph (lookups + related…

  • Mapped the live Zoho CRM (read-only) and wrote it up in docs/ZOHO-CRM.md, linked from the docs index: module inventory + record counts, the full entity-relationship graph (lookups + related lists), the Tasks linkage analysis, and the fault lines vs. SALES-PROCESS.md.
  • Entity relationships: hub-and-spoke around Accounts (company) + Contacts (people); Leads convert into Account/Contact/Deal; Deals hang off Account + Contact (+ Contact Roles M:M); Activities (Tasks/Calls/Meetings) attach polymorphically via Who_Id/What_Id. The live graph is only Leads → Accounts/Contacts → Deals + Activities — the quote-to-cash chain, Cases, Price Books, and the custom Legal Contract Management module are 0 records (empty scaffolding).
  • Volumes: Contacts 140.5k, Tasks 587k, Accounts 50.9k, Calls 19.4k, Leads 18.5k, Deals 1.66k, Meetings 891; Quotes/Sales Orders/Invoices/Purchase Orders/Price Books/Cases/Legal Contract Management all 0.
  • Tasks (587k) link mostly to leads/contacts, not deals; the convention shifted over time (older to Contacts 76%, newer to Leads 83%; Accounts steady ~13%; Deals 4-10%). Confirms activity lives at the top of the funnel, not on opportunities.
  • Docs-only change; no code touched.
Design

Sidebar app shell, shadcn light theme, org chart redesign, full company mapped, Zoho + sales-process absorbed

Sidebar app shell for every role (AppSidebar + AppShell rewrite): shadcn Sidebar (collapsible to an icon rail), role-aware nav with icons from ROLE_NAV (src/lib/nav.ts), an inline SVG logo…

  • Sidebar app shell for every role (AppSidebar + AppShell rewrite): shadcn Sidebar (collapsible to an icon rail), role-aware nav with icons from ROLE_NAV (src/lib/nav.ts), an inline SVG logo mark (components/logo.tsx) + src/app/icon.svg favicon, breadcrumbs in the header (AppBreadcrumbs), and a user dropdown with sign-out. Replaces the old top-nav; nav-links.tsx deleted. All five role layouts now just render <AppShell user={user}>.
  • Pure shadcn LIGHT theme for the app, scoped to .app-theme on the shell wrapper (globals.css): redefines the standard shadcn tokens and the legacy antino tokens (--ink*, --panel*, --clay*, --sage/rust/amber, sidebar tokens) to zinc/neutral light values. The marketing landing + public pages (:root, .dark, .section-dark) are untouched. Deep Daylight styling is deferred; DESIGN.md's warm-cream rules now apply to marketing only, not the app.
  • All logged-in pages are full width — removed every mx-auto max-w-* wrapper (admin home/users/org/audit/settings, role homes, settings panel).
  • Admin home rebuilt into a real overview (people counts briefing + recent-activity + pending-invites cards; no placeholder). Audit/settings full-width; audit maps ::1/127.0.0.1localhost.
  • Users table: sortable columns, shadcn Select for role, a searchable combobox (select2-style) for manager across all people.
  • Org chart redesigned as a top-down boxed hierarchy (org-tree.tsx): level-colored boxes + connector lines, leaf reports grouped into bulleted boxes, search with highlight, collapse/expand + expand-all/collapse-all, auto-collapse on large trees. page.tsx now roots founders/admins (fixes the bug that hid Radhakanth & Rajesh).
  • Notification bell golden rule: recentNotifications() returns the latest 5 unread only (badge still shows total unread).
  • Full company reporting tree loaded (scripts/apply-designation-tree.ts, idempotent + audited): reads the HR designation sheet's RM column (scripts/data/reporting-lines.json), sets manager_id for IT/Operations/Product while sales keeps the org-chart image tree (Sales/Marketing rows skipped). Aligned the two founder codes (Radhakanth ANTINO306, Rajesh ANTINO57) so their reports resolve. 210 manager changes applied; org chart went from 20 → 230 of 234 placed.
  • Sales domain absorbed into docs/SALES-PROCESS.md (statuses, temperature, opportunity stages, 8-phase flow, processes, tasks, SLAs, RACI) and linked in the docs index.
  • Zoho CRM explored (read-only) and stress-tested against the doc — fault lines found: missing Discovery/Solution-Validation deal stages, temperature ~8% populated, forecast discipline unused (Best Case=1 / Committed=0), 77% of leads stuck in Open, a parallel undisciplined Contacts-status funnel, and bulk-enriched data vs. curated ICP. Zoho OAuth creds stored in .env (gitignored).
  • Fixes found via live Chrome verification: root-layout inline <script><noscript> fallback (kills the React-19 script error + the original hydration warning); TooltipProvider added to the shell (sidebar tooltips were crashing); org-tree invalid <li>-in-<li> nesting → divs. tsc green; interactive checks (sorting, bell, dropdown) verified.
  • Monochrome cleanup pass: replaced every leftover warm token in the app (sage/rust/amber/clay) with shadcn-native patterns — status chips → Badge variants (secondary / destructive / outline), form errors + delete → destructive, success → foreground, notification badge/dot/link → primary/muted-foreground, audit action pill → secondary. Org chart stays intentionally color-coded by level. The scalable win: statuses now render through the shared Badge component, not hand-rolled colored spans.
  • Org chart made scalable for the full 234-person tree (org-tree.tsx), and turned horizontal (left-to-right): root on the left, each level a column to the right, siblings stacked vertically, so a large org grows down the page (natural scroll) instead of sprawling sideways. Auto-collapse to the top 3 tiers on large trees (opens as an overview, not a wall), Expand-all / Collapse-all controls, stray leadership split into their own row, and single leaf reports render as boxes instead of a "Reports (1)" list. Large report groups flow into multiple columns (2 at >10, 3 at >24) so a 40-person team is a compact card, not a tall strip, with no internal scrollbars. Fixed the .orgchart connector CSS hijacking the inner report lists (they were laid out sideways).
  • Calendar hydration fix (ui/calendar.tsx): data-day is now a deterministic YYYY-MM-DD id instead of locale toLocaleDateString(), which mismatched between Node SSR and the browser on every day cell (surfaced on /design).
  • `/design` showcase reskinned to the app's shadcn-light theme (scoped .app-theme on its root); intro copy updated. It was still rendering the old cream Daylight tokens.
Design

Micro-interactions on every component, tested live on /design

18-agent workflow: per-component micro-interaction pass across all 60 shadcn components (79 files: ui/ bases + /design section demos), on the product motion voice — 100–250ms,…

  • 18-agent workflow: per-component micro-interaction pass across all 60 shadcn components (79 files: ui/ bases + /design section demos), on the product motion voice — 100–250ms, ease-out-quart/quint, transform/opacity only, reduced-motion safe, purely presentational (no API changes; the polish reaches every app surface).
  • Highlights: checkbox check pops in + indeterminate now renders a minus; radio dot zooms in; switch thumb eases across and compresses on press; slider thumb grows on hover/grab; select/combobox chevrons rotate open and their check indicators pop; OTP digits pop in; input-group copy button flashes a sage confirmation; kbd chips got keycap depth; button trailing icons nudge on hover; toggles scale on press (suppressed in fused segments); avatar images fade in on load; badges-as-links sink on press; field errors rise in; and equivalent passes across feedback, navigation, overlay, and data components.
  • Every one of the 62 /design sections functionally tested in Chromium (Playwright): toggles flipped, overlays opened and Escape-closed, sliders dragged, tabs switched, toasts fired — state asserted via aria/data attributes, console errors collected, screenshots reviewed.
  • 3 real issues found, all fixed and re-verified: CommandDialog crashed the page when opened (missing cmdk <Command> root inside the dialog — real bug, now fixed in ui/command.tsx); split-dropdown menu rendered at min-width; line-chart x-axis dropped its first tick. Build green.
Design

Audit trail page, integrations keys, org chart, audit geo-context, UI polish

/admin/audit — the audit spine gets its read surface: filterable trail (action select from the registry, actor/entity search), expandable rows with before/after payloads and full request…

  • `/admin/audit` — the audit spine gets its read surface: filterable trail (action select from the registry, actor/entity search), expandable rows with before/after payloads and full request context.
  • Audit context enrichmentrecordAudit() now captures ip, raw user agent plus parsed browser/OS/device (ua-parser-js), geography (Vercel/Cloudflare headers when deployed; null locally), referer, and accept-language on every event, automatically for all call sites.
  • Integrations (/admin/settings, admin-only) — Resend + Anthropic API keys stored AES-256-GCM encrypted (key derived from SESSION_SECRET); format-validated (re_… / sk-ant-…); masked previews only (re_••••••cdef) in UI, audit (SETTING_UPDATED), and admin notifications. New app_settings table + SETTING_KEYS/getSetting/setSetting in src/lib/settings.ts.
  • `/admin/org` — org chart from users.manager_id: CEO → Director → Heads → Key Accounts tree with role badges, report counts, suspended dimming, and the unmapped-people count.
  • UI sweep: show/hide toggle on every password field (shared PasswordInput); every button now carries an icon; nav gained an active state (clay underline + aria-current); users table gained an empty state for no-match searches.
  • Tested in real Chromium (Playwright): 16-check flow suite (sign-in incl. show-password toggle, add person → invite accept → bell → change password → suspend/blocked-sign-in → reactivate → forgot-password) plus org/integrations checks — all green, screenshots delivered.
  • Known hydration warning on <html> (js class script from the landing work) noted for a future fix.
Fix

Sticky nav fix, login verified, founders added as ADMIN

Sticky nav bug fixed: .section-dark set position: relative outside any cascade layer, which beat the layered sticky utility on the landing nav (unlayered CSS wins). Dropped the position…

  • Sticky nav bug fixed: .section-dark set position: relative outside any cascade layer, which beat the layered sticky utility on the landing nav (unlayered CSS wins). Dropped the position (its isolation: isolate already provides the stacking context); verified in-browser — nav pinned at top: 0 with position: sticky at scrollY 2400.
  • Login flow verified in the browser (Playwright/Chromium against the prod build): admin@glue.tools + dev password → lands on /admin, shell shows Sign out; DB health confirmed.
  • Founders added as ADMIN (scripts/add-founders.ts, idempotent, audited): Rajesh Kumar (Co-Founder & COO, rajesh@antino.io, id 234) and Radhakanth Kodukula (Co-Founder, radhakanth@antino.io, id 235). Emails follow the @antino.io leadership pattern but are assumed (flagged in the audit context). Vinay was already ADMIN. Admins now: Glue Admin, Vinay, Rajesh, Radhakanth. Users total: 234.
Product

Credential flows, user-management suite, notifications spine live

Schema: new credential_tokens (single-use invite / password-reset links; only the SHA-256 hash stored, prior unused same-type tokens voided on reissue) and notifications (recipient, type =…

  • Schema: new credential_tokens (single-use invite / password-reset links; only the SHA-256 hash stored, prior unused same-type tokens voided on reissue) and notifications (recipient, type = audit action, title/body, FK to the source audit_events row, read_at). The notifications spine is live.
  • Audit registry: all action strings now come from the AUDIT_ACTIONS const in src/lib/audit-actions.ts; added USER_DELETED, USER_INVITED, USER_INVITE_ACCEPTED, PASSWORD_RESET_REQUESTED, PASSWORD_RESET_LINK_ISSUED, PASSWORD_RESET_COMPLETED, PASSWORD_CHANGED. Existing role/status/manager actions refactored to the registry and now also notify (affected user + active admins excluding the actor; old/new manager on manager changes). Every notification carries its audit event id.
  • `/admin/users` suite: "Add person" dialog creates a passwordless user and swaps to a copyable invite-link panel (7-day expiry); rows get a dropdown menu — Edit details (prefilled dialog), Copy invite link (reissues for never-activated users, amber "Invited" chip), Issue reset link (2-hour expiry, for password users), Suspend/Reactivate, and Delete (hard delete, never-activated only, AlertDialog confirm). The page query exposes hasPassword as a SQL boolean so the hash never reaches the client.
  • Public credential pages, styled to match the sign-in card: /forgot-password (byte-identical generic response for known/unknown emails — anti-enumeration — while active users generate an admin-notifying audit event), /invite/[token] (set first password → session → role home; notifies issuer + manager), /reset-password/[token] (set new password → /sign-in; security receipt to the user + admins). Passwords bcrypt cost 10, min 8, confirm-match validated client and server; tokens re-validated server-side on submit and consumed once.
  • Bell + settings: app shell (server component) now renders a notifications bell for every role — clay unread badge (99+ cap), popover list, click-to-mark-read, mark all read — plus a Settings nav item. /<role>/settings for all five roles: read-only profile card + change-password form (verify current, audited PASSWORD_CHANGED, receipt notification).
  • Verified end-to-end against the dev server: tsc clean; full invite lifecycle (create → invite link → set password → signed in as DEV, token consumed); forgot-password enumeration check; reset + change-password round trips with fresh sign-ins; bell showing correct unread count; delete guards (self / activated / has-reports rejected) confirmed in code. Test user and tokens cleaned up.
  • Known limitation (queued): JWT sessions are not invalidated on password change/reset — they remain valid until expiry.

06

Jul 2026

Monday

9 shipped

Design

Docs refresh + full-width /design

Root README.md rewritten from create-next-app boilerplate to a real project readme: stack, env setup (APP_DATABASE_URL, SESSION_SECRET), seeding scripts, dev sign-in, house rules, docs…

  • Root README.md rewritten from create-next-app boilerplate to a real project readme: stack, env setup (APP_DATABASE_URL, SESSION_SECRET), seeding scripts, dev sign-in, house rules, docs pointers.
  • ENTITIES.md brought current: two-pass seeding story (sheet import + org tree), 232-user total, full audit action list.
  • TASKS.md: committed batch (90edadf) moved to Done.
  • /design main column is now full width (dropped the max-w-4xl cap) — component sections fill the viewport beside the sidebar.
Design

Landing page, design system port, /design showcase, polish pass

Design system ported from antino.nuvio.dev (our own production site): full token set (warm cream/espresso/clay, light + dark + .section-dark island) into globals.css (Tailwind v4 @theme),…

  • Design system ported from antino.nuvio.dev (our own production site): full token set (warm cream/espresso/clay, light + dark + .section-dark island) into globals.css (Tailwind v4 @theme), IBM Plex Sans/Mono via next/font, radius 14px, warm elevation scale (shadow-rest/raised/float).
  • shadcn/ui installed — all 60 components at src/components/ui, restyled by the token layer.
  • Landing page (`/`) rebuilt from the live site, sections 00–09 verbatim: dark hero with typewriter briefing card, sample email with numbered receipts, eight-agents grid, animated counters, principles, how-it-works tabs, stop/start diff, built-by stats, FAQ accordion, pilot CTA. Built via a 10-agent ultracode workflow (theme → build → assemble → verify → 3 reviewers → fixer; 12/13 findings fixed).
  • `/design` showcase — 62 sections, every installed component with variants, foundations (tokens/type/radius), scrollspy sidebar, dark-preview strip. All 78 sidebar links verified against section ids.
  • Lift, not borders (rule locked): ~60 card containers across 35 files converted from hairline outlines to tone step + warm shadow + card-lift hover.
  • Polish pass (browser-audited on the production build): hero headline geometry fixed to match source (3 lines), text-balance on display headings, terminal-style caret blink, breathing LIVE dots, feed-row hovers, button press states + arrow nudges, nav underline slides, FAQ numbers turn clay when open, and annotation-cards ↔ email-superscript hover linking. All motion ease-out, reduced-motion safe. Verified live: agents grid, counters (1/15/8/0.59/0), accordion, scrollspy; zero console errors.
  • Stub /privacy + /terms pages; playwright added as devDep (used for full-page captures).
Product

Sales org tree seeded from org chart

Applied the sales hierarchy from the org chart image (scripts/apply-org-tree.ts, idempotent, fully audited): Vinay Krishna Gupta (CEO, ADMIN) → Rohit Naidu (Director-Business & Strategy) →…

  • Applied the sales hierarchy from the org chart image (scripts/apply-org-tree.ts, idempotent, fully audited): Vinay Krishna Gupta (CEO, ADMIN) → Rohit Naidu (Director-Business & Strategy) → four Heads (Anisha Ratnam/Startup, Nitish Kumar/Growth, Himanshu Gupta/Government, Harshad/Growth & Partnerships) → 11 Key Accounts people. 16 reporting edges.
  • Created 5 users missing from the designation sheet: Vinay (vinay@antino.io, ANTINO86) and Paridhi Srivastava (paridhi.srivastava@antino.com, ANTINO875) with real emails from the sheet's second tab; Harshad, Dhruv Raj, Abhishek Rana with placeholder emails (harshad@, dhruv.raj@, abhishek.rana@antino.com) — no source lists their real ones. All get the shared dev password.
  • Renamed "Rohit S" → "Rohit Naidu" (his rohit.naidu@antino.com email + the chart confirm the sheet name was truncated).
  • Role calls: CEO → ADMIN; Heads and Key Accounts (deal-owning) → AE; sheet-designated trainees/interns/tender exec stay SDR. Antino site (antino.com) confirms Vinay as Co-founder & CEO.
  • Users total: 232.
Data

Auth, user import, admin user management

Schema: users gains status (user_status: ACTIVE | SUSPENDED), employee_code (unique), designation, department, password_hash. New audit_events table — the audit spine is live (recordAudit()…

  • Schema: users gains status (user_status: ACTIVE | SUSPENDED), employee_code (unique), designation, department, password_hash. New audit_events table — the audit spine is live (recordAudit() in src/lib/audit.ts, secrets redacted).
  • Import: scripts/import-users.ts loads 226 employees from the designation sheet (scripts/data/designation-data.json) + seed admin (admin@glue.tools). All share the dev password dev-test-pw-1 (bcrypt). Roles mapped heuristically: IT leadership/leads → TECH_LEAD, IT rest → DEV, sales leadership/managers → AE, sales juniors → SDR, other departments → DEV (open question). Result: 1 ADMIN, 26 TECH_LEAD, 9 AE, 6 SDR, 185 DEV. Idempotent; every row audited.
  • Auth: email + password sign-in (/sign-in), jose-signed JWT session cookie (7d) with userId + role. DAL (src/lib/auth/dal.ts): verifySession / getCurrentUser / requireRole. Suspended users blocked at sign-in and DAL.
  • URL convention locked in: /<role-name>/<page-name>. src/proxy.ts (Next 16 renamed middleware → proxy) optimistically gates role areas and bounces wrong-role visitors to their own home; authoritative checks stay in layouts/actions.
  • User management: /admin/users — search/filter 227 people, inline role change, manager assignment (self/cycle-safe), suspend/reactivate. Self-role-change and self-suspend blocked. Every mutation audited + revalidatePath.
  • Role home stubs for all five roles so every sign-in has a landing surface.
  • Verified end-to-end against the dev server: sign-in (admin → /admin, tech lead → /tech-lead, bad password rejected), wrong-role redirect, 227 rows SSR'd on /admin/users, audit counts (227 USER_CREATED, USER_SIGNED_IN on each sign-in).
Docs

Docs system established

Added the living docs set: README, JOURNAL, PRODUCT, TASKS, ENTITIES, PAGES (this folder).

  • Added the living docs set: README, JOURNAL, PRODUCT, TASKS, ENTITIES, PAGES (this folder).
  • Rule locked in: all docs files are updated before every commit and push.
Product

Manager hierarchy (`503b23a`)

users.manager_id: nullable self-referencing FK to users.id; null = top of tree.

  • users.manager_id: nullable self-referencing FK to users.id; null = top of tree.
  • Drizzle relations: manager (one) / directReports (many).
  • Open question noted: does role imply hierarchy position, or is the tree free-form? Currently free-form.
Product

CAPITAL_SNAKE enums (`a330bf9`)

Project-wide convention set: enum values CAPITAL_SNAKE_CASE, plural table names, semantic column names.

  • Project-wide convention set: enum values CAPITAL_SNAKE_CASE, plural table names, semantic column names.
  • user_role values changed to ADMIN | SDR | AE | TECH_LEAD | DEV, default DEV.
Product

Database live

supernova database created on Azure by admin; granted dealflow_app USAGE/CREATE on schema public, all privileges on the database, and default privileges on future tables/sequences.

  • supernova database created on Azure by admin; granted dealflow_app USAGE/CREATE on schema public, all privileges on the database, and default privileges on future tables/sequences.
  • First drizzle-kit push applied: user_role enum + users table. Verified end-to-end as app user.
Product

Initial scaffold (`bd35629`)

Next.js 16 app (App Router, TS, Tailwind v4, ESLint) scaffolded into empty repo.

  • Next.js 16 app (App Router, TS, Tailwind v4, ESLint) scaffolded into empty repo.
  • Drizzle ORM + pg wired to Azure Postgres; .env with APP_DATABASE_URL (git-ignored).
  • /api/health route for DB connectivity checks; db:generate/migrate/push/studio scripts.
  • docs/DESIGN.md copied from antino3 — current direction is Daylight (top section); everything below the HISTORICAL marker is reference only.
  • Git repo initialized, pushed to private GitHub repo verma6uc/antino4.