strategist-playbook-dispatch.md21.8 KBView on GitHub
# Strategist dispatch — playbook-referenced subagent (not a hardcoded system step)

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

We want the Strategist to run on deal events as **a plain, playbook-referenced subagent** — visible and editable in the seller's PLAYBOOK.md, gate-able and reorder-able like any subagent — rather than a special case baked into the orchestrator prompt. Today the Strategist exists as an `aop_agents` row + a `subagents/strategist.md` doc (both seeded) and owns the strategic fields, but it is dispatched by a hardcoded bullet in the on-event orchestrator's "system pipeline order" ([on-event-orchestrator-agent.ts:131-138](apps/server/src/mastra/agents/on-event-orchestrator-agent.ts)) that special-cases `run-subagent(@subagents/strategist)` and its `subagents/strategist.md` doc is seeded off to the side by `ensureStrategistSubagentDoc` ([strategist-subagent-doc.ts](apps/server/src/services/aop/strategist-subagent-doc.ts)) rather than by the normal playbook seeder — and crucially it is **not referenced in the default playbook**. We will move the Strategist's doc-seeding into the standard playbook seeder next to `crm-updater`/`next-steps` ([seed-playbook.ts](apps/server/src/services/playbook/seed-playbook.ts)), add its `<ref>` to the default `<trigger type="any">` block so it dispatches as a normal `@subagents/` line, revert the orchestrator special case, and backfill existing Deals playbooks to include the reference — keeping the row + doc + field ownership exactly as they are so nothing about ownership or attribution changes.

## 2) Present state

### 2.1 Architecture diagram

```text
  ACCOUNT SETUP
   seedSystemDefaultAgents(aopId,userId)                 seedPlaybookFiles(aopId,…)
     ├─ insert aop_agents rows (CRM Updater, Strategist,   ├─ seed subagent DOCS via seedFile+prepareSubagentWrite:
     │   Overview(off), Post-Event, Task Aggregator)       │     crm-updater.md, next-steps.md, meeting-prep.md, daily-agenda.md
     ├─ stamp ownerAgentId = Strategist on 12 fields       │     (agent_id reconciled to the aop_agents row per name)
     └─ ensureStrategistSubagentDoc()  ← SIDE-DOOR         ├─ collect subagentUuids{crm-updater,next-steps,…}
          writes subagents/strategist.md (agent doc,       └─ buildPlaybookTemplate(refs):
          agent_id = Strategist row id)                          <trigger type="any">
                                                                    <ref id="{crmUpdater}"/>
   ⇒ strategist.md EXISTS but is NOT in subagentUuids,               <ref id="{nextSteps}"/>   ← NO strategist ref
     and NOT referenced in the default playbook.                  </trigger>
                         │
  EVENT → on-event-orchestrator-agent (LLM)
     receives <playbook_section> (compiled @subagents lines) + a hardcoded prompt:
       "System pipeline order: crm-updater → STRATEGIST(special-case) → drafting → next-steps"
       "run @subagents/strategist ONLY when it appears in the directory"  ← the hack
     dispatches each via run-subagent(@subagents/name) → reads subagents/{name}.md → runAgent(synthetic, agentId)
```

### 2.2 Step-by-step walkthrough

1. **Seed rows** — `seedSystemDefaultAgents` at [aop-agents.ts:352](apps/server/src/services/aop/aop-agents.ts) inserts the system `aop_agents` rows (incl. Strategist, `pipelineOrder:2`, `EVENT_OCCURRED`), stamps `ownerAgentId` on the 12 strategic fields, then calls `ensureStrategistSubagentDoc`.
   - Data after this step:
     ```json
     { "row": { "id": "47aaf4ad…", "name": "Strategist", "pipelineOrder": 2, "enabled": true },
       "ownedFields": 12 }
     ```
2. **Side-door doc seed** — `ensureStrategistSubagentDoc` at [strategist-subagent-doc.ts](apps/server/src/services/aop/strategist-subagent-doc.ts) resolves the Strategist row and writes `user/playbooks/{aopId}/subagents/strategist.md` (a Phase-20 `agent` doc) with `agent_id` = the row id + `STRATEGIST_INSTRUCTIONS` body. It is written **outside** `seedPlaybookFiles`, so the playbook seeder never learns the doc's UUID.
   - Data after this step (frontmatter):
     ```text
     ---\n\nname: strategist\n\ndescription: …\n\nsystem: true\n\nmodel: sonnet\n\nfill_instructions: …\n\nagent_id: 47aaf4ad…\n\n---\n\n{STRATEGIST_INSTRUCTIONS}
     ```
3. **Seed subagent docs (the standard path)** — `seedPlaybookFiles` at [seed-playbook.ts:540-600](apps/server/src/services/playbook/seed-playbook.ts) iterates a list `[{name:CRM_UPDATER,file:'crm-updater.md',build:buildCrmUpdaterContent}, {name:TASK_AGGREGATOR,file:'next-steps.md',…}, meeting-prep, daily-agenda]`, calls `prepareSubagentWrite` (injects `agent_id`) + `seedFile` (writeDocument), and collects `subagentUuids[name]`. **The Strategist is absent from this list.**
   - Data after this step:
     ```json
     { "subagentUuids": { "crm-updater": "…", "next-steps": "…", "meeting-prep": "…", "daily-agenda": "…" } }
     ```
4. **Build the playbook XML** — `buildPlaybookTemplate` at [seed-playbook.ts:115-160](apps/server/src/services/playbook/seed-playbook.ts) composes the `<trigger type="any">` block from `anyBlockRefs = [refs.crmUpdater, refs.nextSteps]` ([seed-playbook.ts:135](apps/server/src/services/playbook/seed-playbook.ts)); `PlaybookDocRefs` ([seed-playbook.ts:93](apps/server/src/services/playbook/seed-playbook.ts)) has no `strategist` field. So the seeded playbook references crm-updater + next-steps only.
   - Data after this step:
     ```xml
     <trigger type="any">
       <ref id="{crmUpdater}"/>
       <ref id="{nextSteps}"/>
     </trigger>
     ```
5. **Compile the playbook for the orchestrator** — `get-playbook-section.ts` at [get-playbook-section.ts:226-265](apps/server/src/services/playbook/get-playbook-section.ts) resolves each `<ref>` to an `@subagents/{name} (desc) [doc_id:…]` line for the compiled `<playbook_section>`. A missing strategist `<ref>` means no strategist line is compiled.
6. **Orchestrator dispatch** — `on-event-orchestrator-agent` ([on-event-orchestrator-agent.ts:206-230](apps/server/src/mastra/agents/on-event-orchestrator-agent.ts)) receives `<playbook_section>` + a **hardcoded** "System pipeline order" that special-cases the Strategist at [on-event-orchestrator-agent.ts:131-138](apps/server/src/mastra/agents/on-event-orchestrator-agent.ts):
   ```text
   1. run-crm-updater
   2. run-subagent(@subagents/strategist) — … ONLY when @subagents/strategist appears in the directory …
   3. run-post-event-executor
   4. update-next-steps-and-tasks (@subagents/next-steps)
   ```
   It dispatches each via `run-subagent`, which reads `subagents/{name}.md`, builds a synthetic `AopAgent` from the frontmatter `agent_id`, and runs it ([runSubagentTool.ts:160-260](apps/server/src/mastra/tools/event-execution/runSubagentTool.ts)).
7. **Ordering** — the orchestrator forces crm-updater first and next-steps last; every playbook `@subagents/` line runs in between. So a playbook-referenced subagent naturally lands after crm-updater and before next-steps (the exact slot the Strategist wants).

## 3) Designed state

### 3.1 Architecture diagram

```text
  ACCOUNT SETUP
   seedSystemDefaultAgents(aopId,userId)                 seedPlaybookFiles(aopId,…)
     ├─ insert aop_agents rows (incl. Strategist)          ├─ seed subagent DOCS (agent_id reconciled to rows):
     └─ stamp ownerAgentId on 12 fields                    │     crm-updater.md, STRATEGIST.md ◄NEW, next-steps.md, meeting-prep.md, daily-agenda.md
       (NO ensureStrategistSubagentDoc side-door)          ├─ collect subagentUuids{…, strategist ◄NEW}
                                                            └─ buildPlaybookTemplate(refs):
                                                                 <trigger type="any">
                                                                   <ref id="{crmUpdater}"/>
                                                                   <ref id="{strategist}"/>   ◄NEW
                                                                   <ref id="{nextSteps}"/>
                                                                 </trigger>
                         │
  EVENT → on-event-orchestrator-agent (LLM)
     receives <playbook_section> that now contains the @subagents/strategist line (from the ref)
     "System pipeline order: crm-updater → drafting → next-steps"   ← strategist bullet REMOVED (back to generic)
     runs playbook @subagents/ lines (incl. strategist) between the forced crm-updater and next-steps
       → run-subagent(@subagents/strategist) → subagents/strategist.md → runAgent(synthetic, agentId)

  BACKFILL (existing accounts): ensure strategist.md exists + inject <ref id="{strategist}"/> into the
     live PLAYBOOK.md <trigger type="any">, recompile + re-save, null contentYjs to force re-parse.
```

### 3.2 Step-by-step walkthrough

1. **Row seeding unchanged** — `seedSystemDefaultAgents` ([aop-agents.ts:352](apps/server/src/services/aop/aop-agents.ts)) still inserts the Strategist row + stamps field ownership, but **no longer** calls `ensureStrategistSubagentDoc` (the doc is now seeded by the playbook seeder). Row + ownership are identical to today.
2. **Standard doc seeding** — add the Strategist to `seedPlaybookFiles`' subagent list ([seed-playbook.ts:560-595](apps/server/src/services/playbook/seed-playbook.ts)) as `{ name: SystemAgentName.STRATEGIST, file: 'strategist.md', build: buildStrategistSubagentBody }`, using the shared body builder so content matches the side-door version. `prepareSubagentWrite` injects `agent_id` (reconciled to the row by name, same as crm-updater), and `subagentUuids['strategist']` is collected.
   - Data after this step:
     ```json
     { "subagentUuids": { "crm-updater": "…", "strategist": "…NEW", "next-steps": "…" } }
     ```
3. **Reference it in the default playbook** — add `strategist?: string` to `PlaybookDocRefs` ([seed-playbook.ts:93](apps/server/src/services/playbook/seed-playbook.ts)); set `refs.strategist = subagentUuids['strategist']` ([seed-playbook.ts:670-675](apps/server/src/services/playbook/seed-playbook.ts)); include it in `anyBlockRefs` between crm-updater and next-steps ([seed-playbook.ts:135](apps/server/src/services/playbook/seed-playbook.ts)): `[refs.crmUpdater, refs.strategist, refs.nextSteps]`.
   - Data after this step:
     ```xml
     <trigger type="any">
       <ref id="{crmUpdater}"/>
       <ref id="{strategist}"/>
       <ref id="{nextSteps}"/>
     </trigger>
     ```
4. **Revert the orchestrator special case** — restore the generic three-step "System pipeline order" at [on-event-orchestrator-agent.ts:131-138](apps/server/src/mastra/agents/on-event-orchestrator-agent.ts) (crm-updater → drafting → next-steps); drop the strategist bullet and the "ONLY when it appears in the directory" clause. The Strategist now dispatches purely because the playbook lists it, in the natural between-the-brackets slot.
5. **Compile + dispatch (unchanged mechanics)** — `get-playbook-section.ts` compiles the new `<ref>` into an `@subagents/strategist … [doc_id:…]` line; the orchestrator runs it via `run-subagent` exactly like any playbook subagent. No change to `runSubagentTool` or ownership.
6. **Backfill existing playbooks** — a script that, per existing Deals AOP with a Strategist row: (a) ensures `subagents/strategist.md` exists (reuse `ensureStrategistSubagentDoc`), (b) loads the live PLAYBOOK.md, injects `<ref id="{strategistDocId}"/>` into `<trigger type="any">` right after the crm-updater ref if absent, (c) recompiles + re-saves via `writeDocument` (documentType `playbook`) and nulls `contentYjs` to force re-parse — mirroring [patch-playbook-crm-refs.ts](apps/server/src/db/migrations/scripts/account-setup/patch-playbook-crm-refs.ts) / [heal-raw-at-tokens.ts](apps/server/src/db/migrations/scripts/playbook-health/heal-raw-at-tokens.ts). Idempotent; run jesse-only first (`BACKFILL_ALL=1` for everyone).

### 3.3 Schema

No DB tables change. The only structural change is a new optional field on the seed-time `PlaybookDocRefs` shape and the seeded subagent frontmatter (unchanged format). Full definitions:

```ts
// apps/server/src/services/playbook/seed-playbook.ts — PlaybookDocRefs (seed-time ref bag; all fields)
interface PlaybookDocRefs {
  overallGoal?: string;        // always-loaded personal goal doc uuid
  emailStyle?: string;         // always-loaded email style doc uuid
  coachingFramework?: string;  // always-loaded coaching framework doc uuid
  templates?: string;          // on-demand templates doc uuid
  crmUpdater?: string;         // subagents/crm-updater.md uuid → <trigger type="any">
  strategist?: string;         // NEW — subagents/strategist.md uuid → <trigger type="any"> (between crm-updater and next-steps)
  nextSteps?: string;          // subagents/next-steps.md uuid → <trigger type="any">
  meetingPrep?: string;        // subagents/meeting-prep.md uuid → <trigger type="before-meeting">
  dailyAgenda?: string;        // subagents/daily-agenda.md uuid → <trigger type="cron">
}

// Seeded subagent doc frontmatter (unchanged shape; the Strategist now seeded via this path).
// Stored in documents.content; documents.documentType = 'agent' (born-as-agent, phase 20).
type SubagentDocFrontmatter = {
  name: string;                // 'strategist'
  description: string;
  system: true;                // system-default subagent
  model: string;               // 'sonnet'
  fill_instructions: string;   // author-facing header hint
  agent_id: string;            // FK → aop_agents.id (the Strategist row) — reconciled by name
};
```

Relationship diagram:

```text
┌───────────────────────────────┐        ┌───────────────────────────────────────┐
│ aop_agents (Strategist row)   │        │ documents: subagents/strategist.md      │
│  id (PK) 47aaf4ad…            │◄──FK───┤  metadata.agent_id / frontmatter.agent_id│  (documentType 'agent')
│  name 'Strategist'            │  name  │  path user/playbooks/{aopId}/subagents/ │
│  pipelineOrder 2  enabled     │  recon │       strategist.md                     │
│  (owns 12 strategic fields    │        └───────────────┬─────────────────────────┘
│   via customFieldDefinitions. │                        │ uuid collected as subagentUuids['strategist']
│   ownerAgentId = this id)     │                        ▼
└───────────────────────────────┘        ┌───────────────────────────────────────┐
                                          │ documents: PLAYBOOK.md (documentType    │
                                          │  'playbook')  <trigger type="any">      │
   run-subagent(@subagents/strategist) ◄──┤    <ref id="{crmUpdater}"/>             │
     reads the doc, builds synthetic      │    <ref id="{strategist}"/>   ◄NEW ref  │
     AopAgent from agent_id, runAgent     │    <ref id="{nextSteps}"/>              │
                                          └───────────────────────────────────────┘
```

## 4) Implementation phases

### Phase 1 — Seed the Strategist as a normal playbook subagent ✅

**Goal:** New accounts get `subagents/strategist.md` seeded by the standard playbook seeder and referenced in the default `<trigger type="any">`, with `agent_id` reconciled to the Strategist row — no side-door.

- [x] Added `strategist?: string` to `PlaybookDocRefs` in [seed-playbook.ts](apps/server/src/services/playbook/seed-playbook.ts).
- [x] Seed `subagents/strategist.md` in `seedPlaybookFiles` ([seed-playbook.ts](apps/server/src/services/playbook/seed-playbook.ts)) using the shared `buildStrategistSubagentContent(rowId, STRATEGIST_INSTRUCTIONS)` ([strategist-subagent-doc.ts](apps/server/src/services/aop/strategist-subagent-doc.ts), [aop-agents.ts](apps/server/src/services/aop/aop-agents.ts)). **Divergence (better than planned):** rather than adding it to the `subagentDefs` loop whose `withAgentId` only injects for frontmatter-only builders and otherwise depends on a later `heal-subagent-agent-ids` pass (which doesn't know `strategist`), it is seeded just after the loop with the row id emitted **directly** from `agentIdByName` (already in scope) — guaranteeing `agent_id` === the Strategist row id with no heal step.
- [x] Set `refs.strategist = subagentUuids['strategist']` and placed it in `anyBlockRefs` between crm-updater and next-steps ([seed-playbook.ts](apps/server/src/services/playbook/seed-playbook.ts)).
- [x] Removed the `ensureStrategistSubagentDoc` call from `seedSystemDefaultAgents` ([aop-agents.ts](apps/server/src/services/aop/aop-agents.ts)) (kept the row insert + field-ownership stamping). The `ensureStrategistSubagentDoc` helper stays — the backfill (Phase 3) still uses it.

**Tests:**

- [x] **Headless (run as jesse):** [strategist-playbook-seed-smoke.ts](apps/server/src/scripts/strategist-playbook-seed-smoke.ts) seeds a throwaway Deals AOP → asserts `subagents/strategist.md` exists (documentType `agent`), `agent_id` (frontmatter + metadata) === the Strategist row id, and the seeded PLAYBOOK.md `<trigger type="any">` references crm-updater → strategist → next-steps **in order**. PASS; throwaway AOP + docs torn down.
- [x] `pnpm --filter @zero/server test seed-playbook` — 37 passed (existing coverage green; the test reproduces `buildPlaybookTemplate` privately, so the smoke is the authoritative check of the real path).

### Phase 2 — Revert the orchestrator hardcoded strategist step ✅

**Goal:** The orchestrator no longer special-cases the Strategist; it dispatches it only because the playbook lists it.

- [x] Restored the generic three-step "System pipeline order" in [on-event-orchestrator-agent.ts](apps/server/src/mastra/agents/on-event-orchestrator-agent.ts) (crm-updater → post-event/drafting → next-steps); removed the numbered strategist step + the "ONLY when it appears in the directory" clause.
- [x] Added one clarifying sentence that analysis/strategy playbook subagents (e.g. `@subagents/strategist`) run as ordinary playbook lines between crm-updater and next-steps — not special-cased.

**Tests:**

- [x] Grep-based check: the hardcoded strategist step (`run-subagent(@subagents/strategist) …` / "ONLY when …") is gone (count 0), the generic three-step text is present (count 1), and `@subagents/strategist` survives only as an example line. Typecheck clean.

### Phase 3 — Backfill existing Deals playbooks ✅

**Goal:** Existing accounts get `subagents/strategist.md` (if missing) and a `<ref>` to it in their live PLAYBOOK.md, so the Strategist dispatches for them too.

- [x] New script [seed-strategist-playbook-ref.ts](apps/server/src/db/migrations/scripts/seed-strategist-playbook-ref.ts): for each user AOP with a Strategist row — (a) `ensureStrategistSubagentDoc`, (b) load PLAYBOOK.md, (c) if `<trigger type="any">` lacks the strategist ref, insert `<ref id="{strategistDocId}"/>` after the crm-updater ref (indentation-matched), (d) `writeDocument` (documentType `playbook`) — the compile hook recompiles, (e) null `contentYjs` + bump `yjsRevision`. Idempotent (skips when the ref is already present).
- [x] Scope flags mirroring the other Strategist backfills: default jesse-only, `BACKFILL_ALL=1`, `BACKFILL_EMAIL=x`.
- [x] Ran for jesse (`patched:1`); `BACKFILL_ALL` left for a later deliberate rollout.

**Tests:**

- [x] **Headless (run as jesse):** after the backfill, the live Deals PLAYBOOK.md `<trigger type="any">` contains the strategist ref **after crm-updater**, `getPlaybookSection({eventType:'email_received'})` (the exact function the orchestrator calls) returns a section containing `@subagents/strategist (Owns the strategic overview …) [doc_id:…]`, and a re-run reports `already` (no-op).

### Phase 4 — Verify live dispatch end-to-end ✅ (deterministic chain proven; LLM tool-call shared w/ all subagents)

**Goal:** Confirm the Strategist actually fires on a real event now that it is playbook-referenced.

- [x] **Deterministic dispatch chain proven headlessly** ([strategist-dispatch-smoke.ts](apps/server/src/scripts/strategist-dispatch-smoke.ts), run as jesse): (1) `getPlaybookSection({eventType:'email_received'})` — the exact function feeding the on-event orchestrator — emits the `@subagents/strategist … [doc_id:…]` dispatch line; (2) `readSubagentFull(aopId,'strategist')` — the exact resolution `run-subagent` performs — returns the doc with `frontmatter.agent_id` === the Strategist row id + 1776-char instructions, so run-subagent builds the correct synthetic agent (which can write the owned fields, proven in [[strategic-overview]] phase 12).
- [x] **Divergence (no silent deviation):** the *full* orchestrator LLM drive is not headlessly reproducible here — the `run-subagent`/`runAgent` chain can't be imported under `tsx` (an unrelated pre-existing `@barkleapp/css-sanitizer` ESM-export quirk; it loads fine in the real server), and a single synthetic LLM run would be costly + non-deterministic. The one remaining link — the orchestrator LLM *choosing* to call `run-subagent(@subagents/strategist)` — is the identical mechanism crm-updater/next-steps use in production every day; confirm it on the **next real jesse Deals event** via Axiom rather than a manufactured one-off.
- [x] Updated Phase 11's dispatch note in [strategic-overview.md](apps/mail/docs/strategic-overview.md): dispatch is now wired + deterministically proven to the LLM tool-call; the live LLM firing is confirmed via real-event monitoring.

**Tests:**

- [x] [strategist-dispatch-smoke.ts](apps/server/src/scripts/strategist-dispatch-smoke.ts) — PASS (both assertions green, run as jesse).