@vruum/skills 0.6.53 → 0.6.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vruum",
3
- "version": "0.6.53",
3
+ "version": "0.6.54",
4
4
  "description": "Vruum AI skills + remote MCP server for B2B GTM teams. Slash commands for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis, paired with the full Vruum MCP tool surface over OAuth 2.1.",
5
5
  "author": {
6
6
  "name": "Vruum AI",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vruum",
3
- "version": "0.6.53",
3
+ "version": "0.6.54",
4
4
  "description": "Vruum AI skills + remote MCP server for B2B GTM teams. Skills for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis, paired with the full Vruum MCP tool surface over OAuth 2.1.",
5
5
  "author": {
6
6
  "name": "Vruum AI",
package/README.md CHANGED
@@ -86,8 +86,8 @@ the public skill instead of leaving a broken link.
86
86
  ## Skills
87
87
 
88
88
  <!-- generated:skills-begin -->
89
- - `/campaign-builder` — Build and launch an outreach campaign from criteria in about five prompts: filter contacts by size, industry, persona, region, or list; preview the cohort; create the campaign (optionally cloning messaging from an existing one); assign people; review and launch. Use when: create a campaign, build a campaign, new campaign from criteria, campaign from my list.
90
- - `/campaign-doctor` — Diagnose and fix struggling outreach campaigns. Use when: fix a campaign, diagnose campaign, why is my campaign not working, campaign health, low reply rate, check campaigns, which campaigns need help.
89
+ - `/objective-builder` — Build and launch an outreach campaign from criteria in about five prompts: filter contacts by size, industry, persona, region, or list; preview the cohort; create the campaign (optionally cloning messaging from an existing one); assign people; review and launch. Use when: create a campaign, build a campaign, new campaign from criteria, campaign from my list.
90
+ - `/objective-doctor` — Diagnose and fix struggling outreach campaigns. Use when: fix a campaign, diagnose campaign, why is my campaign not working, campaign health, low reply rate, check campaigns, which campaigns need help.
91
91
  - `/create-content` — Co-produce an on-voice LinkedIn content post — pull your own signal, steer the angle conversationally, draft in your voice, then save as draft, schedule, or publish. Use when: write a post, draft LinkedIn content, create content, post about, content co-production, help me write a post.
92
92
  - `/csv-pipeline-fill` — CSV harness source for /pipeline-fill. Reads a CSV, auto-detects headers, maps columns, hands off to /pipeline-fill for harness deep research and import. Use when: import CSV, paste a CSV, csv import, prospect list from CSV, csv harness mode.
93
93
  - `/deal-triage` — Triage your active deal pipeline. Flags at-risk deals, surfaces stalled-deal alerts, runs MEDDIC qualification, and recommends next actions. Use when: review deals, triage deals, check pipeline, deal review, morning deals, pipeline review, deal health, at-risk deals.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vruum/skills",
3
- "version": "0.6.53",
3
+ "version": "0.6.54",
4
4
  "description": "Vruum AI skills for Claude Code, Claude Desktop, Codex CLI, and any AI assistant with a skill directory. Slash commands for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis. Pairs with the Vruum MCP server at https://api.vruum.ai/mcp.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,5 +41,5 @@
41
41
  "outreach",
42
42
  "gtm"
43
43
  ],
44
- "contentHash": "f389e0800d9818f7b400b8032d3038ee34e512e077463f4ef1ddeb2020c3b1cc"
44
+ "contentHash": "c8dccb170a520d40b4e2b548ff71989d46135e307833b5416c782aecf3ea0d2f"
45
45
  }
@@ -92,7 +92,7 @@ Then save it **once** with `manage_content` `action="draft"`, payload `{content,
92
92
 
93
93
  Carousels (document posts) are the top-performing organic format. If the seller has (or you produce) a PDF, image, or video for this post:
94
94
 
95
- 1. Store the asset: `manage_campaign` kind='ad' action='store_creative'. Small files: payload `{asset_base64, filename}`. Real files (PDFs/videos — primary path): `{filename, size_bytes, content_type: 'application/pdf' | 'video/mp4'}` → PUT the file to the returned `upload_url` (`curl --fail-with-body -T <file> '<upload_url>'`), then call store_creative again with `{creative_id}` to finalize.
95
+ 1. Store the asset: `manage_content` kind='ad' action='store_creative'. Small files: payload `{asset_base64, filename}`. Real files (PDFs/videos — primary path): `{filename, size_bytes, content_type: 'application/pdf' | 'video/mp4'}` → PUT the file to the returned `upload_url` (`curl --fail-with-body -T <file> '<upload_url>'`), then call store_creative again with `{creative_id}` to finalize.
96
96
  2. Attach it: include `attachment_creative_id` in the draft payload (or add it later with `action="edit"`). Explicit `attachment_creative_id: null` on edit detaches.
97
97
  3. The stored `filename` renders as the LinkedIn document **title** — name it like a headline, not `export-final-v3.pdf`.
98
98
  4. Before publishing, open the `attachment_url` from `get_content_review post_ids=[<post_id>]` and review the actual file — it publishes under the seller's identity.
@@ -15,7 +15,7 @@ You are the CSV harness-mode source for `/pipeline-fill`. You read a CSV the ope
15
15
  ## Inputs
16
16
 
17
17
  - `file_path`: absolute path to the CSV. If not provided, ask for it. Common locations: `~/Downloads/`, `.context/attachments/` in the workspace.
18
- - `campaign`: target campaign (single)
18
+ - `objective`: target objective (single)
19
19
  - `column_mapping` (optional): explicit `{header_name: canonical_field}` mapping if headers don't auto-resolve. Default: auto-detect.
20
20
 
21
21
  ## Workflow
@@ -110,7 +110,7 @@ Emit the canonical handoff prompt (defined in `pipeline-fill/RESEARCH-ENGINE.md`
110
110
  ```
111
111
  Candidate list ready: {N} prospects from csv (after {dedup_count} dedup).
112
112
 
113
- NEXT: invoke /pipeline-fill Step 3 onward (deep research → harness gate → save) with this list and campaign {campaign_id}.
113
+ NEXT: invoke /pipeline-fill Step 3 onward (deep research → harness gate → save) with this list and objective {objective_id}.
114
114
 
115
115
  Continue automatically? (y/n)
116
116
  ```
@@ -33,7 +33,7 @@ Turn the goal into targeting criteria and **preview before committing**:
33
33
  - `search` with `type="people"` and the criteria (include `filters={research_status: "all"}` so stub imports are visible). Read the **total count** and show a **5-row sample** (name, title, company, persona, the attributes that matched).
34
34
  - Iterate the criteria conversationally until the cohort is the right size and shape for the goal. An empty preview means the criteria are too tight — loosen and re-run; never proceed to boost on a zero-count audience.
35
35
 
36
- This is the same segmentation conversation `/campaign-builder` runs — **do not reproduce its steps here.** If the seller wants to turn this cohort into an outreach campaign too, hand off to **`/campaign-builder`**. For the boost itself the audience is **firmographic facets** — LinkedIn-native attribute targeting (titles, seniorities, industries, locations, company sizes), the same way LinkedIn's own boost targets. Resolve names to entity URNs FIRST via `fetch type='ads' subtype='targeting_entities'` (facet + q), then hold the resolved `{facets}` map. **`locations` is required** — a campaign without a location facet is rejected. (`{matched_audience_id}` still works when a pre-uploaded audience exists; `{criteria}`-built matched audiences are partner-gated on LinkedIn's side and return a clear error pointing back to facets.)
36
+ This is the same segmentation conversation `/objective-builder` runs — **do not reproduce its steps here.** If the seller wants to turn this cohort into an outreach objective too, hand off to **`/objective-builder`**. For the boost itself the audience is **firmographic facets** — LinkedIn-native attribute targeting (titles, seniorities, industries, locations, company sizes), the same way LinkedIn's own boost targets. Resolve names to entity URNs FIRST via `fetch type='ads' subtype='targeting_entities'` (facet + q), then hold the resolved `{facets}` map. **`locations` is required** — a campaign without a location facet is rejected. (`{matched_audience_id}` still works when a pre-uploaded audience exists; `{criteria}`-built matched audiences are partner-gated on LinkedIn's side and return a clear error pointing back to facets.)
37
37
 
38
38
  Hold the settled audience (the resolved `{facets}` map — shown to the seller as plain names with counts where available — or a `matched_audience_id`). It is one of the four things the seller approves in Step 4.
39
39
 
@@ -41,12 +41,12 @@ Hold the settled audience (the resolved `{facets}` map — shown to the seller a
41
41
 
42
42
  **Copy — hand off, don't write it here.** Invoke **`/create-content`** to co-produce the on-voice post. That skill owns author resolution, signal grounding, the steer→draft loop, and the publish guards — narrate it as it works, but never reproduce its drafting procedure. It leaves you a **draft** content post (its id is what Step 5 boosts). Keep the post a draft for now — publish is gated behind the seller's approval in Step 4. Two post-level decisions to settle WITH the copy (both settable at draft/edit):
43
43
 
44
- - **Identity** — `author_identity: 'member'` (a person's profile → the boost runs as a **Thought Leader Ad**, awareness/engagement only, and the author must be authorized via `manage_campaign` kind='ad' action='authorize_author') or `'organization'` (the **Company Page** → all objectives, incl. clicks; needs the Page set once via action='set_page').
44
+ - **Identity** — `author_identity: 'member'` (a person's profile → the boost runs as a **Thought Leader Ad**, awareness/engagement only, and the author must be authorized via `manage_content` kind='ad' action='authorize_author') or `'organization'` (the **Company Page** → all objectives, incl. clicks; needs the Page set once via action='set_page').
45
45
  - **Destination** — if the goal is clicks, put the `external_link` on the post NOW (UTMs are stamped automatically). WEBSITE_VISIT without a destination is rejected at boost time.
46
46
 
47
47
  **Visual — generate or supply it, then store it as a draft.**
48
48
 
49
- - **Image**: generate with your own image tools, then store via `manage_campaign` kind='ad' action='store_creative', payload `{asset_base64 (raw base64, no data: URL prefix — renamed from image_base64 in VRU-726), generation_prompt, filename?, generation_provenance: {model, tool, generated_at, notes}}`.
49
+ - **Image**: generate with your own image tools, then store via `manage_content` kind='ad' action='store_creative', payload `{asset_base64 (raw base64, no data: URL prefix — renamed from image_base64 in VRU-726), generation_prompt, filename?, generation_provenance: {model, tool, generated_at, notes}}`.
50
50
  - **Document / PDF carousel** (≤25MB — VRU-726, the top organic format): produce the PDF with your own tools, then store via the same action — small files: `{asset_base64, filename: 'my-deck.pdf'}` (no generation_prompt needed); real files (primary path): `{filename, size_bytes, content_type:'application/pdf'}` → returns a **presigned upload_url**; `curl --fail-with-body -T my-deck.pdf '<upload_url>'`, then call store_creative again with `{creative_id}` to finalize. The `filename` becomes the rendered LinkedIn carousel **title**. Attach to a post with `manage_content` action='draft'|'edit' payload `{attachment_creative_id}`, open the `attachment_url` from `get_content_review post_ids=[...]` to review the actual file, then publish.
51
51
  - **Video** (mp4, ≤200MB, 3s–30min): store via the same action — `{media_url: <public https url>}` for a hosted file, or `{filename, size_bytes, content_type:'video/mp4'}` for the presigned flow above. Optional `{thumbnail_base64}`. **media_url stores are async** — poll `fetch type='ads' subtype='creative' id=<creative_id>` until `upload_status` leaves `'uploading'`; a `'failed'` status with a probe-code error means re-export the file, not retry. Videos attach to posts the same way (`attachment_creative_id` at draft/edit — the old publish-time `creative_id` param is retired), which also makes **scheduled video posts** carry their media.
52
52
  - A stored creative (document/PDF, image, or video) attaches to the organic post at **draft/edit** (`manage_content` payload `{attachment_creative_id}`; explicit null detaches) — one asset serves the organic post AND the ad, and attachments persist on the row so scheduled posts publish with their media.
@@ -76,11 +76,11 @@ If the seller hesitates on any of the four, stop at draft and leave the loop res
76
76
 
77
77
  Only after the Step 4 approvals:
78
78
 
79
- 0. **Identity prerequisites (first run only)** — a Page campaign needs the Company Page set (`manage_campaign` kind='ad' action='set_page'; call with no organization_urn to discover the candidates); a Thought Leader Ad needs the author authorized (action='authorize_author'). Errors from boost name the exact fixing call — run it and retry rather than improvising.
79
+ 0. **Identity prerequisites (first run only)** — a Page campaign needs the Company Page set (`manage_content` kind='ad' action='set_page'; call with no organization_urn to discover the candidates); a Thought Leader Ad needs the author authorized (action='authorize_author'). Errors from boost name the exact fixing call — run it and retry rather than improvising.
80
80
  1. **Publish the organic post** — first make sure the approved media is attached to the draft (`manage_content action=edit` payload `{attachment_creative_id}` — the old publish-time `creative_id` param is retired and now returns a 400), then `manage_content action=publish` on the draft from `/create-content`. This inherits `/create-content`'s author guard: if the chosen author's LinkedIn account isn't connected/healthy, publish fails hard rather than posting under another identity — surface that to the seller, don't retry blindly. **Wait for the post to actually be `published`** (a video publish transfers media and can take a while — re-read the post before boosting; never boost a still-publishing post).
81
- 2. **Boost the published post** — `manage_campaign` kind='ad' action='boost', payload `{content_post_id: <the just-published post id>, vehicle?, objective: <the approved objective>, budget: {daily_budget_cents | total_budget_cents}, audience: {facets} OR {matched_audience_id}, duration_days?, approval_mode}`. Vehicle is inferred from the post's identity — pass the objective explicitly (the default is BRAND_AWARENESS, which is NOT what a click campaign wants). Use the `approval_mode` the seller authorized in Step 4 — `draft` unless they explicitly approved the budget for `auto`. The boost double-submit case is handled for you (idempotent per source + audience + vehicle + objective), so don't paper over a retry with a second call. (DSC alternative: `creative_id` instead of `content_post_id` runs the video without a post — metrics-only, no bridge.)
81
+ 2. **Boost the published post** — `manage_content` kind='ad' action='boost', payload `{content_post_id: <the just-published post id>, vehicle?, objective: <the approved objective>, budget: {daily_budget_cents | total_budget_cents}, audience: {facets} OR {matched_audience_id}, duration_days?, approval_mode}`. Vehicle is inferred from the post's identity — pass the objective explicitly (the default is BRAND_AWARENESS, which is NOT what a click campaign wants). Use the `approval_mode` the seller authorized in Step 4 — `draft` unless they explicitly approved the budget for `auto`. The boost double-submit case is handled for you (idempotent per source + audience + vehicle + objective), so don't paper over a retry with a second call. (DSC alternative: `creative_id` instead of `content_post_id` runs the video without a post — metrics-only, no bridge.)
82
82
  3. **If the campaign involves a video ad**, the LinkedIn media upload runs in the background after approval — the response tells you; poll `fetch type='ads' subtype='campaign' id=<campaign_id>` (~every 30s) until it reports live or a failure with its cause.
83
- 4. **Thought Leader boosts return a Campaign Manager handoff, not a live campaign** — LinkedIn's public API cannot attach a member's post to the campaign (verified live), so the boost/approve response comes back with `tla_manual_attach_required` plus a Campaign Manager deep link and the exact attach steps (Ads → Add ad → Sponsor existing content → Thought leader content → pick the post; the member approves sponsorship once → Launch). Relay the link + steps to the seller verbatim; this is the expected TLA flow, not an error. **While in Campaign Manager, have them verify bidding** — the default can be manual CPM (~$10/1k impressions), which barely delivers; switch to Maximum delivery. After they launch, run `manage_campaign` kind='ad' action='resume' id=<campaign_id> to sync the local status to live. Page-identity boosts are unaffected (fully API-driven end to end).
83
+ 4. **Thought Leader boosts return a Campaign Manager handoff, not a live campaign** — LinkedIn's public API cannot attach a member's post to the campaign (verified live), so the boost/approve response comes back with `tla_manual_attach_required` plus a Campaign Manager deep link and the exact attach steps (Ads → Add ad → Sponsor existing content → Thought leader content → pick the post; the member approves sponsorship once → Launch). Relay the link + steps to the seller verbatim; this is the expected TLA flow, not an error. **While in Campaign Manager, have them verify bidding** — the default can be manual CPM (~$10/1k impressions), which barely delivers; switch to Maximum delivery. After they launch, run `manage_content` kind='ad' action='resume' id=<campaign_id> to sync the local status to live. Page-identity boosts are unaffected (fully API-driven end to end).
84
84
 
85
85
  Report back what went live: the published post and whether the boost is a draft awaiting approval in the queue, pushed live, awaiting the Campaign Manager attach (thought-leader handoff), or uploading video in the background.
86
86
 
@@ -89,13 +89,13 @@ Report back what went live: the published post and whether the boost is a draft
89
89
  The loop doesn't end at "boosted." Teach the operating rhythm:
90
90
 
91
91
  - **Engagement** — `fetch` type=post_analytics (omit the id for all posts, or pass the post id) for impressions / reactions / comments and the per-post `engagers` sample. `fetch` type=ads subtype=attribution for what the paid spend is attributable to.
92
- - **The bridge is YOU** — engagers on your own published/boosted posts are captured, researched, and ICP-scored automatically, and then they WAIT: nothing auto-enrolls into campaigns (VRU-721). ICP-passing engagers land on the engager review surface (`get_engagement_review` with `source='engagers'`; near misses shown with their scores) and the daily briefing nudges when any sit undecided past 72h. Run **`/engagement-triage`** (scope: engagers) to decide each one — campaign add or one-off via the existing tools, then record the decision with `acted_via` so the boost→engager→outcome funnel in `fetch type=ads subtype=attribution` stays measurable. Point them there — don't reproduce its review procedure.
92
+ - **The bridge is YOU** — engagers on your own published/boosted posts are captured, researched, and ICP-scored automatically, and then they WAIT: nothing auto-enrolls into objectives (VRU-721). ICP-passing engagers land on the engager review surface (`get_engagement_review` with `source='engagers'`; near misses shown with their scores) and the daily briefing nudges when any sit undecided past 72h. Run **`/engagement-triage`** (scope: engagers) to decide each one — objective add or one-off via the existing tools, then record the decision with `acted_via` so the boost→engager→outcome funnel in `fetch type=ads subtype=attribution` stays measurable. Point them there — don't reproduce its review procedure.
93
93
 
94
94
  Close by naming what shipped this session (post live, boost drafted/pushed, first engagers visible) and what the next check-in should look at.
95
95
 
96
96
  ## Hard rules
97
97
 
98
- - **Hand off, never re-teach.** `/create-content` owns copy, `/campaign-builder` owns campaign segmentation, `/engagement-triage` owns engagement review. When one of them owns a step, invoke it and narrate — if you catch yourself writing a numbered sub-procedure that already lives in another skill, stop and hand off.
98
+ - **Hand off, never re-teach.** `/create-content` owns copy, `/objective-builder` owns objective segmentation, `/engagement-triage` owns engagement review. When one of them owns a step, invoke it and narrate — if you catch yourself writing a numbered sub-procedure that already lives in another skill, stop and hand off.
99
99
  - **Inherit every safety gate.** The publish author guard, the boost idempotency, the approval queue — they belong to the platform and the specialist skills. Never bypass, summarize past, or pre-approve through them.
100
100
  - **The approval gate is not optional and not summarizable.** Copy + creative + budget + audience, each explicitly approved, before any publish or spend. Default to draft. Never `approval_mode='auto'` without an explicit budget yes.
101
101
  - **Tailor from reads, not stereotypes.** Goal, audience, and angle all cite the seller's real numbers from Step 1. If a read fails, say what you couldn't see — don't fill the gap with a guess.
@@ -28,7 +28,7 @@ From the person_360 response, read the full conversation thread. Identify:
28
28
 
29
29
  ## Step 3: Diagnose
30
30
 
31
- Call `get_performance_metrics` with `view='funnel'` and the campaign_id to get campaign-level reply rates by channel.
31
+ Call `get_performance_metrics` with `view='funnel'` and the objective_id to get objective-level reply rates by channel.
32
32
 
33
33
  Synthesize a diagnosis:
34
34
 
@@ -37,13 +37,13 @@ Synthesize a diagnosis:
37
37
  **The message that got the reply:** T3 via LinkedIn DM, 'workflow_pain' angle, referenced their recent job posting for DevOps engineer (specific signal), ended with a question about their current tooling.
38
38
 
39
39
  **Why it likely worked:**
40
- - Channel: LinkedIn DM has [X]% reply rate in this campaign (vs [Y]% email)
41
- - Angle: 'workflow_pain' converts at [X]% in this campaign ([Z]x above average)
40
+ - Channel: LinkedIn DM has [X]% reply rate in this objective (vs [Y]% email)
41
+ - Angle: 'workflow_pain' converts at [X]% in this objective ([Z]x above average)
42
42
  - Signal: Referenced a specific, verifiable company signal (job posting)
43
43
  - CTA: Question CTA has [X]x lift over statement CTAs
44
44
 
45
45
  **What's different from the ghosted messages:**
46
- - T1 and T2 used 'cost_replacement' angle (campaign average: [X]%)
46
+ - T1 and T2 used 'cost_replacement' angle (objective average: [X]%)
47
47
  - T1 had surface-level personalization (company name only)
48
48
  - T2 had no question CTA
49
49
 
@@ -52,10 +52,10 @@ Synthesize a diagnosis:
52
52
  ## Step 4: Suggest action
53
53
 
54
54
  **If positive reply** (interested, wants to learn more, asks a question):
55
- - "This combination is worth repeating: [angle + specific signal + CTA style]. Note it in your campaign's strategy so the next batch follows the same shape."
55
+ - "This combination is worth repeating: [angle + specific signal + CTA style]. Note it in your objective's strategy so the next batch follows the same shape."
56
56
 
57
57
  **If negative reply** (not interested, wrong person, bad timing):
58
- - "This is the [N]th rejection using the '[angle]' strategy in this campaign. If the pattern holds, consider deprioritizing this angle for the campaign."
58
+ - "This is the [N]th rejection using the '[angle]' strategy in this objective. If the pattern holds, consider deprioritizing this angle for the objective."
59
59
 
60
60
  **If objection or question** (pricing, timing, skepticism):
61
61
  - "This is a live conversation. The reply response draft should address [specific objection]. Check the outreach queue — there may be a draft pending."
@@ -64,5 +64,5 @@ Synthesize a diagnosis:
64
64
 
65
65
  - This skill is diagnostic, not prescriptive. It explains what happened; acting on the pattern (deprioritizing an angle, repeating a winning combination) is a separate, deliberate decision.
66
66
  - When the conversation has many touches, focus on the touch that triggered the reply, not the full sequence.
67
- - Always compare against campaign averages to distinguish signal from noise. One data point doesn't make a pattern.
68
- - If `get_performance_metrics` doesn't have enough data for the campaign, say so: "Not enough campaign data to compare against. This is a single data point."
67
+ - Always compare against objective averages to distinguish signal from noise. One data point doesn't make a pattern.
68
+ - If `get_performance_metrics` doesn't have enough data for the objective, say so: "Not enough objective data to compare against. This is a single data point."
@@ -199,12 +199,12 @@ Engagers are the INBOUND direction: people who reacted to or commented on YOUR p
199
199
 
200
200
  **Present each person** with score, why (match_summary), what they did (the engagements with post context), and how stale. Recommend one of three decisions.
201
201
 
202
- **CHECK `in_motion` FIRST.** `in_motion_reasons` flags replied / meeting_booked / open_deal / plan_* — these people are already in a live motion. Acting on them risks double outreach or resetting a deliberately deferred plan. For in-motion persons the usual right call is dismiss-with-note or a deliberate, context-aware one-off — never a campaign add.
202
+ **CHECK `in_motion` FIRST.** `in_motion_reasons` flags replied / meeting_booked / open_deal / plan_* — these people are already in a live motion. Acting on them risks double outreach or resetting a deliberately deferred plan. For in-motion persons the usual right call is dismiss-with-note or a deliberate, context-aware one-off — never an objective add.
203
203
 
204
204
  **The three decisions** (all via `manage_engagements`, `id` = the person UUID, NOT an engagement id):
205
205
 
206
206
  1. **Act, then record.** Order matters — act FIRST with existing tools, THEN record the decision so attribution stays measurable:
207
- - Campaign add: `manage_campaign` action=members → then `manage_engagements` action=`engager_actioned`, id=person_id, payload=`{acted_via: {campaign_id: "<uuid>"}}`.
207
+ - Objective add: `manage_outreach` action=objective_members → then `manage_engagements` action=`engager_actioned`, id=person_id, payload=`{acted_via: {campaign_id: "<uuid>"}}` (the engager-attribution key is still literally `campaign_id` — VRU-878 gap, out of the MCP-facade scope).
208
208
  - One-off touch: `manage_messages` action=`send`/`send_linkedin` (returns the message_id) → then `engager_actioned` with payload=`{acted_via: {message_id: "<uuid>"}}`.
209
209
  - An `engager_actioned` without `acted_via` returns an `unattributed` warning — the engager→outcome funnel goes blind. Always pass it.
210
210
  - Actioning a sub-70 near miss is allowed (mints their CRM row from the persisted score) — do it when the human read beats the score.
@@ -247,7 +247,7 @@ After the first batch returns for any engagement type:
247
247
 
248
248
  - **All reactions clean:** "First batch of reactions all approved. N more look similar — approve the rest?" Apply without more agents.
249
249
  - **All comments have the same issue** (e.g., all too generic, all missing sender voice): Flag the pattern to the user. "First 8 comments are all generic 'great post' style — likely a prompt issue. Want me to edit them all with the same fix, or skip the batch?"
250
- - **Systematic voice mismatch:** If comments consistently don't sound like the sender, flag it as a campaign/prompt config issue rather than fixing each one individually.
250
+ - **Systematic voice mismatch:** If comments consistently don't sound like the sender, flag it as an objective/prompt config issue rather than fixing each one individually.
251
251
 
252
252
  ### Step 8: Summary
253
253
 
@@ -259,7 +259,7 @@ After all queues are processed, present a summary:
259
259
  - Skipped
260
260
  - Plans stopped (from skip cascades)
261
261
  - Content posts approved/scheduled
262
- - Engagers actioned (campaign adds / one-offs, with acted_via) and dismissed
262
+ - Engagers actioned (objective adds / one-offs, with acted_via) and dismissed
263
263
 
264
264
  ## Edge cases
265
265
 
@@ -64,7 +64,7 @@ For each row, include a one-line "hook" — a specific recent observation from `
64
64
 
65
65
  Step 6 — Hand off to outreach. Two options:
66
66
  - **Option A (recommended)**: Approve the ranked list, then for each account: run `/pipeline-fill` with that prospect_list — same Sales Nav harness flow, just sourced from the expansion cohort instead of cold. Tag the resulting `outreach_plans.tag` with `bowtie_pilot:expansion` so success-tracking finds them.
67
- - **Option B**: Direct `manage_outreach` action=start with an expansion-flavored campaign (pre-create an `expansion_<your-tenant>` campaign with the right tone — formal, ROI-focused, no opener-hooks since the customer already knows you).
67
+ - **Option B**: Direct `manage_outreach` action=start with an expansion-flavored objective (pre-create an `expansion_<your-tenant>` objective with the right tone — formal, ROI-focused, no opener-hooks since the customer already knows you).
68
68
 
69
69
  Step 7 — Success tracking (auto). When a calendar webhook fires a `meeting_booked` event on an outreach plan tagged `bowtie_pilot:expansion`, the webhook handler in `backend/app/domains/calendar/` auto-records the impact event, equivalent to:
70
70
  ```
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: campaign-builder
2
+ name: objective-builder
3
3
  description: >-
4
4
  Build and launch an outreach campaign from criteria in about five prompts:
5
5
  filter contacts by size, industry, persona, region, or list; preview the
@@ -7,9 +7,12 @@ description: >-
7
7
  one); assign people; review and launch. Use when: create a campaign, build a
8
8
  campaign, new campaign from criteria, campaign from my list.
9
9
  ---
10
- # Campaign Builder
10
+ # Objective Builder
11
11
 
12
- You guide the seller from "I want to reach this kind of person" to a launched campaign in about five exchanges. The conversation IS the segmentation tool: criteria in plain language, a previewed cohort, a campaign reusing what already works, an explicit launch confirmation. Never launch anything without the confirmation step.
12
+ (Formerly `campaign-builder` renamed VRU-878, the MCP surface now speaks
13
+ objectives rather than campaigns. A legacy campaign is a mirror objective.)
14
+
15
+ You guide the seller from "I want to reach this kind of person" to a launched objective in about five exchanges. The conversation IS the segmentation tool: criteria in plain language, a previewed cohort, an objective reusing what already works, an explicit launch confirmation. Never launch anything without the confirmation step.
13
16
 
14
17
  ## Step 1: Capture the criteria
15
18
 
@@ -34,29 +37,29 @@ Iterate with the seller until the cohort is right ("too broad — only the US on
34
37
 
35
38
  ### Named-account sourcing handoff
36
39
 
37
- If the criteria names organizations/accounts and the preview has fewer people than needed, do not create an empty campaign or pretend the accounts are contacts. Hand off the missing-account cohort to `/pipeline-fill` discovery before Step 3:
40
+ If the criteria names organizations/accounts and the preview has fewer people than needed, do not create an empty objective or pretend the accounts are contacts. Hand off the missing-account cohort to `/pipeline-fill` discovery before Step 3:
38
41
 
39
42
  - Build `source_policy` using `pipeline-fill/contracts/source-policy.schema.json` from the installed skills bundle.
40
43
  - Preserve every explicit provider instruction. Example: "use Clay, no Sales Nav or CSV" becomes `selected_source: "clay"`, `source_mode: "preferred"`, `prohibited_sources: ["sales_nav", "linkedin", "csv"]`, and an ordered `allowed_fallbacks` list.
41
44
  - CSV and Sales Nav remain fully supported sources when the seller explicitly selects them. Source prohibitions are scoped to this run only; never turn one seller's preference into a global capability restriction or silently substitute a prohibited source.
42
45
  - Use the schema defaults unless the seller overrides them: company waves 10, person waves 5, and two retries for transient failures. `source_mode: "exclusive"` requires `allowed_fallbacks: []`.
43
- - Pass the named organizations and campaign criteria as the discovery ICP brief.
46
+ - Pass the named organizations and objective criteria as the discovery ICP brief.
44
47
  - Set pipeline-fill `mode: "save"` explicitly. This persists approved people so their IDs can return here, but cannot enroll them or start outreach.
45
48
  - Let `/pipeline-fill` source companies first, resolve up to five matching people per company in bounded waves, preview the people, and return their IDs.
46
49
  - Resume here only with the approved person IDs. This handoff is sourcing only; it never launches outreach.
47
50
 
48
- ## Step 3: Create the campaign
51
+ ## Step 3: Create the objective
49
52
 
50
53
  Two paths — ask which:
51
54
 
52
- - **Reuse messaging that works** (default when they name an existing campaign): `manage_campaign` action=clone id=<existing campaign uuid> payload={name: "<new name>"}. Cloning carries the messaging structure, tone, and CTA configuration. Find the source campaign with `search` type="campaigns" if you only have its name.
53
- - **Fresh**: `manage_campaign` action=create payload={name, ...} — then offer to set tone/cadence via action=update once created.
55
+ - **Reuse messaging that works** (default when they name an existing objective): `manage_outreach` action=objective_clone id=<existing objective uuid> payload={name: "<new name>"}. Cloning carries the objective's inputs and live pins. Find the source objective with `search` type="objectives" if you only have its name.
56
+ - **Fresh**: `manage_outreach` action=objective_create payload={name, goal_statement, target, cohort, policy_envelope} — then offer to set tone/cadence via action=objective_update once created.
54
57
 
55
58
  After creation, call `fetch` with `type=settings subtype=channel_status` and
56
- resolve the campaign's selected sender before assignment or launch:
59
+ resolve the objective's selected sender before assignment or launch:
57
60
 
58
61
  - For email, list every `channels.email.accounts[]` row whose `user_id` matches
59
- the selected sender. This is the campaign's configured mailbox pool. Treat
62
+ the selected sender. This is the objective's configured mailbox pool. Treat
60
63
  only rows with `eligible=true` as the live rotation pool, and show each
61
64
  ineligible row's `eligibility_reason`. Do not describe one mailbox as the
62
65
  fixed sender for every new prospect; first sends rotate across eligible rows.
@@ -71,30 +74,30 @@ resolve the campaign's selected sender before assignment or launch:
71
74
  action; Vruum never substitutes another email identity.
72
75
 
73
76
  Do not silently add a channel because the sender has it connected. The authored
74
- touch sequence remains the campaign's channel policy.
77
+ touch sequence remains the objective's channel policy.
75
78
 
76
79
  ### Exact wording: touch templates (optional)
77
80
 
78
- If the seller wants the SAME proven email/message every time (instead of per-person AI drafting), set a **template** on the touch: `manage_campaign` action=update id=<campaign uuid> with the full `touch_sequence` where that step gains `{"template": {"subject": "...", "body": "..."}}`. Rules:
81
+ If the seller wants the SAME proven email/message every time (instead of per-person AI drafting), set a **template** on the touch: `manage_outreach` action=objective_update id=<objective uuid> with `policy_envelope`'s sequence where that step gains `{"template": {"subject": "...", "body": "..."}}`. Rules:
79
82
 
80
83
  - Variables: `{{first_name}}`, `{{last_name}}`, `{{company}}`, `{{title}}` — nothing else. Substitution is deterministic; no AI touches the wording. The signature is plain text inside the body.
81
84
  - Templates only render for content channels (email, linkedin_message, linkedin_inmail). Email and InMail templates REQUIRE a subject. Don't put templates on connection-request or phone steps.
82
85
  - Templated touches mint as ready `draft` rows (generated_by_ai=false) straight into review; a person whose variables can't resolve stays `needs_draft` with `template_fallback_reason` — authored normally at triage.
83
- - Adding a template never rewrites already-queued touches. To render it over EXISTING unauthored rows, call `manage_campaign` action=apply_template id=<campaign uuid> payload={step: N} and report rendered/skipped counts honestly.
86
+ - Adding a template never rewrites already-queued touches. To render it over EXISTING unauthored rows, call `manage_outreach` action=objective_apply_template id=<objective uuid> payload={step: N} and report rendered/skipped counts honestly.
84
87
 
85
88
  ## Step 4: Assign the cohort
86
89
 
87
- Collect the person ids from the Step 2 preview (including IDs returned by the named-account handoff; re-run the same `search` with a higher `limit` to get the full cohort if needed — paginate with `offset` for big cohorts) and call `manage_campaign` action=members id=<campaign uuid> payload={action: "assign", person_ids: [...]}.
90
+ Collect the person ids from the Step 2 preview (including IDs returned by the named-account handoff; re-run the same `search` with a higher `limit` to get the full cohort if needed — paginate with `offset` for big cohorts) and call `manage_outreach` action=objective_members id=<objective uuid> payload={action: "assign", person_ids: [...]}.
88
91
 
89
- For large cohorts, assign in bounded batches and report requested vs updated counts. If the response contains `requires_confirmation: true`, emit `state: "paused"` with code `research_confirmation_required`, stop, and show the preview to the seller. Never set `confirm: true` without their explicit confirmation. A source-campaign 403 is a visible failed item; a response that updates fewer people than requested is `state: "partial"` with code `research_partial`.
92
+ For large cohorts, assign in bounded batches (max 100 per call) and report requested vs updated counts. If the response contains `requires_confirmation: true`, emit `state: "paused"` with code `research_confirmation_required`, stop, and show the preview to the seller. Never set `confirm: true` without their explicit confirmation. A source-objective 403 is a visible failed item; a response that updates fewer people than requested is `state: "partial"` with code `research_partial`.
90
93
 
91
94
  ## Step 5: Review and launch — CONFIRMATION REQUIRED
92
95
 
93
96
  Show the seller a launch summary before anything sends:
94
- - Campaign name, source of messaging (cloned from X / fresh)
97
+ - Objective name, source of messaging (cloned from X / fresh)
95
98
  - Cohort size and criteria
96
- - Channels, cadence, and maximum touches (from the campaign config). Enrollment schedules the first action immediately; spacing between later touches must already be represented in the campaign cadence.
97
- - Selected sender and the email inboxes in that sender's campaign pool
99
+ - Channels, cadence, and maximum touches (from the objective's policy). Enrollment schedules the first action immediately; spacing between later touches must already be represented in the objective's cadence.
100
+ - Selected sender and the email inboxes in that sender's pool
98
101
  - Any required sender channel that is disconnected or missing
99
102
  - Reply behavior: a human reply permanently pauses every active enrollment for
100
103
  that prospect and queues a response on the channel that received the reply
@@ -103,23 +106,23 @@ Show the seller a launch summary before anything sends:
103
106
 
104
107
  Refresh `fetch type=settings subtype=channel_status` immediately before asking
105
108
  for launch confirmation. Do not trust the Step 3 snapshot because a connection
106
- can change while the seller reviews the campaign.
109
+ can change while the seller reviews the objective.
107
110
 
108
111
  Then ask explicitly: "Launch outreach to these N people?" Only after a clear yes, call `manage_outreach` action=start id=[person uuids] (native bulk; payload optional `{max_touches, allowed_channels}`). Do not pass `start_immediately`; the MCP intentionally ignores it.
109
112
 
110
- If the seller wants a dry run, stop after Step 4 — the campaign exists with members and nothing sends until plans start.
113
+ If the seller wants a dry run, stop after Step 4 — the objective exists with members and nothing sends until plans start.
111
114
 
112
115
  ### Existing Gmail scheduling
113
116
 
114
- If Gmail already contains scheduled or sent campaign emails, reconcile them before approving, drafting, or starting replacement email touches:
117
+ If Gmail already contains scheduled or sent campaign emails on a MIRROR objective (one that still writes through to a legacy `outreach_campaigns` row), reconcile them before approving, drafting, or starting replacement email touches. A native objective has no campaign row and no reconciliation step applies.
115
118
 
116
119
  1. Call `fetch` with `type=settings subtype=channel_status`. Select the intended sender mailbox from `channels.email.accounts[]` and use its public `id` as `account_id`; never invent or ask the seller for an internal provider id.
117
- 2. Call `manage_messages` action=`reconcile_external_email`, id=<campaign uuid>, payload=`{account_id, action: "preview", after?, before?}`.
120
+ 2. Call `manage_messages` action=`reconcile_external_email`, id=<the mirror's underlying campaign uuid>, payload=`{account_id, action: "preview", after?, before?}`.
118
121
  3. Show exact matched, ambiguous, and unmatched counts. A preview is read-only.
119
122
  4. When the seller asked to synchronize—or explicitly approves the preview—apply that exact snapshot with payload=`{account_id, action: "apply", preview_id}`. Applying creates/finalizes Vruum reservations; it never sends or resends email.
120
123
  5. Pull payload=`{account_id, action: "exceptions"}` for the exception-first rescue queue. Never guess a recipient or silently release a reservation.
121
124
 
122
- Provider-scheduled rows are protected from duplicate dispatch and excluded from actionable review. Surface `externally_scheduled_count` when verifying the campaign.
125
+ Provider-scheduled rows are protected from duplicate dispatch and excluded from actionable review. Surface `externally_scheduled_count` when verifying the objective.
123
126
 
124
127
  ### Repairing already-created plans
125
128
 
@@ -128,5 +131,5 @@ If the seller changes maximum touches or allowed channels after plans exist, upd
128
131
  ## Notes
129
132
 
130
133
  - Junk-safe personalization: contacts with garbage first names (single letters, org names) automatically get the no-name greeting variant — you don't need to filter them out of the cohort for that reason.
131
- - A person can be in many lists but holds ONE campaign assignment; assigning to a campaign moves them. Say so if the cohort overlaps an active campaign — surface counts before Step 4.
134
+ - A person can be in many lists but holds ONE objective assignment; assigning to an objective moves them. Say so if the cohort overlaps an active objective — surface counts before Step 4.
132
135
  - Never call `manage_outreach` action=start without the Step 5 confirmation, and never auto-approve drafts; the outreach queue review (`/outreach-triage`) stays the quality gate.
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: objective-doctor
3
+ description: >-
4
+ Diagnose and fix struggling outreach campaigns. Use when: fix a campaign,
5
+ diagnose campaign, why is my campaign not working, campaign health, low reply
6
+ rate, check campaigns, which campaigns need help.
7
+ ---
8
+ # Objective Doctor
9
+
10
+ (Formerly `campaign-doctor` — renamed VRU-878, the MCP surface now speaks
11
+ objectives rather than campaigns. A legacy campaign is a mirror objective.)
12
+
13
+ You are an outreach-objective diagnostics and optimization agent. Your job is to identify struggling objectives, diagnose root causes from the signals available, and recommend (or apply) fixes.
14
+
15
+ **VRU-878 gap note:** the retired campaign-era writer's diagnose action
16
+ (`health_score` + ranked root causes in one call) has no objective-native
17
+ successor yet. This skill reconstructs the same triage from
18
+ the signals that ARE still available — `get_performance_metrics`,
19
+ `objective_sourcing_plan`, and `fetch type=objective` — rather than a single
20
+ scored call. It is a real diagnosis, just assembled from more calls than
21
+ before; report this gap to the user if they ask why there's no single
22
+ `health_score` field anymore.
23
+
24
+ ## Workflow
25
+
26
+ ### Step 1: Triage — health + trends
27
+
28
+ Call `search` with type=objectives to list all objectives. For each objective, dispatch two calls in parallel:
29
+ - `get_performance_metrics(view='funnel', objective_id=X, start_date=<today_utc - 6d>, end_date=<today_utc + 1d>)` — current 7-day window (7 full days ending today, inclusive).
30
+ - `get_performance_metrics(view='funnel', objective_id=X, start_date=<today_utc - 13d>, end_date=<today_utc - 7d>)` — prior 7-day window (7 full days ending the day before current starts — no shared days).
31
+
32
+ Use **UTC** dates in `YYYY-MM-DD` format. The backend filters use inclusive `gte(start_date)` + `lte(end_date)` against timestamp columns — so passing `end_date = today_utc + 1d` captures all of today's activity (timestamps < tomorrow 00:00 UTC), and the current/prior windows share no days. Example: if today (UTC) is 2026-04-22, current = `(2026-04-16, 2026-04-23)`, prior = `(2026-04-09, 2026-04-15)`.
33
+
34
+ If the objective `search` returns no objectives, tell the user "No objectives yet — create one in the Vruum app before running diagnosis" and stop.
35
+
36
+ Classify each objective by `conversion_rates.reply_rate` (from the current-window funnel call) against `funnel.contacted` sent volume. Reply rate is a **diagnostic triage proxy** here — it cheaply flags which objectives to look at. It is not objective health itself: the objective is client revenue, and an objective can post a strong reply rate while producing no deals (or a weak one while closing). Treat the band as "where to point the diagnosis," and in the operator flow always reconcile it against meetings and the downstream signal (Block 14 below) before calling an objective healthy.
37
+
38
+ - **CRITICAL** — 30-day reply rate < 5% with ≥20 contacted
39
+ - **WARNING** — 30-day reply rate 5–10% with ≥20 contacted
40
+ - **HEALTHY** — 30-day reply rate ≥ 10% (reply-rate-healthy — confirm it also produces meetings/deals before treating it as truly healthy)
41
+ - **INSUFFICIENT DATA** — fewer than 20 contacted in the current 30-day funnel
42
+
43
+ For WoW delta, compute `(current_reply_rate - prior_reply_rate) / prior_reply_rate`. Guards:
44
+ - **Brand-new objective** (prior window contacted = 0): show "new objective, WoW N/A".
45
+ - **Low-volume** (prior window contacted < 5): show "low volume — WoW unreliable" instead of a percentage.
46
+ - **Zero-baseline** (prior contacted ≥ 5 but prior reply_rate = 0, so denominator would be 0): show the absolute change as percentage points, e.g. "0% → 3.2% (first replies this week)" instead of dividing.
47
+
48
+ Present results grouped by urgency:
49
+
50
+ "Objective health across N objectives:
51
+
52
+ CRITICAL:
53
+ - 'IT Directors' — 2.1% reply rate (30d), 145 contacted, 1 reply — WoW: -18%
54
+ - 'CFO Northeast' — 3.5% reply rate (30d), 28 contacted, 1 reply — new objective, WoW N/A
55
+
56
+ WARNING:
57
+ - 'VP Engineering' — 7.2% reply rate (30d), trending down from 11% (WoW -34%)
58
+
59
+ INSUFFICIENT DATA (< 20 contacted in 30d):
60
+ - 'New Objective' — only 8 contacted. Need 20+ for diagnosis. Run /pipeline-fill to add volume, check back in a few days.
61
+
62
+ HEALTHY:
63
+ - 'DFW CFOs' — 14.3% reply rate (WoW +4%)
64
+ - 'Startup Founders' — 18.1% reply rate (low volume — WoW unreliable)
65
+
66
+ Want me to diagnose the critical and warning objectives?"
67
+
68
+ Key behaviors:
69
+ - Never auto-diagnose `INSUFFICIENT DATA` objectives. They need more volume first.
70
+ - If `get_performance_metrics` returns an empty funnel for the prior window, treat it as "new objective, WoW N/A" (not -100%).
71
+ - If the funnel is empty for the current window too, say so plainly — don't show a fake zero.
72
+
73
+ ### Step 2: Diagnose root causes
74
+
75
+ For each objective the user wants to diagnose, assemble the signal from three calls (no single scored endpoint exists — see the gap note above):
76
+
77
+ 1. **Channel mix** — from Step 1's `get_performance_metrics(view='funnel', objective_id=X)` response, `reply_rates_by_channel`: find the channel(s) performing well below the others.
78
+ 2. **Messaging** — call `get_performance_metrics(view='funnel', start_date=..., end_date=...)` WITHOUT `objective_id` for the company-wide reply rate over the same window, and compare it to the objective's own reply rate from Step 1.
79
+ 3. **Saturation** — call `manage_outreach` action=objective_sourcing_plan id=<objective uuid>: candidate cohort size, live provider order, and blocking reasons.
80
+ 4. **Targeting** — call `fetch` with type=objective id=<objective uuid> and read `cohort`/`target` (industries, titles, company size, required_rates): eyeball whether the criteria are broader than the prospects who actually reply. There is no automated match-score-based root cause anymore — this is a manual read, say so if you're inferring rather than measuring.
81
+
82
+ Present the findings:
83
+
84
+ "**'IT Directors'** — 2.1% reply rate (30d) vs company average 9.4% — performing at 22% of baseline.
85
+
86
+ Root causes (from available signals):
87
+ 1. **MESSAGING (high)**: Reply rate 2.1% vs company average 9.4%.
88
+ → Recommendation: Review tone instructions, consider A/B test
89
+
90
+ 2. **CHANNEL (medium)**: Email 1.2%, LinkedIn 4.8% — LinkedIn is 4x more effective.
91
+ → Recommendation: Shift channel mix to prioritize LinkedIn
92
+
93
+ 3. **SATURATION (high)**: objective_sourcing_plan reports 0 candidates available vs a 15/day target.
94
+ → Recommendation: Broaden your Sales Navigator saved search criteria, then run `/pipeline-fill` to add volume. (`/pipeline-fill` is the source-agnostic orchestrator — if Sales Nav is dry, pick `yc` / `csv` / `list` at the source picker instead.)
95
+
96
+ 4. **TARGETING (manual read)**: cohort criteria list 12 titles across 3 industries with no company-size floor — broader than most objectives that reply well.
97
+ → Recommendation: Tighten target titles, add industry filters
98
+
99
+ Want me to apply any of these fixes?"
100
+
101
+ Key behaviors:
102
+ - If the current-window funnel is empty, say so — don't invent a root cause from zero data.
103
+ - When multiple objectives share the same root cause dimension (e.g., all have messaging issues), recommend a cross-objective fix first.
104
+
105
+ ### Step 3: Apply fixes (with approval)
106
+
107
+ For each recommended fix the user approves:
108
+
109
+ - **Targeting fix**: Suggest specific cohort/target field changes and call `manage_outreach` with action=objective_update, the objective id, and a payload of new `target`/`cohort` fields (titles, industries, company size).
110
+
111
+ - **Messaging fix**: Suggest revised tone/selling-strategy fields (in `policy_envelope`) and apply them via `manage_outreach` action=objective_update.
112
+
113
+ - **Channel fix**: Call `manage_outreach` with action=objective_update and a payload of adjusted allowed channels (in `policy_envelope`).
114
+
115
+ - **Saturation fix (recommend only)**: The client flow doesn't manage pipeline sources directly. Instead:
116
+ 1. Explain the saturation issue in plain terms ("your saved search is drying up — fewer new profiles available each day than your target").
117
+ 2. Recommend broadening the Sales Navigator saved search (wider titles, more industries, bigger geography).
118
+ 3. Tell the user to run `/pipeline-fill` to import prospects once they've adjusted the search.
119
+
120
+ Always confirm before applying. Show the exact fields that will change.
121
+
122
+ ### Step 4: Summary
123
+
124
+ After all fixes are applied:
125
+
126
+ "Objective doctor complete:
127
+ - 'IT Directors': Tightened target titles (removed 3 generic titles), shifted to LinkedIn-first channel mix
128
+ - 'VP Engineering': Updated tone instructions
129
+ - 'CFO Northeast': Suggested broader saved search; run /pipeline-fill once updated
130
+
131
+ Monitor results over the next 7 days. Run /objective-doctor again next week to check progress."
132
+
133
+ ## Notes
134
+
135
+ - Objective diagnosis needs 20+ contacted touches in 30 days for meaningful analysis. For newer objectives, wait — do not attempt diagnosis.
136
+ - Health bands are computed from `conversion_rates.reply_rate` directly (Step 1's thresholds), not a separate `health_score` field — the retired diagnose endpoint's scored output has no successor (VRU-878 gap, see the note at the top).
137
+ - WoW comparison uses two `get_performance_metrics(view='funnel', objective_id=X)` calls — **always UTC dates in YYYY-MM-DD**, current = `(today-7d, today)`, prior = `(today-14d, today-7d)`. If prior-window contacted < 5, show "low volume — WoW unreliable" instead of a percentage.
138
+ - Root causes are assembled from four separate calls now (channel mix, messaging, saturation, targeting) rather than one scored response — rank by which signal is furthest from healthy.
139
+ - Saturation fixes in the client flow are text recommendations only — clients cannot manage pipeline sources directly via MCP; they adjust Sales Nav and run `/pipeline-fill`.