brain-implementation-context.md34.9 KBView on GitHub
# Brain / Wiki System — Full Technical Context

> Written 2026-05-25. Comprehensive reference for future agent sessions.

---

## What "the brain" is

The brain is Cedar's sales intelligence layer: a persistent, incrementally-built wiki that stores extracted patterns from past deals (objections, techniques, messaging, personas, etc.), a product knowledge base, and per-rep sales process playbooks. It is populated by a backfill pipeline and queried at runtime by chat and event agents.

Three layers:

```
wiki/                   evidence layer — extracted patterns, observations, source citations
    ↓ synthesis
playbooks/              specification layer — the living sales process per rep
    ↓ runtime reference
aop_agents / executor   plumbing layer — Cedar automations that read the playbook at runtime
```

---

## Document store paths

All documents live in the `documents` table (Postgres). Key paths:

| Path | What it is |
|---|---|
| `organisation/wiki/` | Wiki root — all extracted wiki pages |
| `organisation/wiki/_candidates/{category}/{slug}` | Single-observation staging area — not yet validated |
| `organisation/wiki/_proposals/{category}/{slug}` | Human-proposed entries (not yet used) |
| `organisation/wiki/{category}/{slug}` | Published wiki pages |
| `organisation/wiki/_index` | Auto-maintained index of all wiki pages |
| `organisation/wiki/_log` | Append-only log of all wiki writes |
| `organisation/knowledge-base/product-reference` | Auto-compiled product KB doc |
| `organisation/playbooks/` | Playbooks root folder |
| `organisation/playbooks/process/rep-{userId}` | Per-rep sales process doc |
| `organisation/wiki-legacy/` | Old hand-curated wiki pages (read-only, not extraction target) |

Path constants: `apps/server/src/services/document-store/convention-paths.ts`

Key constants:
```ts
ORG_WIKI_PATH = 'organisation/wiki'
ORG_KB_PATH = 'organisation/knowledge-base'
```

Path shortcuts (used in `readDocumentTool` and other tools):
```
#wiki      → organisation/wiki/
#kb        → organisation/knowledge-base/
#files     → organisation/files/
#notes     → user/notes/
#here      → conversation/{conversationId}/   (requires conversation context)
```

Source: `apps/server/src/services/document-store/path-shortcuts.ts`

---

## Wiki categories

Defined in `apps/server/src/services/wiki/constants.ts` — `WIKI_CATEGORIES` array.

| Slug | Title | Has playbooks |
|---|---|---|
| `techniques` | Techniques | yes |
| `objections` | Objections | yes |
| `competitors` | Competitors | yes |
| `stages` | Stages | yes |
| `personas` | Personas | yes |
| `product` | Product | yes |
| `customers` | Customers | yes |
| `messaging` | Messaging | yes |
| `process` | Process | no — synthesis only, never extracted from individual conversations |

Each category has:
- `extractionGuidance` — what to look for when extracting from a conversation
- `qualityBar` — minimum criteria for an entry to be worth keeping
- `pageEntryTemplate` — the `###` block format for each example in this category
- `pageTemplate` — the initial `##` section structure for a new page of this type

Special folders:
```ts
WIKI_SPECIAL_FOLDERS = {
  ROOT: 'wiki',
  PROPOSALS: '_proposals',
  CANDIDATES: '_candidates',
}
WIKI_SPECIAL_FILES = {
  INDEX: '_index.md',
  LOG: '_log.md',
}
```

---

## Wiki page format

### Storage model

New-format pages split content from metadata:
- `documents.metadata` (JSONB column) — frontmatter fields
- `documents.content` (text column) — body-only markdown, no `---yaml---` header

Agents always write/read the combined `---yaml---\nbody` format. The `writeWikiPageTool` and `readWikiPageTool` transparently split/merge on the way in and out.

Legacy pages (wiki-legacy) have frontmatter embedded in the content string — tools detect this via `isWikiMetadata(doc.metadata)` check and fall back to parsing the content.

### Frontmatter fields (full spec)

```ts
interface WikiFrontmatter {
  type?: string;            // 'technique' | 'objection' | 'competitor' | 'stage' | 'persona' |
                            // 'feature' | 'testimonial' | 'case_study' | 'messaging' | 'process'
  title?: string;           // human-readable page title
  summary?: string;         // one-line description (also stored as documents.description)
  keywords?: string[];      // search keywords
  source?: string;          // 'extracted' | 'curated' | 'hybrid'
  maturity?: string;        // 'draft' | 'validated' | 'core'
  status?: 'candidate' | 'published';
  created?: string;         // YYYY-MM-DD
  updated?: string;         // YYYY-MM-DD
  createdBy?: string;       // userId of human who created (for curated pages)
  owner?: string;           // userId of primary owner

  // Candidate-only fields
  candidateCount?: number;  // server-incremented on each write; auto-promotes at ≥2
  firstSeen?: string;       // YYYY-MM-DD of first extraction

  // Stats (updated by analysis agents, not extraction agent)
  stats?: {
    mentions?: number;
    inWon?: number;
    inLost?: number;
    winRate?: number;       // 0–1
    computedAt?: string;
    computedFrom?: string;
  };

  // Invalidation
  invalidAt?: string;       // YYYY-MM-DD — page is stale after this date; excluded from listings

  // Contradiction tracking
  conflictCount?: number;   // count of [[contradicts:]] markers in body
  needsReview?: boolean;    // set to true when conflictCount > 0

  // Misc
  related?: string[];       // paths to related pages
  derivedFrom?: string[];   // source paths for synthesised pages
  lastVerified?: string;    // YYYY-MM-DD
  sourceQuery?: { ... };    // how this page was generated (for proposals)
}
```

### Body format — example entries

Each example is a `###` block. The `[YYYY-MM-DD]` header is the extraction date (not the event date). References go on their own lines, one per line:

```markdown
### [YYYY-MM-DD]
[[user: <cedar-uuid>]]
[[conversation: <conv-uuid>]]
[[event: <event-id>]]
[[field: stage/{stageAtTime}]]
sourceEvents: [eventId1, eventId2]

> "verbatim quote from conversation"

**Immediate reaction:** {what happened next — observable, not interpreted}
```

For objections/competitors, use two-voice format:
```markdown
> **Prospect:** "their exact words"
> **Rep:** "the specific counter-move"

**Immediate reaction:** {prospect's observable reaction}
```

When 2+ distinct reactions accumulate across examples, add a reactions summary:
```markdown
**Reactions observed (3 deals):**
- Prospect agreed to pilot → won (2 instances)
- Prospect asked for more references → open (1 instance)
```

### The "What It Sounds Like" section

Techniques and messaging pages include a `## What It Sounds Like` section at the top. This is ONLY verbatim phrasings and variations observed — no interpretation, no "when to use", no conditions:

```markdown
## What It Sounds Like

_Verbatim phrasings and variations observed._

> "We give AEs at companies like Cursor, Warp, and Mintlify..."
> "I want to make sure we've addressed the integration question before..."

## Examples

_Extraction agent appends examples here._
```

The extraction agent is explicitly prohibited from adding "When to use" or "The Move" subsections — those were removed from the format in a previous iteration because they were consistently AI slop.

---

## The candidate system

### Purpose

The candidate system is a two-observation validation gate. A pattern extracted from a single conversation might be noise. The wiki promotes entries only after the same pattern appears in a second independent deal.

### How it works

1. Extraction agent writes to `_candidates/{category}/{slug}` using `mode: 'candidate'` in `writeWikiPageTool`
2. On first write: creates the page with `candidateCount: 1`, `status: 'candidate'`, `maturity: 'draft'`
3. On second write to same path: `candidateCount` is incremented **server-side** from stored metadata (agents cannot fake the count). When `candidateCount >= 2`, the tool automatically:
   - Strips `candidateCount` and `firstSeen` from frontmatter
   - Sets `status: 'published'`, `maturity: 'draft'`
   - Writes to the promoted path `wiki/{category}/{slug}`
   - Deletes the candidate page
   - Updates `_index` and `_log` with a `promote` entry
   - Returns `promoted: true` and `promotedPath` to the agent

**Why server-side increment matters**: agents write the full frontmatter in their content. If `candidateCount` were read from the agent's submitted content, an agent could submit `candidateCount: 10` on the first write and bypass the gate. The tool reads the stored count from `documents.metadata` and ignores whatever the agent submitted.

### Path routing

The tool handles two input formats for candidates:
- `path: "techniques/pricing-anchoring"` with `mode: "candidate"` → routed to `_candidates/techniques/pricing-anchoring`
- `path: "_candidates/techniques/pricing-anchoring"` → used as-is (already prefixed)

The full DB path is `organisation/wiki/_candidates/{category}/{slug}`.

### Manual promotion

`promote-wiki-candidate` tool accepts:
- `_candidates/category/slug` (relative to wiki root)
- `organisation/wiki/_candidates/category/slug` (full path)

It strips candidate fields, sets `status: 'published'`, `maturity: 'draft'`, writes to the promoted path, and deletes the candidate.

**Bug fixed 2026-05-25**: `list-wiki-pages` was returning `organisation/_candidates/...` (with `wiki/` stripped from the middle) instead of `_candidates/...` (wiki-relative). This caused both `read-wiki-page` and `promote-wiki-candidate` to return `found: false`. Fixed by replacing `path.replace('wiki/', '')` with `startsWith(orgWikiPrefix) ? path.slice(orgWikiPrefix.length) : path`.

---

## The invalidation system

### Purpose

Wiki pages become stale as the product evolves or sales processes change. The `invalidAt` frontmatter field is a forward-looking expiry date.

### How it works

- `retractWikiPageTool` sets `invalidAt: YYYY-MM-DD` on the target page's metadata
- `listWikiPagesTool` filters out pages where `frontmatter.invalidAt` is set (regardless of date — any set value causes exclusion)
- `grepWikiTool` respects the same filter
- Stale pages still exist in the DB — they are not deleted, just excluded from listings

This design allows:
- Rolling invalidation schedules (e.g., "this technique is only valid through Q2 2026")
- Manual invalidation via chat: "retract the pricing anchoring page"
- Audit trail — the page and its evidence remain queryable

The `readWikiPageTool` does NOT filter on `invalidAt` — it reads any page directly by path, allowing humans to review stale pages.

---

## Contradiction detection

When the extraction agent identifies content that contradicts an existing page, it can embed `[[contradicts: {path}]]` markers in the body. `writeWikiPageTool` detects these:

```ts
function countContradictions(content: string): number {
  return (content.match(/\[\[contradicts:/g) ?? []).length;
}
```

If any contradictions are found, `conflictCount` and `needsReview: true` are injected into the frontmatter metadata. This surfaces the page in any query filtering for `needsReview === true`.

---

## `_index` and `_log` — race condition fixes

### `_log` — atomic append

The `_log` document is an append-only change log. With concurrent backfill runs processing multiple conversations simultaneously, naive read-modify-write would cause duplicate entries.

**Fix**: Uses a raw SQL `content || $entry` update — a single atomic DB operation with no read:
```sql
UPDATE documents
SET content = content || $logEntry,
    updated_at = NOW()
WHERE id = $logNodeId
```

This is safe for concurrent writers; each appends atomically.

### `_index` — advisory lock

The `_index` document is a full rebuild from all current wiki pages (not append-only). If two concurrent writes both trigger a rebuild, they can overwrite each other's result.

**Fix**: Uses a Postgres session-level advisory lock:
```sql
SELECT pg_try_advisory_lock(hashtext('wiki-index-rebuild')::bigint) AS acquired
```

If the lock is already held by another connection, the current call skips the index rebuild (the other call will complete it). The lock is released automatically when `lockConn.end()` is called.

The index format:
```markdown
# Wiki Index

_Auto-generated. Last updated: 2026-05-25T..._

## Candidates
- [[doc: uuid]] [draft] (updated: 2026-05-25)

## Messaging
- [[doc: uuid]] [draft] (updated: 2026-05-25)

## Techniques
- [[doc: uuid]] [validated] (updated: 2026-05-20)
```

Index uses `[[doc: uuid]]` references (document IDs) rather than paths, so they remain stable if pages are moved.

---

## Frontmatter parsing

`apps/server/src/services/wiki/frontmatter.ts`

Key exports:
- `parseFrontmatter(content)` — splits `---yaml---\nbody`, returns `{ frontmatter, body }`
- `serializeFrontmatter(frontmatter)` — back to `---yaml---` string
- `parseExamples(body)` — extracts structured example metadata from `### [YYYY-MM-DD]` blocks
- `formatExample(input)` — generates a properly formatted example block
- `appendExampleToBody(body, example)` — finds the `## Examples` section and appends
- `validateFrontmatter(frontmatter)` — checks `type` and `maturity` against known constants

### `parseExamples` — what it extracts per example

For each `### [YYYY-MM-DD]` block, it scans the next 5 lines for:
- `[[user: id]]` or `[[userId: id]]` (handles both formats)
- `[[conversation: id]]`
- `[[event: id]]`
- `[[person: id]]`
- `[[stageAtTime: slug]]` or `[[field: stage/slug]]` (both formats recognized)

This is used by `listWikiPagesTool` to count `exampleCount` per page.

### Format detection

`isWikiMetadata(doc.metadata)` checks for presence of `type`, `maturity`, or `title` in the metadata JSONB column to distinguish new-format pages (metadata in column) from legacy pages (frontmatter embedded in content).

---

## Entity resolution

`apps/server/src/services/wiki/entity-resolver.ts`

When `readWikiPageTool` returns a wiki page, all `[[type: id]]` references in the content are resolved to human-readable labels and returned as a separate `resolvedContext` block.

### Supported reference types

```ts
CEDAR_DOC_REFERENCE_TYPE = {
  CONVERSATION: 'conversation',   // [[conversation: uuid]] → deal name, company, status
  EVENT: 'event',                 // [[event: id]] → event title, type, date
  PERSON: 'person',               // [[person: id]] → contact name, title
  USER: 'user',                   // [[user: uuid]] → user name, email — added 2026-05
  FIELD: 'field',                 // [[field: stage/slug]] → stage display label — added 2026-05
}
```

Source: `apps/server/src/services/document-store/document-types.ts`

### `sourceEvents` resolution

Separately, `parseSourceEventsFromContent` scans for `sourceEvents: [id1, id2, ...]` lines in the body and resolves each to deal win/loss outcome. `readWikiPageTool` returns a `sourceEventsContext` block:

```
## Source Events Context

### [2026-05-21] — social-proof messaging
3 source events → 2 won, 1 open
```

### Frontend TipTap pill rendering

All reference types render as interactive, clickable pills in the document editor:

| Reference | Extension file | Format recognized |
|---|---|---|
| `[[conversation: uuid]]` | `ConversationNode.tsx` (agentCanvas) | Both `@[uuid]` and `[[conversation: uuid]]` |
| `[[event: id]]` | `EventRefNode.tsx` (tiptap-extensions) | `[[event: id]]` |
| `[[user: uuid]]` | `UserRefNode.tsx` (tiptap-extensions) | `[[user: uuid]]` — new |
| `[[field: stage/slug]]` | `FieldRefNode.tsx` (tiptap-extensions) | `[[field: stage/slug]]` — new |

Extensions registered in `apps/mail/components/document.tsx` (`composedExtensions`) for global document rendering, and in `apps/mail/modules/company/components/CompanyExplorer.tsx`.

---

## Content shortcuts

`apps/server/src/services/document-store/content-shortcuts.ts`

Used in `readDocumentTool` via the `content` input field. The `content` field of the tool is optional; when present, it is passed through `parseContentShortcut(content, docContent)` before returning.

| Shortcut | What it returns |
|---|---|
| `#sections` | List of `## ` section headers only (no body content) |
| `#section/SectionName` | Full content of the named `## SectionName` section, matched case-insensitively |
| `#outline` | Every `## ` section header + its content up to the first `### Template:` subsection — strips template bodies, returns structural facts only |

### `#outline` use case

Playbook docs use `### Template: {name}` as the prefix for email template subsections. `#outline` strips everything after the first `### Template:` within each stage section, giving a compact structural view:

```
## Stage: 3_pilot_won — Pilot Won

**Entry:** OF signed
**Exit:** Kickoff call scheduled
**Duration:** 1–2 days
**Typical touchpoints:** OF response (5 min), kickoff scheduling email (same day)

## Stage: 4_pilot_inflight — Pilot In-Flight
...
```

This is designed for the orchestrator to read efficiently (~500 tokens instead of ~8k) to decide what to draft.

---

## Wiki extraction skill

`apps/server/.claude/skills/wiki-extraction/SKILL.md`

Used by the `wikiExtractionAgent` during brain backfill. The skill is not loaded via `load-skill` — it is embedded in the agent's system instructions via the skill registry at startup.

### Key rules

**Category routing** (priority order):
1. `objections/` — if prospect pushed back with a specific objection + rep had a counter-move
2. `techniques/` — if rep used a specific replicable move that advanced the deal
3. `competitors/` — if competitor was named + rep positioned against it
4. `stages/` — if something was characteristic of a specific deal stage
5. `personas/` — if behavior reveals how this type of buyer thinks/decides
6. `product/` — if a feature or value prop resonated or confused
7. `customers/` — if a specific customer outcome or testimonial was referenced
8. `messaging/` — if language framing was distinctive and proved effective

**Candidates-first rule**: ALWAYS write to `_candidates/` first. Never write directly to the main wiki path. Only `promote-wiki-candidate` can write to `wiki/{category}/` — not `write-wiki-page` with a direct path.

**Quality bars** (from `WIKI_CATEGORIES[].qualityBar`):
- Techniques: must have rep's actual words AND observable prospect reaction. "Built rapport" = skip.
- Objections: must have BOTH exact objection words AND specific counter-move. "Acknowledged" = skip.
- Competitors: must name the competitor AND have rep positioning response.
- Messaging: must have evidence the language landed (reaction, forwarded, quoted back).
- Product: must name specific feature + prospect reaction in their own words.

**Prohibited content** (AI slop):
- No interpretation ("this shows that...", "this suggests...")
- No conditions ("use this when...", "works best if...")
- No commentary on why something worked
- No "When to use" or "The Move" sections
- No AI-generated summary paragraphs
- Only: what happened, verbatim quotes, observable reactions

**Reference format rules**:
- `[[user: <UUID>]]` — Cedar `users.id` UUID (passed in extraction prompt, NOT email)
- `[[conversation: <conv-uuid>]]` — Cedar conversation UUID
- `[[event: <event-id>]]` — Cedar event ID (from CRM events table)
- `[[field: stage/{stageAtTime}]]` — stage slug at time of event
- Each ref on its own line

**Dedup rules**:
- Same event + same quote = do NOT extract twice to different pages
- Before writing to `product-reference` KB: call `search-documents` first; if exact quote or fact exists, skip
- `product-reference` KB: always use `patch`/`append` mode, never `upsert` (would overwrite entire doc)

**`stageAtTime`**: The stage the deal was in at the time of the event. Comes from `crm_events.stageAtTime` (if populated) or inferred from `crm_conversation_updates` for the conversation at the event's `occurred_at`.

---

## Brain backfill pipeline

### Step Functions steps

`apps/server/src/workflows/step-registry/conversation-sync/brain-backfill-steps.ts`

Three steps:

**`brainBackfillPrepare`**:
- Calls `seedWikiStructure(orgId)` — idempotent, creates all folders/index/log/product-reference if missing
- Queries `crm_conversations` for the user's deals (filtered by deal AOP)
- Applies stage filters and CRM-synced filter from input
- Returns batched conversation IDs

**`brainBackfillBatch`**:
- For each conversation ID in the batch:
  - Checks `agent_executions` for existing `source: 'brain'` + `status: 'completed'` (idempotency skip unless `forceReExtract: true`)
  - Inserts `agent_executions` row with `source: 'brain'`, `status: 'executing'`
  - Runs `wikiExtractionAgent.generate()` with `model: 'anthropic/claude-haiku-4-5'`, `maxSteps: 15`
  - Updates `agent_executions` to `completed` or `failed`

Extraction prompt per conversation:
```
{extractionScope} Use fetch-conversation with showStageChanges: true on conversationId: {id}. Org ID: {orgId}. The Cedar rep user ID for this conversation is: {userId} — use this exact value for all [[user: {userId}]] references in wiki entries, not the email address.
```

`extractionScope` is one of:
- "Extract all sales patterns from this conversation to the wiki AND update the product reference KB doc."
- "Extract sales patterns from this conversation to the wiki only. Do NOT write to the product reference KB doc."
- "Extract product/company facts from this conversation to the product reference KB doc ONLY. Do NOT write to the wiki."

**`brainBackfillSynthesize`**:
- Selects closed deals for the user (filtered by `wonStage`/`lostStage` params or first won/lost stage in AOP)
- Runs synthesis agent with `model: anthropic/claude-sonnet-5` on selected deals
- Writes output to `playbooks/process/rep-{userId}`

**Bug fixed 2026-05-25**: Extraction prompt now includes `userId` explicitly. Previously agents would see the rep's email in conversation content and use that for `[[user:]]` references instead of the Cedar UUID.

### Idempotency

The backfill checks `agent_executions` before processing each conversation:
```ts
const existing = await db.query.agentExecutions.findFirst({
  where: and(
    eq(agentExecutions.conversationId, conversationId),
    eq(agentExecutions.source, 'brain'),
    eq(agentExecutions.status, 'completed'),
  ),
});
if (existing) { processed++; continue; }
```

Override with `forceReExtract: true` in the playground UI.

### Playground UI

`apps/mail/app/(routes)/playground/brain.tsx`

- User selector (admin can run for any user in same org)
- Deal list: scrollable, filterable by CRM synced, stage pills, search bar
- Step checkboxes: `runWikiExtraction`, `runKBExtraction`, `runSynthesis`
- `forceReExtract` toggle
- Live progress polling (queries `agent_executions` for `source: 'brain'` runs)
- "Delete Wiki" button (admin, deletes all wiki docs for org)

### tRPC routes

`apps/server/src/trpc/routes/wiki.ts`:
- `listDealsForUser` — `crm_synced` filter via `integration_metadata @> '[{"type":"external_crm"}]'`
- `deleteWiki` — org-level admin operation, auth checked at org level
- `getStats` — wiki statistics, filters `documentType !== 'folder'`

---

## Wiki seeding

`apps/server/src/services/wiki/seed.ts` — `seedWikiStructure(orgId)`

Idempotent via `onConflictDoNothing()` for all folder and file creates. On conflict, fetches the existing node and continues.

Creates:
- `organisation/wiki/` root folder
- Category folders: `techniques/`, `objections/`, `competitors/`, `stages/`, `personas/`, `product/`, `customers/`, `messaging/`, `process/`
- `_candidates/` root folder + all category subfolders
- `organisation/wiki/_index` doc
- `organisation/wiki/_log` doc
- `organisation/knowledge-base/product-reference` doc (initial content: single-line placeholder)

Called at the start of every `brainBackfillPrepare` run. Safe to call repeatedly.

---

## Wiki tools (Mastra)

All in `apps/server/src/mastra/tools/wiki/`:

### `list-wiki-pages`

Lists pages with frontmatter stats. `includeCandidates: true` shows `_candidates/` pages. `ownerFilter: [userId]` returns only pages with at least one example from those users.

**Returns paths relative to wiki root** — e.g., `_candidates/messaging/slug`, `techniques/pricing-anchoring`. NOT `organisation/wiki/...`.

**Bug fixed 2026-05-25**: Was returning `organisation/_candidates/...` due to `path.replace('wiki/', '')` stripping the interior occurrence. Fixed to `startsWith('organisation/wiki/') ? path.slice('organisation/wiki/'.length) : path`.

### `read-wiki-page`

Reads a page. Accepts wiki-relative paths, `_candidates/` paths, and paths with `wiki/` prefix (stripped automatically). Returns:
- `content` — reconstructed `---yaml---\nbody` format
- `frontmatter` — parsed metadata object
- `resolvedContext` — resolved entity references
- `sourceEventsContext` — win/loss breakdown for each example's sourceEvents
- `categoryTemplate` — the `pageEntryTemplate` for this category

### `write-wiki-page`

Modes:
- `page` (default) — writes to exact path given
- `candidate` — routes to `_candidates/{path}`, increments `candidateCount` server-side, auto-promotes at ≥2
- `proposal` — routes to `_proposals/{path}`

After every write: updates `_log` (atomic append) and `_index` (advisory-locked rebuild).

Contradiction detection: if body contains `[[contradicts:]]` markers, sets `conflictCount` and `needsReview: true` in metadata.

### `grep-wiki`

Full-text search across wiki page content. `includeCandidates: true` includes staging area. Supports regex patterns. Returns `path`, `excerpt`, `resolvedContext`, `categoryTemplate` per match.

### `promote-wiki-candidate`

Manual promotion. Accepts `_candidates/category/slug` or `organisation/wiki/_candidates/category/slug`. Strips `candidateCount`/`firstSeen`, sets `status: 'published'`, `maturity: 'draft'`, writes to promoted path, deletes candidate.

**Registered in `intelligence` skill** (added 2026-05-25) so chat agent can promote candidates on request.

### `retract-wiki-page`

Sets `invalidAt: today` on a page's metadata. Excluded from subsequent `list-wiki-pages` and `grep-wiki` results. Page data is preserved, not deleted.

---

## Intelligence skill

`apps/server/src/mastra/skills/intelligence.ts`

Tools registered: `run-sql`, `read-wiki-page`, `grep-wiki`, `list-wiki-pages`, `promote-wiki-candidate`

Loads SKILL.md from `apps/server/.claude/skills/intelligence/SKILL.md`.

Context injected at load time:
- `userId`, `orgId`, Deals AOP ID
- Wiki category templates (for each category: slug, pageEntryTemplate, grep targets)

Resources (loaded by agent via `Read` tool when needed):
- `coaching.md` — use wiki for live deal coaching
- `analysis.md` — rep performance pattern analysis
- `analytics.md` — SQL patterns for pipeline metrics (note: `crm_events` has no `organization_id`, must JOIN to `crm_conversations` for org scoping; email events use `event_type = 'email'` + `direction`)
- `search.md` — vector search
- `process-doc-template.md` — authoritative playbook format
- `bant.md`, `saya.md`, `meddpicc.md` — methodology frameworks

Mode routing:
- Coaching question → load `coaching.md`, use `list-wiki-pages` + `read-wiki-page` + `grep-wiki`
- Rep analysis → load `analysis.md`, use `grep-wiki` + `run-sql`
- Pipeline metrics → load `analytics.md`, use `run-sql`
- BANT/SAYA/methodology → load relevant resource

---

## Sales process doc format

### Structure

Authoritative spec: `apps/server/.claude/skills/intelligence/resources/process-doc-template.md`
Synthesis prompt template: `apps/server/src/services/wiki/constants.ts` → `PROCESS_DOC_SYNTHESIS_TEMPLATE` (exported constant, used by `brainBackfillSynthesize`)

```markdown
---
type: sales-process
rep: {repName}
generated: YYYY-MM-DD
---

# {Rep Name} — Deal Process
*Synthesized from N deals (start_date–end_date)*

---

## Stage: {slug} — {Display Name}

**Entry:** {factual one-liner}
**Exit:** {factual one-liner}
**Duration:** {median X days}
**Typical touchpoints:** {e.g. "2 emails, 1 demo call"}

### Template: {Touchpoint name}   ← ### Template: prefix enables #outline stripping
**Type:** email | call | collateral | document send
**When:** {timing}
**Thread:** reply-to-existing | new thread
**CC:** {if applicable}
**Rule:** {one-line hard constraint — optional}
**Sources:** [[conversation: id1]] [[conversation: id2]]
**Seen in:** n/total deals analyzed

{email body with [placeholders in brackets]}

**Example:**
> [[event: evt_id]] [[conversation: conv_id]]
> "verbatim text..."

---
```

### `### Template:` prefix convention

Touchpoint subsections use `### Template: {name}` to enable the `#outline` content shortcut. The `#outline` shortcut strips everything from the first `### Template:` line through the next `## ` section, returning only the structural layer (entry/exit/duration/typical touchpoints).

Structural `###` subsections that are NOT template blocks (e.g., `### Won Deals (6)`, `### Three-Thread Model`, `### 6-Question Block`) do NOT get the `Template:` prefix.

### Hard rules

- Zero AI commentary — no "this shows X", no "this is why Y happened"
- Attributions everywhere — `[[conversation: id]]` on every touchpoint, `[[event: id]]` on every verbatim example
- S1–S4: full templates. S5+: full templates if reusable (subscription proposals, legal docs, price hold mechanics), shortform only if truly deal-specific
- Touchpoint names derived from observed patterns, never hardcoded

### Willem Ebbinge's doc (live as of 2026-05-25)

- **Path**: `organisation/playbooks/process/rep-lCtSraQ7QW0FIcRs5JQ1zwUrRS9MjiSZ`
- **Doc ID**: `85fbaba2-f5da-4c4e-a504-018a8517e787`
- **Folder IDs**: `organisation/playbooks` = `811953f0`, `organisation/playbooks/process` = `a0da4d91`
- **Content**: 12-deal synthesis (6 won, 6 lost), S1–S7 full templates, 32 `### Template:` blocks
- **Source**: `organisation/knowledge-base/sales-process` (id: `04058958`) + `user/notes/sales-process-map` (id: `fd15e1cc`)
- **Local draft**: `tmp/willem-process-doc-draft.md`

---

## Playbook → agent wiring (in progress)

### Architecture

```
Orchestrator
  step 3 (before deciding what to draft):
    → read-document('organisation/playbooks/process/rep-{userId}', content: '#outline')
    → sees stage headers + entry/exit/duration/touchpoints, no verbose templates
    → decides: "Draft post-kickoff recap email"
    → passes to executor: "Draft post-kickoff recap. Playbook section: #section/Stage: 3_pilot_won"

Executor
    → reads #section/Stage: 3_pilot_won from the playbook (full templates)
    → drafts using ### Template: blocks verbatim, filling in [placeholders]
```

### Why this split

- Orchestrator only needs the structural layer to make the drafting decision (~500 tokens)
- Executor needs the full templates to draft correctly (~2-8k tokens per stage section)
- The `#section/Stage: {slug}` shortcut gives the executor exactly the section it needs — no full-doc read, no navigation overhead
- The orchestrator already knows the current stage from conversation context — no extra reasoning needed

### Current wiring status

- `#outline` shortcut: **live** (added 2026-05-25)
- Willem's doc `### Template:` prefixes: **live** (added 2026-05-25)
- Orchestrator step 3 instruction: **pending**
- Executor `conditionDescription` addition: **pending**

Long-term v2: `runPostEventExecutorTool` auto-detects playbook at `organisation/playbooks/process/rep-{userId}`, pre-fetches the relevant section, and injects it as `<playbook_section>` block — no per-rep config change needed.

Source: `apps/server/src/mastra/tools/event-execution/orchestrator-dispatch-tools.ts` — `runPostEventExecutorTool`

Plan doc: `.cursor/plans/sales_playbook_as_agent_source_of_truth_e80c09b4.plan.md`

---

## Key files reference

| File | What changed |
|---|---|
| `apps/server/src/services/wiki/constants.ts` | Wiki categories, WIKI_SPECIAL_FOLDERS, WIKI_SPECIAL_FILES, category configs |
| `apps/server/src/services/wiki/frontmatter.ts` | Parser/serializer, parseExamples, formatExample, appendExampleToBody, validateFrontmatter |
| `apps/server/src/services/wiki/seed.ts` | Idempotent seeding, `_candidates/` folders, product-reference doc |
| `apps/server/src/services/wiki/entity-resolver.ts` | Resolves conversation/event/person/user/field refs |
| `apps/server/src/services/wiki/templates.ts` | generateSalesProcessDocTemplate, generateProductReferenceTemplate |
| `apps/server/src/services/document-store/document-types.ts` | Added USER and FIELD to CEDAR_DOC_REFERENCE_TYPE |
| `apps/server/src/services/document-store/content-shortcuts.ts` | #sections, #section/SectionName, #outline shortcuts |
| `apps/server/src/services/document-store/convention-paths.ts` | ORG_WIKI_PATH, ORG_KB_PATH, path constants |
| `apps/server/src/services/document-store/path-shortcuts.ts` | #wiki, #kb, #files, #notes, #here expansions |
| `apps/server/src/mastra/tools/wiki/listWikiPagesTool.ts` | Fixed path calculation (bug fix 2026-05-25) |
| `apps/server/src/mastra/tools/wiki/readWikiPageTool.ts` | Entity resolution, sourceEvents enrichment, category template |
| `apps/server/src/mastra/tools/wiki/writeWikiPageTool.ts` | Candidate auto-promotion, advisory lock for _index, atomic _log append, contradiction detection |
| `apps/server/src/mastra/tools/wiki/promoteWikiCandidateTool.ts` | Manual promotion, metadata-first format |
| `apps/server/src/mastra/tools/wiki/grepWikiTool.ts` | Full-text wiki search |
| `apps/server/src/mastra/tools/wiki/retractWikiPageTool.ts` | Sets invalidAt |
| `apps/server/src/mastra/tools/docs/readDocumentTool.ts` | Integrated content shortcuts |
| `apps/server/src/mastra/skills/intelligence.ts` | Unified skill; added promoteWikiCandidateTool (bug fix 2026-05-25) |
| `apps/server/src/workflows/step-registry/conversation-sync/brain-backfill-steps.ts` | Backfill pipeline; userId in prompt (bug fix 2026-05-25) |
| `apps/server/src/trpc/routes/wiki.ts` | listDealsForUser, deleteWiki, getStats |
| `apps/mail/app/(routes)/playground/brain.tsx` | Brain backfill UI |
| `apps/mail/modules/company/components/CompanyExplorer.tsx` | YAML frontmatter banner for wiki pages |
| `apps/mail/components/document.tsx` | ConversationNode, EventRefNode, UserRefNode, FieldRefNode in composedExtensions |
| `apps/mail/modules/agentCanvas/extensions/ConversationNode.tsx` | Dual tokenizer: @[uuid] and [[conversation: uuid]] |
| `apps/mail/modules/conversations/components/tiptap-extensions/UserRefNode.tsx` | New — [[user: uuid]] pill |
| `apps/mail/modules/conversations/components/tiptap-extensions/FieldRefNode.tsx` | New — [[field: stage/slug]] pill |
| `apps/server/.claude/skills/wiki-extraction/SKILL.md` | Comprehensive extraction instructions |
| `apps/server/.claude/skills/intelligence/SKILL.md` | Unified skill with mode routing |
| `apps/server/.claude/skills/intelligence/resources/process-doc-template.md` | Authoritative playbook format |

---

## Pending next steps

1. Wire orchestrator step 3 to read `#outline` from playbook and pass section shortcut in `orchestratorNotes`
2. Wire Willem's executor `conditionDescription` to read `#section/Stage: {slug}` on receiving the pointer
3. Trigger test execution for Willem and verify via `agent_tool_calls`
4. If validated: build auto-inject into `runPostEventExecutorTool` (no per-rep config needed)
5. Run brain backfill for other users once wiki quality validated on second run
6. Fill in `[[conversation:]]` attribution gaps in Willem's process doc from CRM data