strategic-overview-modules.md49.4 KBView on GitHub
# Strategic Overview — seeded Strategist, per-conversation overview instances, conditional & rich fields, contact profiles

## 1) Introduction — goal, present state, future state

We want the Strategic Overview to be owned by a seeded, always-on **Strategist** system agent (like the CRM updater — worst case it does nothing), where each conversation carries its own **instantiated active list of overview items** (fields *and* embedded docs) materialized from an AOP-level catalog by evaluating per-item conditions against that deal's facts, and where personalization is a natural-language prompt the main chat agent applies to the Strategist's config. A single Strategist that manages the whole overview from its own playbook doc is the default and is fully sufficient; the ability to *reference and dispatch* further subagents (a battlecard, a stakeholder map) is an available option, not a requirement — nothing here forces recursion. Today the Strategist is not seeded for the general population (only `crm-updater, next-steps, meeting-prep, daily-agenda` are in `SYSTEM_SUBAGENT_FILENAMES` at [aop-agents.ts:176](apps/server/src/services/aop/aop-agents.ts); `SystemAgentName.STRATEGIST` exists in the enum but is jesse-only backfilled), the overview layout is one AOP-wide `displayConfig.strategicOverview` of scalar-only `topRow`+`sections` ([aop-schema.ts:447](apps/server/src/db/aop-schema.ts)) whose empty state falls back to *dumping every custom field* ([StrategicOverviewTab.tsx:47](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)), with no conditional visibility and no rich rendering (only a `linkedDocPath` link icon), and per-contact research produced by Meeting-Prep/Multithreader is trapped in conversation-level markdown and never reaches the person (`crmContacts.notes`/`customFields` exist but are unsurfaced, and there is no `contact/{…}` document scope). We will (a) seed the Strategist as a real always-on agent — both its `subagents/strategist.md` doc **and** a `<ref>` in every account's playbook (new via the template, existing via backfill), since a subagent only runs when the playbook references it; (b) evolve the overview into a two-layer catalog→instance model with a small declarative condition grammar plus an agent-relevance gate; (c) add markdown field rendering and inline document-embed items (reusing the doc hydrator + `ConversationLayoutItem`'s existing `doc` kind); (d) add a `contact/{personKey}` document scope so durable research accumulates on the person and surface the existing structured notes; and (e) give the main chat agent tools to read and mutate the Strategist's config so an AE personalizes by prompt. The empty overview no longer dumps generic CRM fields — it renders the **seeded strategic fields empty**, with a persistent "Add & configure your overview" control and a "Populate overview" action that invokes the Strategist; and the Strategist's seeded instructions carry a durable "how to manage the conversation overview context" section so the capability is self-documenting and survives future edits. The AE-facing install *gallery/wizard* is out of scope (deferred). Two attribute fields ship early: `risk` is **reused** from the native `crmConversations.risk` column, and a new `on_track` select is seeded on every deal.

## 2) Present state

### 2.1 Architecture diagram

```text
  SEED (per account):
    seedPlaybookFiles → SYSTEM_SUBAGENT_FILENAMES = { crm-updater, next-steps,
                        meeting-prep, daily-agenda }   ← NO strategist
    computeStrategicSeedGapFill → Deals AOP.customFieldDefinitions += STRATEGIC_OVERVIEW_FIELDS
                        + displayConfig.strategicOverview = STRATEGIC_OVERVIEW_LAYOUT (topRow+sections)
                         │
  EVENT → on-event-orchestrator dispatches @subagents/<name> in fixed order
            crm-updater → (strategy subagents if playbook lists them) → next-steps
                         │  each runAgent() writes owned fields (ownerAgentId) + an
                         │  agent-namespace overview doc (agentOverviewPath)
                         ▼
  RENDER (per conversation, but layout is AOP-wide):
    StrategicOverviewTab
      layout = aop.displayConfig.strategicOverview ?? buildFallbackLayout(defs)
      topRow.map(renderStat)          → scalar badges only
      sections.map(renderTextSection) → scalar text / textarea only
      linkedDocPath → a 🔗 icon that navigates to the doc (no inline embed)
                         │
  CONTACTS (parallel, disconnected):
    peopleGlobal (enrichment) ─ crmContacts{notes,customFields} (unsurfaced)
      ─ conversationContacts{role}     ContactsTab renders enriched people
    Meeting-Prep / Multithreader → conversation/{id}/agent-{agentId}/overview (markdown)
      per-attendee research NEVER flows down to the contact record
```

### 2.2 Step-by-step walkthrough

1. **System subagents seeded** — `SYSTEM_SUBAGENT_FILENAMES` at [aop-agents.ts:176](apps/server/src/services/aop/aop-agents.ts) is `{ crm-updater, next-steps, meeting-prep, daily-agenda }`; the default playbook + docs are built by `buildPlaybookTemplate` / `seedPlaybookFiles` at [seed-playbook.ts:147](apps/server/src/services/playbook/seed-playbook.ts) with builders `buildCrmUpdaterContent`/`buildNextStepsContent`/`buildMeetingPrepContent`/`buildDailyAgendaContent` ([seed-playbook.ts:217-315](apps/server/src/services/playbook/seed-playbook.ts)). `SystemAgentName.STRATEGIST` exists ([aop-schema.ts:220](apps/server/src/db/aop-schema.ts)) but is **not** seeded — no builder, not in the filename set, not referenced in the default playbook.
   - Seeded subagent doc frontmatter shape:
     ```yaml
     name: crm-updater
     description: Keeps objective CRM fields current
     model: sonnet
     enabled: true
     agent_id: <uuid>
     fill_instructions: Customize what fields this agent focuses on…
     ```
2. **Strategic fields + layout seeded** — `computeStrategicSeedGapFill` at [strategic-overview-fields.ts:195](apps/server/src/services/crm/strategic-overview-fields.ts) idempotently adds `STRATEGIC_OVERVIEW_FIELDS` ([:26](apps/server/src/services/crm/strategic-overview-fields.ts), 12 fields: forecast, the_play, how_we_win, risks, 8× meddpicc_*) and sets `displayConfig.strategicOverview = STRATEGIC_OVERVIEW_LAYOUT` ([:161](apps/server/src/services/crm/strategic-overview-fields.ts)) when absent, never clobbering user edits.
   - `StrategicOverviewConfig` ([aop-schema.ts:447](apps/server/src/db/aop-schema.ts)) — **scalar only, AOP-wide**:
     ```ts
     { topRow: [{fieldId:'status',kind:'cedar'}, …5 cells],
       sections: [{id:'the_play',title:'The play',fieldIds:['the_play']},
                  {id:'meddpicc',title:'MEDDPICC',fieldIds:['meddpicc_metrics', …8]}] }
     ```
3. **Native attribute columns already exist** — `crmConversations` at [crm-schema.ts:429](apps/server/src/db/crm-schema.ts) already has `status` ([:455](apps/server/src/db/crm-schema.ts)), `priority` ([:456](apps/server/src/db/crm-schema.ts)), and **`risk` ([:460](apps/server/src/db/crm-schema.ts))** ("enum low/medium/high, agent-maintained by the crm-updater and a signal in the conversation index"), `dealValue` ([:468](apps/server/src/db/crm-schema.ts)). There is **no** `on_track` and **no** per-conversation overview list column.
4. **Company facts available** — `companiesGlobal` at [crm-schema.ts:140](apps/server/src/db/crm-schema.ts) carries `linkedinHeadcount` ([:173](apps/server/src/db/crm-schema.ts)) and `employeeCountRange` ([:174](apps/server/src/db/crm-schema.ts)), joined to a conversation via `primaryCompanyId → crmCompanyRelationships`. These are the facts a segment/size condition can read.
5. **Event dispatch** — the on-event orchestrator ([on-event-orchestrator-agent.ts:142](apps/server/src/mastra/agents/on-event-orchestrator-agent.ts)) runs subagents in a fixed order (crm-updater → strategy subagents the playbook lists → next-steps); each subagent is resolved + dispatched by `runSubagentTool` ([runSubagentTool.ts](apps/server/src/mastra/tools/event-execution/runSubagentTool.ts)), which honors `frontmatter.enabled === false` (skip, [:159](apps/server/src/mastra/tools/event-execution/runSubagentTool.ts)) and builds a `syntheticAgent` with **no model field** ([:181-200](apps/server/src/mastra/tools/event-execution/runSubagentTool.ts)) — so `frontmatter.model` is display-only, not wired to runtime selection.
6. **Subagents write fields + a per-deal doc** — each `runAgent` gives the agent `update-conversation-fields` and an agent-namespace overview doc at `agentOverviewPath` ([convention-paths.ts:76](apps/server/src/services/documents/convention-paths.ts) → `conversation/{id}/agent-{agentId}/overview`); field writes are gated by `ownerAgentId` ([aop-schema.ts:316](apps/server/src/db/aop-schema.ts), Phase 10 of [[strategic-overview]]).
7. **Overview render is AOP-wide + scalar, with a bad empty state** — `StrategicOverviewTab` resolves `const layout = normalizeLegacyLayout(aop?.displayConfig?.strategicOverview ?? buildFallbackLayout(defs))` ([StrategicOverviewTab.tsx:424](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)); when no strategic layout is configured, `buildFallbackLayout` ([:47](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) synthesizes a section that **dumps every non-internal custom field** — so an unconfigured deal shows a wall of generic CRM fields instead of the strategic set. It then does `topRow.map(renderStat)` ([:677](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) and `sections.map` special-cased by id ([:706](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)). `renderStat` ([:441](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) and `renderTextSection`/`FieldValue` ([:617](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx),[:221](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) emit only badges/text/textarea. The only doc affordance is a 🔗 icon from `linkedDocPath` calling `openDoc` ([:648](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)). No conditional visibility exists anywhere (empty cells simply drop). There is no "configure" or "populate" affordance in the tab body.
   - Note the *doc-capable* item type already exists but is used only by the CRM card / overview-tab configs, not the strategic overview: `ConversationLayoutItem` ([aop-schema.ts:387](apps/server/src/db/aop-schema.ts)) has `fieldId:'doc'` + `docPath`.
8. **Contacts model** — three layers: `peopleGlobal` (enrichment, keyed by email), `crmContacts` ([crm-schema.ts:284](apps/server/src/db/crm-schema.ts)) with per-user `notes` ([:309](apps/server/src/db/crm-schema.ts)) + `customFields` ([:312](apps/server/src/db/crm-schema.ts)) (both **not** in the frontend `PersonGlobal` type, so unsurfaced), and `conversationContacts` (join with `role`). Rendered by `ContactsTab.tsx` / `DirectoryTab.tsx`. Doc scopes today are org/user/conversation/thread only ([convention-paths.ts](apps/server/src/services/documents/convention-paths.ts)); there is **no** `contact/{…}` scope, so per-attendee research from Meeting-Prep ("Who you're meeting") and the Multithreader decision-maker map stays as conversation-level markdown and never lands on the person.

## 3) Designed state

### 3.1 Architecture diagram

```text
  SEED (per account, new + backfill):
    SYSTEM_SUBAGENT_FILENAMES += 'strategist'; buildStrategistContent() seeds
      subagents/strategist.md + a default playbook <ref> in <trigger type="any">
    Strategic fields seeded with ownerAgentId = Strategist; + on_track (new select);
      risk = REUSE native crmConversations.risk (surfaced, not re-created)
    displayConfig.strategicOverview evolves → CATALOG (items + visibleWhen conditions)
                         │
  ┌─ CATALOG (AOP-level: what CAN appear) ───────────────────────────────────────┐
  │  OverviewCatalogItem[] { key, kind:field|doc|contacts, fieldId?/docPath?,     │
  │    render:scalar|markdown, ownerAgentId?, visibleWhen?, section?, order }     │
  └───────────────────────────────┬──────────────────────────────────────────────┘
        instantiateOverview(convId): resolveDealFacts + evaluate visibleWhen
                         ▼
  ┌─ INSTANCE (crmConversations.overviewItems jsonb: what DOES appear here) ──────┐
  │  OverviewInstanceItem[] { key, kind, fieldId?/docPath?, render, order,        │
  │    addedBy:seed|agent|user, hidden? }   ← Strategist & user & chat-agent edit │
  └───────────────────────────────┬──────────────────────────────────────────────┘
                         ▼
  EVENT → Strategist (always-on orchestrator, pipelineOrder 2):
    1. re-instantiate overviewItems for this deal (conditions may have changed)
    2. dispatch enabled module subagents whose runWhen holds (run-subagent)
    3. write assessment doc + distill owned fields (relevance gate: mark an
       irrelevant item hidden:true; unpopulated-but-relevant renders blank)
                         ▼
  RENDER: StrategicOverviewTab reads conversation.overviewItems (fallback: SEEDED
    strategic catalog EMPTY — never a generic-CRM-field dump)
    field(scalar) → badge   field(markdown) → <Markdown>   doc → <DocEmbed docPath>
    contacts → ContactsDirectorySection (roster → links to contact profile doc)
    footer → [Add & configure your overview] (→ config)  [Populate overview] (→ chat
             message that invokes the Strategist for this deal)
                         │
  CONTACTS: contact/{personKey}/profile doc scope (durable research accumulates);
    crmContacts.notes/customFields surfaced in ContactCard; Meeting-Prep/Multithreader
    write per-attendee profiles down into the person doc
                         │
  PERSONALIZE (prompt, not gallery): main chat agent
    configure-strategist tool → mutate catalog items / conditions / owned fields
    manage-subagent tool → create/enable/disable module subagents
```

### 3.2 Step-by-step walkthrough

1. **Seed the Strategist as an always-on system agent** — add `'strategist'` to `SYSTEM_SUBAGENT_FILENAMES` ([aop-agents.ts:176](apps/server/src/services/aop/aop-agents.ts)), a `buildStrategistContent()` builder + entry in the system-subagent array ([seed-playbook.ts:591](apps/server/src/services/playbook/seed-playbook.ts)), and a `<ref>` to it inside the default `<trigger type="any">` in `buildPlaybookTemplate` ([seed-playbook.ts:147](apps/server/src/services/playbook/seed-playbook.ts)). `triggerTypeFromFilename` ([aop-agents.ts:183](apps/server/src/services/aop/aop-agents.ts)) already defaults to `EVENT_OCCURRED`. Backfill via a script mirroring the existing `seed-strategist-agents.ts`. Its instructions gate it ("if there is no new signal, make no changes") so worst-case it is a no-op like the CRM updater.
2. **Seed the two attribute fields** — in `STRATEGIC_OVERVIEW_FIELDS` ([strategic-overview-fields.ts:26](apps/server/src/services/crm/strategic-overview-fields.ts)) add `on_track` (select) and register `risk` as a **cedar** cell (it maps to the native `crmConversations.risk` column, not a custom field). Add both to the top row of the evolved catalog.
   - `on_track` field def:
     ```json
     { "id":"on_track","type":"select","label":"On track","displayOrder":0,
       "options":[{"value":"on_track","label":"On track","color":"#22c55e","enumOrder":0},
                  {"value":"fast","label":"Fast","color":"#3b82f6","enumOrder":1},
                  {"value":"stalling","label":"Stalling","color":"#f59e0b","enumOrder":2},
                  {"value":"cold","label":"Cold","color":"#ef4444","enumOrder":3}],
       "signal":{"enabled":true,"description":"green = On track/Fast; yellow = Stalling; red = Cold."},
       "ownerAgentId":"<strategist id>" }
     ```
3. **Evolve the AOP layout into a catalog** — replace the scalar `StrategicOverviewConfig` with an item catalog carrying conditions + render hints + doc/contacts kinds (see 3.3). A `normalizeStrategicCatalog(config)` migrates a legacy `{topRow, sections}` into `OverviewCatalogItem[]` (each `topRow` cell → a `field` item order 0-4; each `section` field → a `field` item under that section) so existing AOPs keep working with zero data migration.
   - Catalog item examples:
     ```json
     [ {"key":"on_track","kind":"field","fieldId":"on_track","fieldType":"custom","render":"scalar","displayOrder":0},
       {"key":"risk","kind":"field","fieldId":"risk","fieldType":"cedar","render":"scalar","displayOrder":1},
       {"key":"the_play","kind":"field","fieldId":"the_play","fieldType":"custom","render":"markdown","section":"the_play","ownerAgentId":"<strat>"},
       {"key":"battlecard","kind":"doc","docPath":"conversation/{id}/agent-<battlecard>/overview","title":"Battlecard","visibleWhen":{"all":[{"fact":"headcount","op":"gte","value":200}]}},
       {"key":"stakeholders","kind":"contacts","title":"Stakeholders","visibleWhen":{"fact":"headcount","op":"gte","value":75}} ]
     ```
4. **Resolve deal facts** — new `resolveDealFacts(db, conversationId)` in [deal-facts.ts](apps/server/src/services/crm/deal-facts.ts) returns a flat record from the conversation + joined company: `{ status, risk, dealValue, headcount, onTrack, segment, field:<key> }`. `headcount` from `companiesGlobal.linkedinHeadcount`/`employeeCountRange` ([crm-schema.ts:173](apps/server/src/db/crm-schema.ts)); `segment` derived from headcount bands (SMB/mid-market/enterprise) until an explicit column exists.
   - Facts snapshot:
     ```json
     { "status":"negotiation","risk":"high","dealValue":120000,"headcount":420,
       "onTrack":"stalling","segment":"enterprise","field:meddpicc_champion":"Priya" }
     ```
5. **Evaluate conditions (pure)** — new `evaluateVisibleWhen(cond, facts): boolean` in [overview-conditions.ts](apps/server/src/services/crm/overview-conditions.ts): a single `{fact,op,value}` or an `all`/`any` group; ops `eq|neq|gt|gte|lt|lte|in|exists`. Deterministic and unit-testable; missing fact ⇒ `exists`=false, comparisons=false.
6. **Instantiate the per-conversation list** — new `instantiateOverview(db, conversationId)` in [overview-instance.ts](apps/server/src/services/crm/overview-instance.ts): read catalog + `resolveDealFacts`, keep items whose `visibleWhen` holds (or is absent), map to `OverviewInstanceItem` with `addedBy:'seed'`, **merge** onto any existing `crmConversations.overviewItems` (preserving `addedBy:'agent'|'user'` items and `hidden` overrides, keyed by `key`), and persist. Called on conversation create and at the top of every Strategist run.
   - Persisted instance:
     ```json
     [ {"key":"on_track","kind":"field","fieldId":"on_track","order":0,"addedBy":"seed"},
       {"key":"risk","kind":"field","fieldId":"risk","order":1,"addedBy":"seed"},
       {"key":"battlecard","kind":"doc","docPath":"conversation/…/overview","addedBy":"agent"} ]
     ```
7. **Strategist manages the overview (dispatch is optional)** — `STRATEGIST_INSTRUCTIONS` (seeded doc body) directs it to: re-instantiate the overview list, write its assessment doc at `agentOverviewPath`, and distill concise conclusions into owned fields — applying a **relevance gate**: an owned field that is genuinely not relevant to this deal is marked `hidden:true` on its instance item (an explicit suppression), while a relevant-but-not-yet-populated field simply renders blank. This keeps the empty-but-expected fields visible (so the user knows what the Strategist will fill) and only removes what the agent deliberately deemed irrelevant (the agent-decided half of 3a; the declarative `visibleWhen` is the hard half). A single Strategist doing all of this from its own playbook is the default; *optionally*, if the playbook lists module subagents, it may dispatch the ones whose `runWhen` holds via `run-subagent`. The seeded body includes a durable **"How to manage the conversation overview context"** section (how items, conditions, docs, and the instance list work) so the capability is self-documenting and a later edit or reseed does not silently strip it.
8. **Render from the instance, with a strategic (not generic) empty state** — `StrategicOverviewTab` ([StrategicOverviewTab.tsx:424](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) reads `conversationData.overviewItems`, sorts by `order`, and dispatches by `kind`: `field` + `render:'scalar'` → existing `renderStat`/`FieldValue`; `field` + `render:'markdown'` → a new `<MarkdownField>`; `doc` → a new `<DocEmbedCell docPath>`; `contacts` → a new `<ContactsDirectorySection>`. `buildFallbackLayout` ([:47](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) is **removed**: when a conversation has no instance yet, the fallback is the **seeded strategic catalog rendered with empty values** (the same strategic fields, blank), never a dump of generic custom fields. A persistent footer renders two controls regardless of fill state: **"Add & configure your overview"** (navigates to the config editor) and **"Populate overview"** (sends a prefilled chat message — "Populate the strategic overview for this deal" — that the main chat agent handles by invoking the Strategist for this conversation via the existing `spawn-subagent` path). Because the empty cells are always present, the footer is always reachable by scrolling.
9. **Markdown field + doc embed** — `<MarkdownField>` renders a field's string value through the existing markdown renderer used by the document viewer (read-only, `prose`-styled). `<DocEmbedCell>` fetches the doc via the existing `documents.getDoc` query and renders its markdown inline, with a header + a ⧉ open-in-viewer affordance (reusing the `OpenArtifactPanel`/hydrator machinery from [[chat-context-set]]). Both are collapsible sections in the overview.
10. **Contact profile doc scope** — add `contactProfilePath(personKey)` = `contact/{personKey}/profile` + `isContactDocPath` to [convention-paths.ts](apps/server/src/services/documents/convention-paths.ts) and a born-as-contact write-hook branch in [documents/index.ts](apps/server/src/services/documents/index.ts) (mirroring `isSubagentDocPath` at [convention-paths.ts:231](apps/server/src/services/documents/convention-paths.ts)). `personKey` = `crmContacts.personId` when present, else an email slug. Durable *research* is org-scoped (shared); personal *notes* stay per-user on `crmContacts.notes`.
11. **Surface structured contact notes** — add `notes` + `customFields` to the frontend `PersonGlobal` type ([apps/mail/modules/crm/types/index.ts](apps/mail/modules/crm/types/index.ts)) and to the `crm.getConversation` payload; render an editable Notes field + custom attributes in `ContactCard` ([ContactsTab.tsx](apps/mail/modules/conversations/components/contacts/ContactsTab.tsx)), each card linking to its `contact/{personKey}/profile` doc.
12. **Research flows down to the person** — amend the Meeting-Prep ([seed-meeting-prep-agent.ts](apps/server/src/db/migrations/scripts/seed-meeting-prep-agent.ts)) and Multithreader instructions to also upsert a per-attendee section into `contact/{personKey}/profile`, so durable background accumulates once per person and is reused across every deal they appear in.
13. **Prompt-driven personalization** — new chat-agent tools registered in both tool maps of [chat-agent.ts:524](apps/server/src/mastra/agents/chat-agent.ts): `configure-strategist` (read/patch the AOP catalog — add/remove/reorder items, set `render`, set `visibleWhen`, set a field's `ownerAgentId`) wrapping `aop.updateAop`, and `manage-subagent` (create/enable/disable a module subagent) wrapping `aop.createSubagent` ([aop.ts:286](apps/server/src/trpc/routes/aop.ts)) + `aop.updateSubagentHeader` ([aop.ts:424](apps/server/src/trpc/routes/aop.ts)). An AE personalizes by prompting the main agent ("for enterprise deals always show a battlecard doc") which the agent translates into catalog + subagent mutations. The install *gallery/wizard* is deferred.

### 3.3 Schema

Full schema:

```ts
// apps/server/src/db/aop-schema.ts — condition grammar (NEW)
export type OverviewFact =
  | 'status' | 'risk' | 'onTrack' | 'dealValue' | 'headcount' | 'segment'
  | `field:${string}`;                                   // any custom field key
export type OverviewConditionOp =
  | 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'exists';
export type OverviewCondition = {
  fact: OverviewFact;
  op: OverviewConditionOp;
  value?: string | number | Array<string | number>;      // omitted for 'exists'
};
export type VisibleWhen =
  | OverviewCondition
  | { all: OverviewCondition[] }
  | { any: OverviewCondition[] };

// apps/server/src/db/aop-schema.ts — AOP-level CATALOG item (NEW; supersedes StrategicOverviewCell/Section)
export type OverviewItemKind = 'field' | 'doc' | 'contacts';
export type OverviewCatalogItem = {
  key=[redacted];                        // stable id, unique within the catalog
  kind: OverviewItemKind;
  fieldId?: string;                   // kind:'field' — cedar field or custom field key
  fieldType?: 'cedar' | 'custom';     // kind:'field'
  docPath?: string;                   // kind:'doc' — supports {id} placeholder → conversationId
  title?: string;                     // display label / section title
  section?: string;                   // grouping id (e.g. 'the_play','meddpicc'); absent = top row
  render?: 'scalar' | 'markdown';     // NEW — field render mode; default 'scalar'
  displayOrder: number;
  ownerAgentId?: string;              // module that contributes/maintains this item
  visibleWhen?: VisibleWhen;          // NEW — declarative gate; absent = always
  defaultValue?: string;
};
// Evolved config: catalog replaces topRow/sections; both kept optional for one release.
export type StrategicOverviewConfig = {
  items?: OverviewCatalogItem[];      // NEW — the catalog
  topRow?: StrategicOverviewCell[];   // legacy, normalized into items on read
  sections?: StrategicOverviewSection[]; // legacy, normalized into items on read
};

// apps/server/src/db/aop-schema.ts — per-conversation INSTANCE item (NEW)
export type OverviewInstanceItem = {
  key=[redacted];                        // catalog key, or ad-hoc for agent/user adds
  kind: OverviewItemKind;
  fieldId?: string;
  fieldType?: 'cedar' | 'custom';
  docPath?: string;                   // resolved (no placeholder)
  title?: string;
  section?: string;
  render?: 'scalar' | 'markdown';
  displayOrder: number;
  addedBy: 'seed' | 'agent' | 'user'; // NEW — provenance; seed items re-materialize, others persist
  hidden?: boolean;                   // NEW — suppressed by agent/user without deleting
};
```

```sql
-- apps/server/src/db/crm-schema.ts — crmConversations gains the instance list (NEW column)
ALTER TABLE crm_conversations
  ADD COLUMN overview_items jsonb;            -- OverviewInstanceItem[]; NULL ⇒ instantiate lazily
-- risk already exists: crm_conversations.risk text (low|medium|high) — REUSED, not added
-- on_track is a custom field in customFieldDefinitions, NOT a native column

-- apps/server/src/services/crm/strategic-overview-fields.ts — on_track custom field (NEW seed)
--   customFieldDefinitions['on_track'] = { type:'select', options:[On track|Fast|Stalling|Cold], signal }
```

```ts
// apps/server/src/services/documents/convention-paths.ts — contact doc scope (NEW)
export function contactProfilePath(personKey=[redacted] string;   // `contact/${personKey}/profile`
export function isContactDocPath(path: string): boolean;         // /^contact\/[^/]+\/[^/]+$/

// apps/server/src/services/crm/deal-facts.ts (NEW)
export type DealFacts = Record<OverviewFact, string | number | undefined>;
export async function resolveDealFacts(db: Db, conversationId: string): Promise<DealFacts>;

// apps/server/src/services/crm/overview-conditions.ts (NEW, pure)
export function evaluateVisibleWhen(cond: VisibleWhen | undefined, facts: DealFacts): boolean;

// apps/server/src/services/crm/overview-instance.ts (NEW)
export function normalizeStrategicCatalog(config: StrategicOverviewConfig): OverviewCatalogItem[];
export async function instantiateOverview(db: Db, conversationId: string): Promise<OverviewInstanceItem[]>;

// apps/mail/modules/crm/types/index.ts — PersonGlobal gains (NEW, mirrors crmContacts)
//   notes?: string;
//   customFields?: Record<string, unknown>;
```

Relationship diagram:

```text
┌─────────────────────────────────────────────┐        ┌──────────────────────────────┐
│ agentOperatingProcedures                     │        │ aopAgents / subagent docs     │
│  id (PK)                                     │        │  id (PK)                      │
│  displayConfig.strategicOverview ▼           │        │  name (Strategist, module…)   │
│    { items: OverviewCatalogItem[] }          │        │  frontmatter{model,enabled,   │
│      key,kind,fieldId?/docPath?,             │        │    runWhen?, fill_instructions}│
│      render,visibleWhen?,ownerAgentId ───FK──┼───────►│                               │
│  customFieldDefinitions ▼                    │        └───────┬──────────────────────┘
│    Record<key, CustomFieldDefinition>        │                │ agentOverviewPath(conv,agent)
│      { …, ownerAgentId?, signal? }           │                ▼
└──────────────┬───────────────────────────────┘        documents:
     normalizeStrategicCatalog + resolveDealFacts        · conversation/{id}/agent-{agent}/overview
     + evaluateVisibleWhen = instantiateOverview          · contact/{personKey}/profile  ◄── NEW scope
               ▼                                          · organisation/... user/... thread/...
┌─────────────────────────────────────────────┐
│ crmConversations                             │        ┌──────────────────────────────┐
│  id (PK)                                     │──FK────►│ crmCompanyRelationships       │
│  aopId ──FK► agentOperatingProcedures        │ primary │  → companiesGlobal            │
│  status / risk(low|med|high) / dealValue     │ Company │    linkedinHeadcount,         │
│  overview_items ▼  ◄── NEW column            │         │    employeeCountRange (facts) │
│    OverviewInstanceItem[]                    │        └──────────────────────────────┘
│      { key,kind,render,addedBy,hidden? }     │
└──────────────┬───────────────────────────────┘
               │ conversationContacts (role)
               ▼
┌─────────────────────────────────────────────┐        ┌──────────────────────────────┐
│ crmContacts (per-user)                       │──ref──►│ peopleGlobal (enrichment)     │
│  personEmail / personId (personKey)          │ email  │  email (PK), role, summary…   │
│  notes ◄── surfaced   customFields ◄─surfaced│        └──────────────────────────────┘
│  personKey ──► contact/{personKey}/profile doc (durable research accumulates)          │
└─────────────────────────────────────────────┘
```

## 4) Implementation phases

### Phase 1 — Attribute fields: reuse `risk`, seed `on_track`

**Goal:** Every deal shows an `on_track` select and the native `risk` in the overview, seeded idempotently, signal-enabled.

- [x] Add the `on_track` select field def (options On track/Fast/Stalling/Cold + signal) to `STRATEGIC_OVERVIEW_FIELDS` ([strategic-overview-fields.ts:26](apps/server/src/services/crm/strategic-overview-fields.ts)).
- [x] Add `on_track` (custom) and `risk` (cedar) cells to the top row of `STRATEGIC_OVERVIEW_LAYOUT` ([strategic-overview-fields.ts:161](apps/server/src/services/crm/strategic-overview-fields.ts)). **Decision:** widened the default top row to **6 cells** (`nextStepDate, on_track, status, risk, forecast, dealValue`) and dropped `lastContactedAt` — non-lossy for the strategic fields; the catalog rework (Phase 4/5) replaces this layout anyway. Only affects fresh seeds (gap-fill never rewrites an existing layout).
- [x] Render the native `risk` cedar cell in `renderStat` ([StrategicOverviewTab.tsx:459](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) as a signal-colored select (low 🟢 / medium 🟡 / high 🔴), reading `conversation.risk` (new `RISK_OPTIONS` const). Frontend — manual visual check.
- [x] Confirm `computeStrategicSeedGapFill` ([strategic-overview-fields.ts:213](apps/server/src/services/crm/strategic-overview-fields.ts)) gap-fills `on_track` into existing AOPs without clobbering (additive; existing layouts untouched).

**Tests:**

- [x] Unit: `computeStrategicSeedGapFill` adds `on_track` when missing, no-ops on re-run, preserves user edits; `on_track` is a 4-option signal select; top row is 6 cells with `on_track`+`risk` — [seed-strategic.test.ts](apps/server/src/services/crm/__tests__/seed-strategic.test.ts) (8 passed).
- [x] `pnpm exec vitest run src/services/crm/__tests__/seed-strategic.test.ts` → 8 passed.

### Phase 2 — Strategist seeded as an always-on system agent

**Goal:** Every account (new + backfill) has a seeded, dispatchable Strategist that owns the strategic fields.

- [x] Add `'strategist'` to `SYSTEM_SUBAGENT_FILENAMES` ([aop-agents.ts:210](apps/server/src/services/aop/aop-agents.ts)).
- [x] Add `buildStrategistContent()` + a system-subagent array entry ([seed-playbook.ts](apps/server/src/services/playbook/seed-playbook.ts)) with `STRATEGIST_INSTRUCTIONS` ([aop-agents.ts:172](apps/server/src/services/aop/aop-agents.ts)) — assess deep → distill owned fields → relevance gate (hidden vs blank) → optional dispatch → optional recommendations.
- [x] Include a durable **"How to manage the conversation overview context"** section in `STRATEGIST_INSTRUCTIONS` so the capability is self-documenting and survives reseed/edits.
- [x] Add a `<ref>` to `@subagents/strategist` inside the default `<trigger type="any">` in `buildPlaybookTemplate` ([seed-playbook.ts](apps/server/src/services/playbook/seed-playbook.ts)) — placed between crm-updater and next-steps (the orchestrator's fixed order). `buildPlaybookTemplate` exported for unit testing.
- [x] Stamp `ownerAgentId = <strategist id>` via `assignStrategistFieldOwnership` ([strategist-ownership.ts](apps/server/src/services/aop/strategist-ownership.ts)), wired best-effort at the end of `seedPlaybookFiles`. **Divergence:** ownership covers the custom strategic fields **except `forecast`** (rep-owned) and **excludes native `risk`** (a column, not a custom field). `on_track` is included.
- [x] Backfill script `seed-strategist-agents.ts` ([migrations/scripts](apps/server/src/db/migrations/scripts/seed-strategist-agents.ts)) — ownership half, idempotent, jesse-scoped by default (`BACKFILL_ALL=1`). **Divergence:** it does **not** rewrite an already-authored `PLAYBOOK.md`; existing accounts lacking the strategist doc/`<ref>` are reported and left for a separate playbook-ref rollout (new accounts get it from the template; jesse already has it).

**Tests:**

- [x] Live headless drivers (as jesse): `strategist-dispatch-smoke.ts` (regression — `@subagents/strategist` compiled into the playbook, resolves to the row) and new `strategist-ownership-smoke.ts` — Strategist resolves + **11/11 present strategic fields owned** (on_track absent from jesse's live defs until next field-seed), dry-run stamp = 0. Read-only.
- [x] Unit: `strategist-seed.test.ts` (7 passed) — filename set, durable section, ref ordering, `stampStrategistOwnership` (stamps unowned, skips forecast, idempotent). `pnpm exec vitest run src/services/playbook/__tests__/strategist-seed.test.ts`.
- [x] `pnpm deps:check` clean (1216 modules, no circular imports); touched files typecheck clean.

### Phase 3 — Overview condition grammar + deal-facts resolver (pure)

**Goal:** A deterministic, unit-tested condition layer and fact resolver, wired to nothing yet.

- [x] Add `OverviewFact`/`OverviewConditionOp`/`OverviewCondition`/`VisibleWhen` types to [aop-schema.ts](apps/server/src/db/aop-schema.ts).
- [x] Add pure `evaluateVisibleWhen(cond, facts)` + `DealFacts` type + `getFact` in [overview-conditions.ts](apps/server/src/services/crm/overview-conditions.ts) (no DB — deterministically testable). **Note:** `DealFacts` lives in the pure module so the condition evaluator has no DB dependency; `deal-facts.ts` imports the type.
- [x] Add `resolveDealFacts(db, conversationId)` + pure `mapDealFacts`/`segmentFromHeadcount`/`parseHeadcountRange` in [deal-facts.ts](apps/server/src/services/crm/deal-facts.ts) — native columns off `crm_conversations`, headcount via `primaryCompanyId → crm_company_relationships → companies_global`, custom values via `getConversationFieldValues`. Segment banded smb (<100) / mid-market (<1000) / enterprise.

**Tests:**

- [x] Unit: `evaluateVisibleWhen` — each op, `all`/`any`, `field:<key>`, missing-fact falsy; `mapDealFacts`/segment banding/range parsing — [overview-conditions.test.ts](apps/server/src/services/crm/__tests__/overview-conditions.test.ts) (8 passed).
- [x] Live: `deal-facts-smoke.ts` (as jesse, read-only) — resolved real facts for "Peter @ Pirros" (headcount 43 → segment smb, custom values mapped); `evaluateVisibleWhen` ran (headcount>=75 → false, correct).
- [x] `pnpm exec vitest run src/services/crm/__tests__/overview-conditions.test.ts` → 8 passed; touched files typecheck clean.

### Phase 4 — Catalog model + per-conversation instantiation

**Goal:** The AOP layout becomes an item catalog; each conversation gets a materialized `overview_items` list.

- [x] Add `OverviewItemKind`/`OverviewCatalogItem` + `OverviewInstanceItem` types and evolve `StrategicOverviewConfig` to `items?` (topRow/sections now optional, normalized on read) ([aop-schema.ts](apps/server/src/db/aop-schema.ts)).
- [x] Add `overview_items jsonb` column to `crm_conversations` ([crm-schema.ts:490](apps/server/src/db/crm-schema.ts)) + migration `0051_conversation_overview_items.sql` + journal entry; **applied to the live DB** (idempotent `ADD COLUMN IF NOT EXISTS`, verified present as jsonb).
- [x] Add `normalizeStrategicCatalog(config)` (legacy topRow/sections → items) + pure `computeOverviewInstance(catalog, facts, existing, convId)` + `instantiateOverview(db, convId)` (materialize + persist) + `getOverviewItems` (lazy read) in [overview-instance.ts](apps/server/src/services/crm/overview-instance.ts).
- [x] Expose via a dedicated `crm.getOverviewItems({ conversationId })` query ([crm.ts](apps/server/src/trpc/routes/crm.ts)) with an access guard. **Divergence:** a dedicated query rather than bloating the strict `getConversation` output schema + hot path (the render reads it the same way). Lazy-instantiates on first read.

**Tests:**

- [x] Unit: `normalizeStrategicCatalog` maps a legacy layout; `computeOverviewInstance` keeps condition-passing seed items, preserves `addedBy:'agent'|'user'`, carries a prior `hidden` override, resolves the `{id}` doc placeholder, drops condition-failing items — [overview-instance.test.ts](apps/server/src/services/crm/__tests__/overview-instance.test.ts) (6 passed).
- [x] Live: `overview-instance-smoke.ts` (jesse) — materialized **16 items** for "Peter @ Pirros" from the normalized catalog, restored `overview_items` afterward (no residue).
- [x] Live tRPC e2e: `overview-items-trpc.e2e.test.ts` (gated `E2E_OVERVIEW=1`, `createCaller` as jesse) — route returns items + rejects an unknown conversation (access guard), restored. (createCaller runs under vitest, not raw tsx — css-sanitizer ESM quirk.)
- [x] `pnpm exec vitest run src/services/crm/__tests__/overview-instance.test.ts` (6) + `pnpm deps:check` clean (1221 modules); touched files typecheck clean (crm.ts pre-existing errors unrelated).

### Phase 5 — Render from the instance + markdown fields + doc embeds + strategic empty state

**Goal:** The overview renders the per-conversation instance (markdown fields + inline doc embeds), shows the seeded strategic fields empty when unfilled (never a generic-CRM dump), and always exposes configure/populate controls.

- [x] **Empty state fixed** — `buildFallbackLayout` ([StrategicOverviewTab.tsx](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.tsx)) no longer dumps every custom field; the no-layout fallback is the **strategic fields** (top row `nextStepDate/on_track/status/risk/forecast/dealValue` + the_play/how_we_win/risks/MEDDPICC sections), rendered blank. `isInternalFieldId` (now unused) deleted. **Refinement (per feedback):** every strategic field renders **even when the AOP has no def for it** — `STRATEGIC_FIELD_FALLBACK_DEFS` is merged *under* the AOP's own defs (real defs win), giving each field a label + type so the render (which drops def-less fields) shows it blank instead of omitting it.
- [x] Fetch the per-conversation instance via `crm.getOverviewItems` (new `useTRPC`+`useQuery`), and render `kind:'doc'` items as inline embeds — new [OverviewDocEmbed.tsx](apps/mail/modules/conversations/components/strategicOverview/OverviewDocEmbed.tsx) (fetch `documents.getDoc` by path → `ReadOnlyMarkdownView`, collapsible, ⧉ open-in-viewer, graceful degrade to an open link).
- [x] Persistent footer with **"Add & configure your overview"** (→ `editOverview` = `/agents/playbook?aop=<id>`) and **"Populate overview"** (prefills a chat prompt + `sendMessage` on a fresh thread — the same store pattern `AgentRow` uses to run an agent).
- [x] **Divergence — native `risk` is now read-only** in the top row (the conversation-update mutation's input type doesn't accept `risk`); it shows a signal-coloured label + dot. Corrects a Phase-1 type error (I'd only typechecked the server then).
- [ ] **Deferred (needs visual verification):** full replacement of the layout-based render with the item-model render, and a per-field `render:'markdown'` `<MarkdownField>`. The current render keeps the working scalar path + additively renders doc embeds; the empty-state + footer + doc-embed asks are met. Markdown rendering ships via `OverviewDocEmbed`/`ReadOnlyMarkdownView`.

**Tests:**

- [x] Frontend component test ([StrategicOverviewTab.test.tsx](apps/mail/modules/conversations/components/strategicOverview/StrategicOverviewTab.test.tsx)) — top-row labels, green SignalDot, and the always-present footer controls. **Also repaired a pre-existing red test** (it never mocked `useAgentsForConversation` → `aopAgents.listForAop`, broken since Phase 13); added deep-proxy tRPC + react-query mocks. 3 passed.
- [x] `pnpm exec jest modules/conversations/components/strategicOverview/StrategicOverviewTab` → 3 passed; touched non-test files typecheck clean.
- [ ] **Visual verification by the user** — open a deal's Strategic Overview: empty state shows strategic fields blank (not generic CRM), footer controls present, a doc-embed renders when the instance has a `doc` item.

### Phase 6 — Contact profile doc scope + surface structured notes

**Goal:** Individual contacts get a durable profile doc and their existing notes/customFields become visible.

- [x] Add `contactProfilePath(personKey)` + `isContactDocPath` + `contactKeyFromEmail` to [convention-paths.ts](apps/server/src/services/documents/convention-paths.ts). **Divergence:** no born-as-contact write-hook / new document type — a contact profile is a plain markdown `document`; the **path is the scope**. A doc type would only earn its keep with structured rendering (Phase 20's agent doc), which contacts don't need. This is the load-bearing enabler for Phase 7.
- [ ] **Deferred (manual-test + hot-path):** `notes`/`customFields` on `PersonGlobal` + the `crm.getConversation` contact payload. The service returns `people` strictly typed `peopleGlobal.$inferSelect[]` and validated by `HydratedConversationSchema`; joining `crmContacts.notes` cascades through the output schema + every consumer — a hot-path change with UI-only verification. Split out so the doc-scope enabler ships verified.
- [ ] **Deferred (manual-test):** editable Notes + link-to-profile in `ContactCard` ([ContactsTab.tsx](apps/mail/modules/conversations/components/contacts/ContactsTab.tsx)) and a `contacts`-kind `ContactsDirectorySection`. Depend on the payload above.

**Tests:**

- [x] Unit: `contactProfilePath`/`isContactDocPath`/`contactKeyFromEmail` — [contact-paths.test.ts](apps/server/src/services/documents/__tests__/contact-paths.test.ts) (3 passed).
- [x] Live: `contact-doc-smoke.ts` (as jesse) — write → read (content round-trips) → delete a `contact/{key}/profile` doc, no residue. PASS.
- [x] `pnpm exec vitest run src/services/documents/__tests__/contact-paths.test.ts` → 3 passed.

### Phase 7 — Research flows down to the contact

**Goal:** Meeting-Prep and Multithreader accumulate per-attendee research on the person, not just the conversation.

- [x] Amend `MEETING_PREP_INSTRUCTIONS` ([aop-agents.ts:19](apps/server/src/services/aop/aop-agents.ts)) with **Step 1b** — persist each attendee's durable background to `contact/{personKey}/profile` (via write-document), keyed by email slug when no stable id. **Note:** the instruction body lives in `MEETING_PREP_INSTRUCTIONS` (seeded into `meeting-prep.md`), not the older `seed-meeting-prep-agent.ts` migration.
- [x] Amend the Multithreader template instructions ([AgentsEditor.tsx:606](apps/mail/modules/aop/components/AgentsEditor.tsx)) to write each stakeholder's durable background into the same per-contact profile.
- [x] Idempotent re-write: instructions require READ → rewrite the `## Research` section **in place** (never a blind append), preserving `## Notes`/other sections; deal-specific stance stays in the deal, not the person profile.

**Tests:**

- [x] Unit: `research-flowdown.test.ts` — the seeded Meeting-Prep instructions carry the `contact/{personKey}/profile` directive + the idempotent-in-place rule (2 passed). Proves the directive shipped.
- [ ] **Deferred (sandbox LLM block):** live dispatch of Meeting-Prep to observe the agent actually writing the contact doc — the LLM/skill run can't be imported under tsx (css-sanitizer ESM quirk), same limitation as the chat e2e. The doc-write path itself is proven by Phase 6's `contact-doc-smoke`. Existing accounts pick up the new instruction text on next meeting-prep reseed; new accounts get it from the template.

### Phase 8 — Prompt-driven personalization tools (chat agent)

**Goal:** An AE personalizes the Strategist by prompting the main chat agent, which mutates the catalog and module subagents.

- [x] Add pure `applyCatalogPatch(catalog, ops)` + DB-facing `patchStrategicCatalog(db, aopId, ops)` ([overview-instance.ts](apps/server/src/services/crm/overview-instance.ts)) — ops: `add` (upsert by key), `remove`, `setVisibleWhen` (null clears), `setRender`, `setOwner` (null clears), `reorder`; persists `displayConfig.strategicOverview.items`.
- [x] Add `configure-strategist` tool ([configureStrategistTool.ts](apps/server/src/mastra/tools/config/configureStrategistTool.ts)) wrapping `patchStrategicCatalog`; input `{ aopId?, ops[] }` (aopId falls back to the conversation context). Rich description maps user asks ("show a battlecard for enterprise deals", "hide Forecast") → ops.
- [x] Register `configure-strategist` in **both** tool maps of [chat-agent.ts:530](apps/server/src/mastra/agents/chat-agent.ts),[555](apps/server/src/mastra/agents/chat-agent.ts).
- [ ] **Deferred — `manage-subagent`** (create/enable/disable a module subagent). `createSubagent`/`updateSubagentHeader` are tRPC-route-bound flows (slug + doc write + playbook `<ref>` insertion); exposing them cleanly as a chat tool is its own change. `configure-strategist` already delivers the core personalization (fields, embedded docs, conditions, render, ownership on the catalog). Enabling/disabling an existing subagent is also achievable today via its doc frontmatter.

**Tests:**

- [x] Unit: `applyCatalogPatch` — upsert-by-key, remove, set visibleWhen/render/owner (+ null-clear), unknown-key no-op, sorted — [overview-instance.test.ts](apps/server/src/services/crm/__tests__/overview-instance.test.ts) (10 passed total).
- [x] Live: `configure-strategist-smoke.ts` (as jesse) — patch adds an always-visible `smoke_doc` (→ materializes in the instance, `{id}` resolved) and a `headcount>=999999`-gated item (→ correctly absent); AOP `displayConfig` + conversation `overview_items` fully restored (no residue). PASS.
- [x] `pnpm exec vitest run src/services/crm/__tests__/overview-instance.test.ts` (10) + `pnpm deps:check` clean (1224 modules); touched files typecheck clean.