@vruum/skills 0.6.5 → 0.6.6
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/vruum-deal-reviewer.md +5 -8
- package/agents/vruum-engagement-reviewer.md +17 -15
- package/agents/vruum-outreach-reviewer.md +11 -13
- package/package.json +2 -2
- package/skills/campaign-doctor/SKILL.md +13 -13
- package/skills/create-content/SKILL.md +15 -15
- package/skills/csv-pipeline-fill/SKILL.md +3 -3
- package/skills/deal-triage/SKILL.md +14 -14
- package/skills/engagement-triage/SKILL.md +15 -13
- package/skills/engagement-triage/references/content-subagent-instructions.md +1 -1
- package/skills/engagement-triage/references/engagement-subagent-instructions.md +13 -11
- package/skills/enrich-prospect/SKILL.md +5 -5
- package/skills/expansion-fill/COHORT-QUERIES.md +18 -17
- package/skills/expansion-fill/SKILL.md +20 -17
- package/skills/outreach-triage/SKILL.md +10 -10
- package/skills/outreach-triage/references/subagent-instructions.md +5 -5
- package/skills/pipeline-fill/RESEARCH-ENGINE.md +36 -33
- package/skills/pipeline-fill/SKILL.md +10 -10
- package/skills/sales-nav-deep-fill/SKILL.md +4 -4
- package/skills/winback-fill/COHORT-QUERIES.md +9 -9
- package/skills/winback-fill/SKILL.md +19 -16
- package/skills/yc-pipeline-fill/SKILL.md +4 -4
|
@@ -67,20 +67,22 @@ Reactions are usually KEEP. Only edit if reaction_type is wrong for the post con
|
|
|
67
67
|
### 2e. Budget check
|
|
68
68
|
If `budget_status` shows the sender account near daily limits, note it in REASONING (the operator may want to defer some approvals).
|
|
69
69
|
|
|
70
|
-
## Step 3: Apply the uplift via
|
|
70
|
+
## Step 3: Apply the uplift via manage_engagements
|
|
71
71
|
|
|
72
|
-
When RECOMMENDATION=uplifted, write back via `
|
|
72
|
+
When RECOMMENDATION=uplifted, write back via `manage_engagements`:
|
|
73
73
|
|
|
74
74
|
```
|
|
75
|
-
|
|
76
|
-
engagement_ids="<id>",
|
|
75
|
+
manage_engagements(
|
|
77
76
|
action="edit",
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
id="<id>",
|
|
78
|
+
payload={
|
|
79
|
+
"content": "<your uplifted comment>",
|
|
80
|
+
"polish_provenance": {
|
|
81
|
+
"source": "skill",
|
|
82
|
+
"model": "<your model — claude-opus-4-7 / claude-sonnet-4-6 / etc.>",
|
|
83
|
+
"at": "<ISO8601 timestamp>",
|
|
84
|
+
"rewrite_notes": "<one line: what changed and why>"
|
|
85
|
+
}
|
|
84
86
|
}
|
|
85
87
|
)
|
|
86
88
|
```
|
|
@@ -116,6 +118,6 @@ VALIDATOR_FAILURES_FIXED: {comma-separated codes you cleared, or "none"}
|
|
|
116
118
|
## Error handling
|
|
117
119
|
|
|
118
120
|
- **MCP call failure**: retry once. If it still fails, set RECOMMENDATION=flag with REASONING="MCP error: <message>" and continue with the next item. Don't abort the whole batch.
|
|
119
|
-
- **
|
|
121
|
+
- **manage_engagements write fails (idempotency)**: report failure in REASONING; the operator can re-run. The skill keeps partial state in `/tmp/engagement-triage-$$/pending.jsonl` (parent skill responsibility).
|
|
120
122
|
- **No polished_floor AND no first_draft AND no content**: the queue row is malformed; flag with REASONING="malformed_queue_row, no draft to uplift".
|
|
121
123
|
- **Empty dossier + empty post text**: flag with REASONING="no grounding available".
|
|
@@ -16,13 +16,13 @@ This is not a database lookup. This is an analyst's brief.
|
|
|
16
16
|
|
|
17
17
|
Call these in parallel:
|
|
18
18
|
- `get_person_360` — profile, match analysis, research, activity, outreach plan, deal
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
19
|
+
- `fetch` type=person_research — structured research data (if exists)
|
|
20
|
+
- `fetch` type=company_research — company intelligence
|
|
21
21
|
|
|
22
22
|
If research is thin (no person_research, or match_analysis is null):
|
|
23
|
-
- `
|
|
23
|
+
- `research` action=linkedin_fetch — pull their recent posts and profile
|
|
24
24
|
- WebSearch for "[person name] [company name]" — recent news, talks, publications
|
|
25
|
-
- `
|
|
25
|
+
- `search` type=kb — relevant sales docs
|
|
26
26
|
|
|
27
27
|
## Step 2: Diarize
|
|
28
28
|
|
|
@@ -69,7 +69,7 @@ No embedding search finds these gaps. No keyword filter finds them. You have to
|
|
|
69
69
|
|
|
70
70
|
"Append this diarization as a note? It will appear on the person card timeline and be visible to the outreach agent when writing messages."
|
|
71
71
|
|
|
72
|
-
If approved: call `
|
|
72
|
+
If approved: call `manage_person` action=note (payload={body}) with a condensed version of the diarization (the SAYS/ACTUALLY gap, key signals, and recommended approach). Each call appends a new row to the person's notes timeline — no overwrite of prior notes.
|
|
73
73
|
|
|
74
74
|
## Notes
|
|
75
75
|
|
|
@@ -12,29 +12,29 @@ exposes.
|
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
# Step 1: pull closed-won deals (results are scoped to your session's tenant)
|
|
15
|
-
won =
|
|
15
|
+
won = search(type="deals", filters={"outcome": "won"}, limit=200)
|
|
16
16
|
|
|
17
17
|
# Step 2: keep deals whose actual_close_date is more than 60 days old
|
|
18
18
|
cutoff = now() - 60 days
|
|
19
19
|
won_old = [d for d in won.deals if parse(d.actual_close_date) < cutoff]
|
|
20
20
|
|
|
21
21
|
# Step 3: drop people who currently have an OPEN deal (i.e., a deal with
|
|
22
|
-
# outcome=None on the same person). If you can call
|
|
23
|
-
# filter, post-filter; otherwise rely on Step 3 of the skill workflow
|
|
24
|
-
# get_person_360 surfaces each person's full deal history.
|
|
25
|
-
open_deals =
|
|
22
|
+
# outcome=None on the same person). If you can call search type=deals with no
|
|
23
|
+
# outcome filter, post-filter; otherwise rely on Step 3 of the skill workflow
|
|
24
|
+
# where get_person_360 surfaces each person's full deal history.
|
|
25
|
+
open_deals = search(type="deals", filters={"outcome": None}, limit=500) # NULL outcome = still open
|
|
26
26
|
open_person_ids = { d.person_id for d in open_deals.deals }
|
|
27
27
|
candidates = [d for d in won_old if d.person_id not in open_person_ids]
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
If you have more than 200 won deals, paginate with the `offset` parameter on
|
|
31
|
-
`
|
|
31
|
+
`search`. Cap the candidate list to the most recently won 50 (sort by
|
|
32
32
|
`actual_close_date DESC`) before per-account enrichment.
|
|
33
33
|
|
|
34
34
|
Why this works without a DB query:
|
|
35
35
|
|
|
36
|
-
- `
|
|
37
|
-
tenant.
|
|
36
|
+
- `search` type=deals is tenant-scoped server-side; you don't (and can't)
|
|
37
|
+
choose the tenant.
|
|
38
38
|
- Account-to-person mapping (the old `company_people` JOIN) is resolved by
|
|
39
39
|
`get_person_360` in Step 3 of the skill workflow — call it per surfaced
|
|
40
40
|
person and read `current_positions[0].company_id`.
|
|
@@ -42,8 +42,8 @@ Why this works without a DB query:
|
|
|
42
42
|
## Optional refinement: account stage is tagged
|
|
43
43
|
|
|
44
44
|
If your team has been tagging account lifecycle stages on the Vruum
|
|
45
|
-
dashboard, call `
|
|
46
|
-
the skill's Step 3 enrichment and:
|
|
45
|
+
dashboard, call `fetch(type="account_state", id=<company_id>)` for each
|
|
46
|
+
candidate in the skill's Step 3 enrichment and:
|
|
47
47
|
|
|
48
48
|
- Up-rank rows where `account_stage` is `adopting` or `expansion_ready`.
|
|
49
49
|
- Down-rank or skip rows where `account_stage` is `dormant` or `churned` —
|
|
@@ -57,20 +57,21 @@ Vruum dashboard by whoever owns the account lifecycle.
|
|
|
57
57
|
## Scoring inputs (consumed by skill Step 4)
|
|
58
58
|
|
|
59
59
|
Per-account features the skill computes from `get_person_360` +
|
|
60
|
-
`
|
|
60
|
+
`fetch` type=company_research:
|
|
61
61
|
|
|
62
|
-
- `accounts.renewal_at` (from `
|
|
62
|
+
- `accounts.renewal_at` (from `fetch` type=account_state) — proximity weight
|
|
63
63
|
(60-180d sweet spot)
|
|
64
64
|
- `accounts.health_score` — gate (>70 only)
|
|
65
65
|
- `accounts.account_stage` — boost (`adopting`, `expansion_ready`) or skip
|
|
66
66
|
(`dormant`, `churned`)
|
|
67
67
|
- Most recent `practice='adoption'` activity in last 60d — engagement signal
|
|
68
68
|
- Recent touch sent + reply received → champion present
|
|
69
|
-
- New hire signals on company LinkedIn (via `
|
|
70
|
-
stakeholder = hook
|
|
71
|
-
- New dept created (via `
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
- New hire signals on company LinkedIn (via `research` action=linkedin_fetch)
|
|
70
|
+
— fresh stakeholder = hook
|
|
71
|
+
- New dept created (via `research` action=linkedin_fetch) — multi-team
|
|
72
|
+
expansion play
|
|
73
|
+
- Recent product/news events (via `research` action=enrich_company or saved
|
|
74
|
+
research) — timing hook
|
|
74
75
|
|
|
75
76
|
## Hook generation
|
|
76
77
|
|
|
@@ -16,31 +16,31 @@ The post-sale side (Onboarding → Adoption → Expansion → Win-back) is where
|
|
|
16
16
|
|
|
17
17
|
## Where heavy logic lives
|
|
18
18
|
|
|
19
|
-
[`COHORT-QUERIES.md`](./COHORT-QUERIES.md) — the cohort recipes (Vruum-MCP harness flow + scoring rationale). Read it before running the workflow if you need to understand or tweak the cohort definition. All recipes use `
|
|
19
|
+
[`COHORT-QUERIES.md`](./COHORT-QUERIES.md) — the cohort recipes (Vruum-MCP harness flow + scoring rationale). Read it before running the workflow if you need to understand or tweak the cohort definition. All recipes use `search` type=deals + post-filter; tenant scope is automatic from your session.
|
|
20
20
|
|
|
21
21
|
## Workflow
|
|
22
22
|
|
|
23
23
|
Step 1 — Read the impact scoreboard.
|
|
24
24
|
```
|
|
25
|
-
|
|
25
|
+
fetch(type="scoreboard", subtype="impact", filters={"window_days": 90})
|
|
26
26
|
```
|
|
27
27
|
- If `state == "no_events_yet"`: no impact data yet — fall through to the cohort SQL below to use the day-1 heuristic. Mention that the scoreboard is empty so you understand why this is running off deal data not impact events.
|
|
28
28
|
- Otherwise: surface the practice rollups so you see current state.
|
|
29
29
|
|
|
30
30
|
Step 2 — Build the cohort.
|
|
31
31
|
Follow the **primary cohort** recipe in `COHORT-QUERIES.md`. The recipe runs
|
|
32
|
-
`
|
|
33
|
-
automatic from your authenticated session. Cohort criteria:
|
|
32
|
+
`search(type="deals", filters={"outcome": "won"})` and post-filters in-memory;
|
|
33
|
+
tenant scope is automatic from your authenticated session. Cohort criteria:
|
|
34
34
|
- A deal closed `won` more than 60 days ago AND
|
|
35
35
|
- No open deal exists on the same person now (post-filter against
|
|
36
|
-
`
|
|
36
|
+
`search` type=deals filters={outcome: None}) AND
|
|
37
37
|
- The person is still surfaceable via `get_person_360` (Step 3 enrichment).
|
|
38
38
|
|
|
39
39
|
Limit 50. Order by `actual_close_date DESC` (most recently won first — freshest relationship).
|
|
40
40
|
|
|
41
41
|
Step 3 — Per-account enrichment. For each surfaced (person, company):
|
|
42
42
|
- `get_person_360(person_id=<id>)` — pulls current title, recent activity, last touch, deal history
|
|
43
|
-
- `
|
|
43
|
+
- `fetch(type="company_research", id=<company domain>)` — pulls firmographics, recent news/triggers
|
|
44
44
|
|
|
45
45
|
Step 4 — Score and rank. Within the cohort, rank by:
|
|
46
46
|
- (a) **Renewal pressure** — `accounts.renewal_at` within 60-180 days → up-rank
|
|
@@ -60,24 +60,27 @@ Step 5 — Surface a ranked table:
|
|
|
60
60
|
| 1 | Acme | Tyler T | adopt | 85 | 47d | 3 new dept LinkedIns past 30d |
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
For each row, include a one-line "hook" — a specific recent observation from `get_person_360` or `
|
|
63
|
+
For each row, include a one-line "hook" — a specific recent observation from `get_person_360` or `fetch` type=company_research (new hire, new dept, news event, product usage signal) that frames the expansion conversation. **Never push expansion without a hook** — generic "checking in" outreach kills relationships.
|
|
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 `
|
|
67
|
+
- **Option B**: Direct `manage_outreach` action=start with an expansion-flavored segment (pre-create an `expansion_<your-tenant>` segment with the right tone — formal, ROI-focused, no opener-hooks since the customer already knows you).
|
|
68
68
|
|
|
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-
|
|
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
|
```
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
manage_account(
|
|
72
|
+
action="record_impact",
|
|
73
|
+
payload={
|
|
74
|
+
practice: 'expansion',
|
|
75
|
+
event_type: 'expansion_meeting_booked',
|
|
76
|
+
value_delivered_numeric: deal.estimated_value,
|
|
77
|
+
...
|
|
78
|
+
}
|
|
76
79
|
)
|
|
77
80
|
```
|
|
78
|
-
You do NOT manually
|
|
81
|
+
You do NOT manually record the impact event for tagged plans. If a meeting is booked outside Vruum (manual scheduling, calendar tool not connected), record it manually via `manage_account` action=record_impact from the person 360 Activity tab.
|
|
79
82
|
|
|
80
|
-
After 30 days, run `
|
|
83
|
+
After 30 days, run `fetch` type=scoreboard subtype=impact to measure cohort uplift: expansion `event_count` should be > 0 with `impact_sum` matching booked deal values.
|
|
81
84
|
|
|
82
85
|
## When NOT to use this skill
|
|
83
86
|
|
|
@@ -92,4 +95,4 @@ See `docs/ACCOUNT-LIFECYCLE-VOCABULARY.md` for the 8 canonical account-lifecycle
|
|
|
92
95
|
|
|
93
96
|
## Backend authoritative gate
|
|
94
97
|
|
|
95
|
-
This skill is harness-mode: the harness picks the cohort, scores, and ranks. The backend's authoritative gate is `
|
|
98
|
+
This skill is harness-mode: the harness picks the cohort, scores, and ranks. The backend's authoritative gate is the impact-event write itself (`manage_account` action=record_impact) — the dedupe key `(user_company_id, company_id, activity_type, source_type, source_id)` prevents double-write. The harness is uplift, not the SLA floor.
|
|
@@ -16,7 +16,7 @@ Reviewing outreach messages is context-expensive. Each message with full context
|
|
|
16
16
|
## Subagent architecture
|
|
17
17
|
|
|
18
18
|
This skill uses the custom agent `vruum-outreach-reviewer` (bundled at `agents/vruum-outreach-reviewer.md`). That agent has:
|
|
19
|
-
- Full Vruum MCP access (can call get_outreach_review,
|
|
19
|
+
- Full Vruum MCP access (can call get_outreach_review, manage_messages, search, etc.)
|
|
20
20
|
- Web search for prospect research
|
|
21
21
|
- Complete review instructions baked into its system prompt
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ For small queues (5 or fewer) or when subagents can't access MCP, review directl
|
|
|
28
28
|
|
|
29
29
|
### Step 1: Get the lay of the land
|
|
30
30
|
|
|
31
|
-
Call `
|
|
31
|
+
Call `fetch` with type=stats and subtype=outreach to see the pending queue shape. Present a quick summary:
|
|
32
32
|
|
|
33
33
|
"You have X reply responses, Y pending T1s, Z T2+ follow-ups. [Any critical alerts.] Want me to run full triage or focus on a specific category?"
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ Keep it short. The user knows their queue — they just need the numbers to deci
|
|
|
36
36
|
|
|
37
37
|
### Step 2: Build the dispatch list and categorize
|
|
38
38
|
|
|
39
|
-
Once the user says go (or picks a focus area), pull the lightweight message queue via `
|
|
39
|
+
Once the user says go (or picks a focus area), pull the lightweight message queue via `search` with type=messages, a status=draft filter, and limit=100. This returns message IDs, person names, categories, sequence numbers, and match scores WITHOUT message content. Very cheap on tokens.
|
|
40
40
|
|
|
41
41
|
Categorize into three processing groups:
|
|
42
42
|
|
|
@@ -96,7 +96,7 @@ For each message:
|
|
|
96
96
|
4. Rate personalization depth (surface/basic/deep)
|
|
97
97
|
5. Check strategic fit (CTA matches stage, moves conversation forward)
|
|
98
98
|
|
|
99
|
-
If a message needs fixes, edit it via
|
|
99
|
+
If a message needs fixes, edit it via manage_messages with action=edit. If personalization is weak, use search with type=kb to find better hooks.
|
|
100
100
|
|
|
101
101
|
Return a structured summary per message:
|
|
102
102
|
MESSAGE: {id} | PERSON: {name} | MATCH_SCORE: {n} | CATEGORY: T{n} | RECOMMENDATION: {approve|edited|flag|reject} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"}
|
|
@@ -115,10 +115,10 @@ Message type: T{sequence_number} follow-up
|
|
|
115
115
|
|
|
116
116
|
Steps:
|
|
117
117
|
1. Call get_outreach_review with message_ids="{message_id}" and content_length="full" to get the current message, thread context, segment instructions, and match analysis.
|
|
118
|
-
2. Call
|
|
119
|
-
3. Call
|
|
118
|
+
2. Call fetch with type=person_research plus get_person_360 for this person to get everything we know.
|
|
119
|
+
3. Call fetch with type=company_research to understand the company's product, positioning, and what problems it solves.
|
|
120
120
|
4. Search the web for this prospect and their company to understand what they actually do, what challenges they face, what they post about.
|
|
121
|
-
5. Call
|
|
121
|
+
5. Call search with type=kb for any relevant intel.
|
|
122
122
|
|
|
123
123
|
Review the message against what you learned:
|
|
124
124
|
- Does the message accurately reflect what this prospect's company does?
|
|
@@ -126,7 +126,7 @@ Review the message against what you learned:
|
|
|
126
126
|
- Is the personalization based on real, verified information?
|
|
127
127
|
- Are there AI tells, cross-touch duplication, or structural issues?
|
|
128
128
|
|
|
129
|
-
If the message is good as-is, approve it. If there is clear opportunity to improve (weak personalization when rich signals exist, fabricated references, wrong framing), edit it via
|
|
129
|
+
If the message is good as-is, approve it. If there is clear opportunity to improve (weak personalization when rich signals exist, fabricated references, wrong framing), edit it via manage_messages with action=edit. Do NOT rewrite messages that are already solid just because you can.
|
|
130
130
|
|
|
131
131
|
Return a structured summary:
|
|
132
132
|
MESSAGE: {id} | PERSON: {name} | MATCH_SCORE: {n} | CATEGORY: T{n} | RECOMMENDATION: {approve|edited|flag|reject} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"} | RESEARCH_SUMMARY: {2-3 sentences on what you found} | PROBLEM_IDENTIFIED: {yes/no/speculative} | REWRITE_REASON: {why you edited, or "n/a"}
|
|
@@ -175,7 +175,7 @@ After outreach messages are processed, ask if the user wants to review the engag
|
|
|
175
175
|
|
|
176
176
|
- **Small queue (5 or fewer total):** skip subagent dispatch, pull `get_outreach_review` directly and review inline.
|
|
177
177
|
- **Small queue of follow-ups (5 or fewer T2+) with many T1s:** still use subagents for T1 structural review, review follow-ups inline.
|
|
178
|
-
- **User wants to review a specific person:** pull that person's conversation with `
|
|
178
|
+
- **User wants to review a specific person:** pull that person's conversation with `fetch` (type=conversation) and review directly. No batch workflow.
|
|
179
179
|
- **Subagent can't reach MCP tools:** fall back to inline review.
|
|
180
|
-
- **Homogeneous T1 pattern:** if the first T1 batch all had the identical issue, fix the remaining in bulk with `
|
|
180
|
+
- **Homogeneous T1 pattern:** if the first T1 batch all had the identical issue, fix the remaining in bulk with a single `manage_messages` call passing an id array (same action applied to every id, max 50 per call). Confirm first.
|
|
181
181
|
|
|
@@ -74,11 +74,11 @@ If personalization is surface level or basic, and you have the tools to go deepe
|
|
|
74
74
|
|
|
75
75
|
If a message needs better personalization or you need to verify something, you have access to:
|
|
76
76
|
|
|
77
|
-
- **Knowledge base** (`
|
|
77
|
+
- **Knowledge base** (`search` with type=kb): Search the company's uploaded sales docs — positioning, case studies, battlecards, objection handling, process docs. Browse with no extra filters first to see what's available, then narrow with the `doc_type` or `query` filters. Use the `document_id` or `include_content: true` filters to read full content. This is your FIRST stop for company-specific messaging guidance, proof points, and competitive positioning.
|
|
78
78
|
- **Web search**: Search for recent news about the prospect's company, their recent activity, industry trends relevant to them
|
|
79
|
-
- **LinkedIn data** (`
|
|
79
|
+
- **LinkedIn data** (`research` with action=linkedin_fetch): Pull the prospect's recent posts if not already in the review data
|
|
80
80
|
- **Obsidian vault** (Read/Grep on `/sessions/amazing-lucid-shannon/mnt/Jon's Neural Net/`): Search for notes on this vertical, company, or prospect. The vault contains pricing frameworks, competitive intel, and vertical playbooks.
|
|
81
|
-
- **Company research** (`
|
|
81
|
+
- **Company research** (`fetch` with type=company_research; `research` with action=enrich_company): Get deeper company context if the match analysis feels thin
|
|
82
82
|
|
|
83
83
|
Use these tools when:
|
|
84
84
|
- The draft's personalization is surface-level and you can find something better
|
|
@@ -91,7 +91,7 @@ Do NOT use these tools for every message. Only when the draft needs improvement
|
|
|
91
91
|
|
|
92
92
|
## Step 4: Edit if needed
|
|
93
93
|
|
|
94
|
-
If the message needs changes, rewrite it and apply the edit using `
|
|
94
|
+
If the message needs changes, rewrite it and apply the edit using `manage_messages` with action=edit, the message id, and the new content in the payload.
|
|
95
95
|
|
|
96
96
|
When rewriting:
|
|
97
97
|
- Keep the same strategic intent (don't change a T2 into a T4)
|
|
@@ -156,7 +156,7 @@ RESEARCH_DONE: {list of extra research you did, or "none"}
|
|
|
156
156
|
|
|
157
157
|
**EDITED** when:
|
|
158
158
|
- Message had fixable issues (AI tells, weak personalization, minor repetition, wrong CTA format)
|
|
159
|
-
- You've applied the fix via
|
|
159
|
+
- You've applied the fix via manage_messages (action=edit)
|
|
160
160
|
- The rewritten version passes all checks
|
|
161
161
|
|
|
162
162
|
**FLAG** when:
|
|
@@ -30,14 +30,14 @@ All harness source skills produce candidate lists matching this shape exactly. T
|
|
|
30
30
|
|
|
31
31
|
**Rules:**
|
|
32
32
|
- At minimum, each candidate needs **either** `linkedin_url` **or** (`name`-fields + `company`). Candidates with neither are skipped at Step 3.
|
|
33
|
-
- `full_name` is a convenience for sources that don't pre-split. Engine's Step 7 splits via last-space heuristic (`Jane van der Merwe` → first=`Jane`, last=`van der Merwe`). Multi-token surnames like `Maria Del Carmen Garcia` may split imperfectly — Phase B's `
|
|
33
|
+
- `full_name` is a convenience for sources that don't pre-split. Engine's Step 7 splits via last-space heuristic (`Jane van der Merwe` → first=`Jane`, last=`van der Merwe`). Multi-token surnames like `Maria Del Carmen Garcia` may split imperfectly — Phase B's linkedin_fetch call (`research` action=linkedin_fetch) returns canonical first/last when `linkedin_url` is present and overrides the heuristic.
|
|
34
34
|
- Field additions are additive only. Removing a field is a breaking change for source skills.
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
38
|
## MCP-availability precheck (load-bearing — runs before Step 3)
|
|
39
39
|
|
|
40
|
-
Before any other Step 3 work, call `
|
|
40
|
+
Before any other Step 3 work, call `fetch(type="research_playbook", id=<segment_id>)`. If this fails with "tool not found" / 404 / connection error, abort the run with this exact message:
|
|
41
41
|
|
|
42
42
|
> Vruum MCP not configured as a user-scoped server. Run:
|
|
43
43
|
>
|
|
@@ -47,7 +47,7 @@ Before any other Step 3 work, call `get_research_playbook(segment_id=<id>)`. If
|
|
|
47
47
|
|
|
48
48
|
This catches the common silent-failure mode: deep-research subagents dispatch, all return `STATUS: failed` because they can't reach MCP, and the operator gets a confusing "0 enrolled, no errors" report. One MCP call upfront vs an hour of debugging.
|
|
49
49
|
|
|
50
|
-
The
|
|
50
|
+
The research_playbook fetch also doubles as the ICP load — capture target_titles, target_industries, value_proposition, positioning_angle, ACV floor, signals_to_watch, exclusions for use in subagent dispatch prompts.
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -56,8 +56,8 @@ The `get_research_playbook` call also doubles as the ICP load — capture target
|
|
|
56
56
|
Per segment's candidate list:
|
|
57
57
|
|
|
58
58
|
1. **MCP precheck + ICP load** (above) — abort run on failure.
|
|
59
|
-
2. **Batch dedup against existing pipeline.** Call `
|
|
60
|
-
3. **Batch company-cache check.** Collect unique company domains from surviving candidates. Call `
|
|
59
|
+
2. **Batch dedup against existing pipeline.** Call `search(type="people", query=[{name, company, linkedin_url} for each candidate])`. Returns one match record per candidate (in input order). Drop candidates with non-null `match` — they're already in pipeline.
|
|
60
|
+
3. **Batch company-cache check.** Collect unique company domains from surviving candidates. Call `fetch(type="company_research", id=[the domains])`. Returns `[{domain, cached_research, age_days}]`.
|
|
61
61
|
- Cache hit (`cached_research != null` AND `age_days <= 90`) → company skips Phase A; the cached research carries forward.
|
|
62
62
|
- Cache miss or stale (`age_days > 90`) → company joins the Phase A research queue.
|
|
63
63
|
4. **Operator confirmation gate (CSV / large lists only).** If the original candidate list was >200 (CSV) or >100 (manual list), confirm count to process before continuing.
|
|
@@ -68,7 +68,7 @@ Per segment's candidate list:
|
|
|
68
68
|
|
|
69
69
|
## Step 4 — Phase A: company research
|
|
70
70
|
|
|
71
|
-
**Concurrency cap: 10 parallel.** Phase A subagents don't call `
|
|
71
|
+
**Concurrency cap: 10 parallel.** Phase A subagents don't call `research` with action=linkedin_fetch — they hit `fetch` (type=company_research), `research` (action=enrich_company), `WebFetch`, `WebSearch`. No Unipile rate-limit pressure.
|
|
72
72
|
|
|
73
73
|
Dispatch one `vruum-company-deep-researcher` per unique uncached company. Subagent file at `.claude/agents/vruum-company-deep-researcher.md` defines the workflow + tools.
|
|
74
74
|
|
|
@@ -79,7 +79,7 @@ You are vruum-company-deep-researcher. Research this company against segment "{s
|
|
|
79
79
|
|
|
80
80
|
company_name: {name}
|
|
81
81
|
domain: {domain}
|
|
82
|
-
segment_icp_summary: {one paragraph from
|
|
82
|
+
segment_icp_summary: {one paragraph from the research_playbook fetch}
|
|
83
83
|
acv_floor: {dollars or default $10K}
|
|
84
84
|
|
|
85
85
|
Run your workflow (a–i) and return the structured output block.
|
|
@@ -89,7 +89,7 @@ Each subagent returns: `company_id`, `funding_data`, `growth_metrics`, `current_
|
|
|
89
89
|
|
|
90
90
|
**Wait for the wave to complete before Phase B.** Phase B inputs depend on Phase A's signals (or null if failed).
|
|
91
91
|
|
|
92
|
-
**Subagent timeout cascade (load-bearing):** when STATUS=failed for a company, the orchestrator does NOT skip the prospects from that company. Phase B still runs for them with `null` company signals. The harness pre-filter gate then tags them `harness_gate_status: gate_inconclusive` (a fourth status alongside pass/warming/low_priority/dismiss). `
|
|
92
|
+
**Subagent timeout cascade (load-bearing):** when STATUS=failed for a company, the orchestrator does NOT skip the prospects from that company. Phase B still runs for them with `null` company signals. The harness pre-filter gate then tags them `harness_gate_status: gate_inconclusive` (a fourth status alongside pass/warming/low_priority/dismiss). `manage_person` action=save_discovered is still called — the backend's `MatchAnalysisAgent` may have cached company research from earlier runs and gates them appropriately. Surface gate-inconclusive prospects in the final report so the operator can re-run the failed companies later.
|
|
93
93
|
|
|
94
94
|
**Inter-wave progress line.** After each wave (5–10 subagents):
|
|
95
95
|
```
|
|
@@ -101,7 +101,7 @@ Helps operators distinguish "still working" from "stuck."
|
|
|
101
101
|
|
|
102
102
|
## Step 5 — Phase B: prospect research
|
|
103
103
|
|
|
104
|
-
**Concurrency cap: 5 parallel** (lowered from Phase A's 10 because Phase B subagents call `
|
|
104
|
+
**Concurrency cap: 5 parallel** (lowered from Phase A's 10 because Phase B subagents call `research` action=linkedin_fetch and the Unipile rate limiter throws over cap — see `backend/app/domains/channels/services/unipile/rate_limiter.py:36`. Lower concurrency keeps us under the per-account window.)
|
|
105
105
|
|
|
106
106
|
Dispatch one `vruum-prospect-deep-researcher` per surviving candidate. Subagent file at `.claude/agents/vruum-prospect-deep-researcher.md`.
|
|
107
107
|
|
|
@@ -122,10 +122,10 @@ phase_a_signals:
|
|
|
122
122
|
outbound_motion_score: {0|1|2 or null}
|
|
123
123
|
triggers: [list or null]
|
|
124
124
|
|
|
125
|
-
segment_icp_summary: {one paragraph from
|
|
125
|
+
segment_icp_summary: {one paragraph from the research_playbook fetch}
|
|
126
126
|
acv_floor: {dollars}
|
|
127
127
|
|
|
128
|
-
Run your workflow (a–k) and return the structured output block. Note: do NOT call
|
|
128
|
+
Run your workflow (a–k) and return the structured output block. Note: do NOT call manage_person action=save_discovered or manage_outreach action=start — those are orchestrator-only and not in your tools list.
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
Each subagent returns: `topics_of_interest`, `recent_posts`, `opening_hooks[]` (2–3, with source URLs), `decision_maker_level` (junior/mid/senior), `email_status` (found/pending), `role_start_date`, per-prospect `triggers[]`, `STATUS`. Note: `person_id` is NOT returned here — identity resolution happens in Step 7.
|
|
@@ -139,17 +139,17 @@ Each subagent returns: `topics_of_interest`, `recent_posts`, `opening_hooks[]` (
|
|
|
139
139
|
|
|
140
140
|
## Step 6 — Harness pre-filter gate (orchestrator-side, pre-save)
|
|
141
141
|
|
|
142
|
-
This is a **coarse pre-filter** — its job is to avoid wasted backend `
|
|
142
|
+
This is a **coarse pre-filter** — its job is to avoid wasted backend save calls (`manage_person` action=save_discovered) on obvious dismisses. The **authoritative** gate is server-side `MatchAnalysisAgent.match_score >= 70` and runs inside that save call. The harness gate cannot override the backend gate; it can only dismiss before reaching it.
|
|
143
143
|
|
|
144
144
|
Per surviving prospect, evaluate four criteria using the segment's playbook ICP and the Phase A + Phase B signals:
|
|
145
145
|
|
|
146
146
|
### 1. ACV class meets segment threshold?
|
|
147
147
|
- `acv_class >= acv_floor_class` → pass this criterion (smb=$5K, mid=$5–50K, ent=$50K+; segment's `acv_floor` from playbook maps to a class)
|
|
148
|
-
- If no → dismiss `acv_too_low`. Don't call `
|
|
148
|
+
- If no → dismiss `acv_too_low`. Don't call `manage_person` action=save_discovered.
|
|
149
149
|
|
|
150
150
|
### 2. Outbound motion or hiring signal?
|
|
151
151
|
- `outbound_motion_score > 0` OR explicit hiring trigger present → pass
|
|
152
|
-
- If no → flag `warming_candidate` (still call `
|
|
152
|
+
- If no → flag `warming_candidate` (still call `manage_person` action=save_discovered — operator may want to warm-track them; backend match analysis tells us if the segment fit is real)
|
|
153
153
|
|
|
154
154
|
### 3. Decision-maker level senior?
|
|
155
155
|
- `decision_maker_level == senior` → pass
|
|
@@ -158,7 +158,7 @@ Per surviving prospect, evaluate four criteria using the segment's playbook ICP
|
|
|
158
158
|
|
|
159
159
|
### 4. Trigger event in last 90d?
|
|
160
160
|
- 1+ trigger from Phase A (`funding`, `exec_hire`, `launch`, `m_and_a`, `partnership`) OR Phase B (`new_role`, `topical_post`, `press_mention`, `promotion`) → pass
|
|
161
|
-
- If no → flag `low_priority` (still call `
|
|
161
|
+
- If no → flag `low_priority` (still call `manage_person` action=save_discovered)
|
|
162
162
|
|
|
163
163
|
### Tag each prospect:
|
|
164
164
|
- `harness_gate_status: pass` — all four criteria passed
|
|
@@ -178,43 +178,46 @@ For non-dismiss outcomes, also set `dismiss_reason` to null and `flag` to the re
|
|
|
178
178
|
Per surviving prospect:
|
|
179
179
|
|
|
180
180
|
### a. Save company research (once per company)
|
|
181
|
-
If the prospect's company isn't already cached and Phase A produced fresh research, call `
|
|
181
|
+
If the prospect's company isn't already cached and Phase A produced fresh research, call `research(action="save_company", payload={company_name, domain, funding_data, growth_metrics, current_priorities})`. Skip if `CACHE_HIT: true` for that company.
|
|
182
182
|
|
|
183
183
|
### b. Identity resolution + person research (load-bearing — corrects Codex Finding #6)
|
|
184
184
|
|
|
185
|
-
`
|
|
185
|
+
`research` action=save_person requires `first_name` + `last_name` in the payload, NOT `name`. `manage_person` action=save_discovered requires `person_id` from a prior save step. So Step 7 is a 2-step backend dance:
|
|
186
186
|
|
|
187
187
|
1. **Split full_name** if `first_name`/`last_name` aren't already set:
|
|
188
188
|
- Last-space heuristic: split on the last space. `Jane Smith` → first=`Jane`, last=`Smith`. `Jane van der Merwe` → first=`Jane`, last=`van der Merwe`.
|
|
189
|
-
- **Override with Phase B canonical names** if
|
|
189
|
+
- **Override with Phase B canonical names** if the linkedin_fetch research call returned them. LinkedIn's `first_name`/`last_name` fields are authoritative; the heuristic is a fallback for candidates without `linkedin_url`.
|
|
190
190
|
|
|
191
|
-
2. **Call `
|
|
191
|
+
2. **Call `research` with action=save_person.** The backend now requires you to identify the company unambiguously — pick ONE of these two paths:
|
|
192
192
|
|
|
193
|
-
**Path A (preferred): pass `company_id`.** Run
|
|
193
|
+
**Path A (preferred): pass `company_id`.** Run the save_company call (`research` action=save_company) first, capture the returned `company_id`, then pass it in the payload here.
|
|
194
194
|
|
|
195
|
-
**Path B (when Path A isn't done yet): pass `company_name` + at least one anchor.** Required anchors are any of `company_domain`, `company_website`, or `company_linkedin_url`. The data is in the LinkedIn payload you already fetched. The prospect's CURRENT employer is the entry in `work_experience[]` with `end_date: null` — that entry has `company_linkedin_url` (e.g. `https://linkedin.com/company/microsoft`). If you ran `
|
|
195
|
+
**Path B (when Path A isn't done yet): pass `company_name` + at least one anchor.** Required anchors are any of `company_domain`, `company_website`, or `company_linkedin_url`. The data is in the LinkedIn payload you already fetched. The prospect's CURRENT employer is the entry in `work_experience[]` with `end_date: null` — that entry has `company_linkedin_url` (e.g. `https://linkedin.com/company/microsoft`). If you ran linkedin_fetch with `include_company: true` in the payload, the separate company response carries `website` and `industry`. Domain can be derived from website (e.g. `microsoft.com` from `https://microsoft.com`) or from the prospect's verified work email.
|
|
196
196
|
|
|
197
197
|
**Anchor-less name-only saves are rejected with HTTP 422.** This was hardened to stop orphan stub creation in the companies table — name-only saves were silently producing duplicate rows for common names like Microsoft.
|
|
198
198
|
|
|
199
199
|
Example call:
|
|
200
200
|
```
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
research(
|
|
202
|
+
action="save_person",
|
|
203
|
+
payload={
|
|
204
|
+
first_name=..., last_name=...,
|
|
205
|
+
email=..., linkedin_url=...,
|
|
206
|
+
# ONE of:
|
|
207
|
+
company_id=<from the save_company call>
|
|
208
|
+
# OR:
|
|
209
|
+
company_name=..., company_linkedin_url=..., # at least one anchor
|
|
210
|
+
# ...rest of research fields
|
|
211
|
+
}
|
|
209
212
|
)
|
|
210
213
|
```
|
|
211
214
|
|
|
212
|
-
- If the prospect already had `person_id` set on the candidate (e.g. operator pasted a Vruum person UUID), pass it explicitly: `
|
|
215
|
+
- If the prospect already had `person_id` set on the candidate (e.g. operator pasted a Vruum person UUID), pass it explicitly in the payload: `research(action="save_person", payload={person_id: ..., ...})` — backend updates rather than creating a new record.
|
|
213
216
|
- The response includes the `person_id`. Capture it for step c.
|
|
214
217
|
|
|
215
218
|
### c. Save discovered person (the backend authoritative gate runs here)
|
|
216
219
|
|
|
217
|
-
Call `
|
|
220
|
+
Call `manage_person(action="save_discovered", payload={person_id: <from b>, segment_id: ...})`. This:
|
|
218
221
|
- Runs server-side `analyze_person_match` + signal eval
|
|
219
222
|
- Returns `match_score` (0–100) and `quality_gate_pass` (bool, true iff `match_score >= 70`)
|
|
220
223
|
- Writes the `company_people` row that puts the prospect into the segment
|
|
@@ -225,7 +228,7 @@ Call `save_discovered_person(person_id=<from b>, segment_id=...)`. This:
|
|
|
225
228
|
|
|
226
229
|
### d. Bulk enrollment (only after all prospects saved)
|
|
227
230
|
|
|
228
|
-
Collect all `person_id`s where `harness_gate_status == pass` AND backend `quality_gate_pass == true` AND `mode == save-and-enroll`. Then call `
|
|
231
|
+
Collect all `person_id`s where `harness_gate_status == pass` AND backend `quality_gate_pass == true` AND `mode == save-and-enroll`. Then call `manage_outreach(action="start", id=[those person_ids], payload={segment_id: ...})` ONCE at the end of Step 7.
|
|
229
232
|
|
|
230
233
|
- Per-prospect outcomes are returned (enrolled | skipped | failed). Surface per-prospect failures in the report.
|
|
231
234
|
- If `harness_gate_status` is `warming` or `low_priority`, exclude from the bulk enroll list. Operator decides on review.
|
|
@@ -285,7 +288,7 @@ For multi-segment runs, group the report by segment and include a totals summary
|
|
|
285
288
|
- **Manual-list cap** — if >100 lines pasted, orchestrator asks "{N} prospects pasted — process all, or first M? (a/N)".
|
|
286
289
|
- **CSV >200 rows** — same prompt at Step 5 of csv-pipeline-fill.
|
|
287
290
|
- **LinkedIn rate-limit (Unipile 429)** — Phase B subagent dismisses with `linkedin_data_unavailable`; orchestrator surfaces in report; operator reruns later.
|
|
288
|
-
- **Unicode multi-token surnames** — `Maria Del Carmen Garcia`: heuristic splits to first=`Maria Del Carmen`, last=`Garcia` (last space wins). When `linkedin_url` is present,
|
|
291
|
+
- **Unicode multi-token surnames** — `Maria Del Carmen Garcia`: heuristic splits to first=`Maria Del Carmen`, last=`Garcia` (last space wins). When `linkedin_url` is present, the linkedin_fetch research call overrides with canonical names. Imperfect for candidates without LinkedIn URL — operator can edit via `manage_person` action=update_contact post-import.
|
|
289
292
|
|
|
290
293
|
---
|
|
291
294
|
|
|
@@ -29,7 +29,7 @@ When you reach Step 3, **stop and read** `RESEARCH-ENGINE.md`. That doc is the c
|
|
|
29
29
|
|
|
30
30
|
This skill uses two subagents (defined in `.claude/agents/`):
|
|
31
31
|
- `vruum-company-deep-researcher` — Phase A, one per unique company in the batch (max 10 in parallel)
|
|
32
|
-
- `vruum-prospect-deep-researcher` — Phase B, one per person (max 5 in parallel — Phase B is rate-limited because it calls `
|
|
32
|
+
- `vruum-prospect-deep-researcher` — Phase B, one per person (max 5 in parallel — Phase B is rate-limited because it calls `research` action=linkedin_fetch)
|
|
33
33
|
|
|
34
34
|
### MCP access requirements (load-bearing)
|
|
35
35
|
|
|
@@ -39,7 +39,7 @@ Subagents need access to the Vruum MCP server. Register it once in your AI assis
|
|
|
39
39
|
- **Codex CLI**: `~/.codex/config.toml` → `[mcp_servers.vruum]` with `url = "https://api.vruum.ai/mcp"`
|
|
40
40
|
- **Other**: connect to `https://api.vruum.ai/mcp` (HTTP, OAuth via standard MCP flow)
|
|
41
41
|
|
|
42
|
-
The orchestrator's MCP precheck at the top of Step 3 (`
|
|
42
|
+
The orchestrator's MCP precheck at the top of Step 3 (the `fetch` type=research_playbook call) catches misconfiguration upfront. Don't skip it.
|
|
43
43
|
|
|
44
44
|
### Dispatch methods (in order of preference)
|
|
45
45
|
|
|
@@ -56,7 +56,7 @@ The orchestrator's MCP precheck at the top of Step 3 (`get_research_playbook` ca
|
|
|
56
56
|
|
|
57
57
|
## Workflow — Step 1: Show pipeline status & pick segments
|
|
58
58
|
|
|
59
|
-
Call `
|
|
59
|
+
Call `import_prospects(action="sales_nav_searches", payload={action: "list"})` + `fetch(type="stats", subtype="outreach")` for queue depth + `search(type="campaigns")` for non-Sales-Nav segments. Present a numbered table with **per-segment ETA**:
|
|
60
60
|
|
|
61
61
|
```
|
|
62
62
|
Pipeline status:
|
|
@@ -100,8 +100,8 @@ The conditional rendering happens at package-build time, not at skill-runtime
|
|
|
100
100
|
|
|
101
101
|
Per source pick, dispatch:
|
|
102
102
|
|
|
103
|
-
- `sales-nav-platform` → invoke `/sales-nav-platform-fill` (calls `
|
|
104
|
-
- `csv-platform` → invoke `/csv-platform-fill` (calls `
|
|
103
|
+
- `sales-nav-platform` → invoke `/sales-nav-platform-fill` (calls `import_prospects` action=sales_nav_import; backend handles everything; **skip Steps 3-8 of this skill entirely** — backend agents own the rest).
|
|
104
|
+
- `csv-platform` → invoke `/csv-platform-fill` (calls `import_prospects` action=csv_start; backend handles everything; same — skip Steps 3-8).
|
|
105
105
|
- `sales-nav-deep` → invoke `/sales-nav-deep-fill` to produce a candidate list, then continue to Step 3 with it.
|
|
106
106
|
- `yc` → invoke `/yc-pipeline-fill` to produce a candidate list, then continue to Step 3 with it.
|
|
107
107
|
- `csv` → invoke `/csv-pipeline-fill` to produce a candidate list, then continue to Step 3 with it.
|
|
@@ -116,7 +116,7 @@ Discovery mode covers two paths off the same prompt:
|
|
|
116
116
|
**Path A — operator pastes candidates** (you already know who you want)
|
|
117
117
|
Tolerant line parser, candidates produced directly:
|
|
118
118
|
|
|
119
|
-
- **Line is a LinkedIn URL** (matches `^https?://(www\.)?linkedin\.com/in/[^/?]+/?(\?.*)?$`) → set `linkedin_url`, leave `name` and `company` null. Phase B will fill them via
|
|
119
|
+
- **Line is a LinkedIn URL** (matches `^https?://(www\.)?linkedin\.com/in/[^/?]+/?(\?.*)?$`) → set `linkedin_url`, leave `name` and `company` null. Phase B will fill them via the linkedin_fetch research call.
|
|
120
120
|
- **Line has comma(s)** → split as `name, company[, linkedin_url][, email]`. If 4 fields, last is email. If 3 fields, last is linkedin_url IF it matches the LinkedIn URL pattern, else interpret as email if it has `@`, else treat as a 2-field line + extra junk.
|
|
121
121
|
- **Line is just text** → treat as `full_name`, prompt operator: "what company for {full_name}?". If the operator gets prompted for >3 lines, ask once "set company={X} for all unspecified?" to batch.
|
|
122
122
|
|
|
@@ -127,13 +127,13 @@ Drop blank lines and lines starting with `#` (treat as comments).
|
|
|
127
127
|
**Path B — operator describes an ICP** (you want the harness to discover candidates)
|
|
128
128
|
Operator gives a brief like "Series A-C SaaS founders, US, 50-500 ppl" or "directors of operations at MSPs in DFW, recently posted about hiring". Harness sources candidates from scratch:
|
|
129
129
|
|
|
130
|
-
1. **Anchor on segment ICP** — read the segment's existing ICP/company profile (via `
|
|
130
|
+
1. **Anchor on segment ICP** — read the segment's existing ICP/company profile (via `fetch` type=campaign and `fetch` type=settings subtype=profile) and merge with the operator's brief. Show a one-line synthesis ("OK so: Series A-C SaaS, US, 50-500 ppl, founder/CEO/CTO titles") and confirm before sourcing.
|
|
131
131
|
2. **Source companies first** — use harness tools to find candidate companies matching the brief:
|
|
132
132
|
- `WebSearch` for funding announcements, news, lists ("Series A SaaS 2026", "TechCrunch Series B SaaS announcements")
|
|
133
133
|
- `WebFetch` on Crunchbase / PitchBook / company directories
|
|
134
|
-
- `
|
|
135
|
-
3. **Source people from each company** — for each candidate company, use `
|
|
136
|
-
4. **Dedup against existing pipeline** — for each discovered person, check `
|
|
134
|
+
- `mcp__vruum__import_prospects` with action=sales_nav_search (payload={keywords, title, limit}) for company-fitting roles when the brief is people-shaped (e.g. "VPs of Eng at Series A SaaS")
|
|
135
|
+
3. **Source people from each company** — for each candidate company, use `mcp__vruum__search` with type=companies and filters={domain, seniority} (Unipile-backed; respects LinkedIn rate limits) to find titles matching the segment ICP. Cap at ~5 people per company to spread the discovery surface.
|
|
136
|
+
4. **Dedup against existing pipeline** — for each discovered person, check `mcp__vruum__search` with type=people and a name/company keyword query so you don't research someone the segment already has.
|
|
137
137
|
5. **Show the discovered list to the operator** before handoff. Format: `Name (title) — Company [linkedin]`. Cap the surface at 2x daily_target so we don't over-source. Get a "go" / "drop X" before continuing.
|
|
138
138
|
|
|
139
139
|
Discovery-path candidates produced in either path use the canonical shape in `RESEARCH-ENGINE.md` and feed into Step 3 the same way.
|