@vruum/skills 0.6.4 → 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 +20 -0
- package/.codex-plugin/plugin.json +19 -0
- package/.mcp.json +8 -0
- 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 +6 -3
- 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
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
---
|
|
9
9
|
## MCP smoke test (run early)
|
|
10
10
|
|
|
11
|
-
Before triaging, confirm Vruum MCP is reachable. Call `
|
|
11
|
+
Before triaging, confirm Vruum MCP is reachable. Call `fetch` with type=marketing, subtype=overview as a lightweight liveness check. On failure, surface this error and stop:
|
|
12
12
|
|
|
13
13
|
> Vruum MCP is not connected. Set up the MCP server (see Vruum docs) and re-invoke. Subagent dispatch needs user-scope MCP — cloud-mode MCP is not inherited.
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ You review the user's pending LinkedIn engagement drafts (warming comments, nurt
|
|
|
20
20
|
|
|
21
21
|
## Why this is a skill and not just "call the tool"
|
|
22
22
|
|
|
23
|
-
The backend produces a research dossier + shippable-floor comment for every engagement (the `polished_floor` field — quality SLA for all four front doors per `project_four_front_doors_architecture`). The skill's job is to UPLIFT that floor into a great comment using the operator's Claude subscription, then write the polished result back via `
|
|
23
|
+
The backend produces a research dossier + shippable-floor comment for every engagement (the `polished_floor` field — quality SLA for all four front doors per `project_four_front_doors_architecture`). The skill's job is to UPLIFT that floor into a great comment using the operator's Claude subscription, then write the polished result back via `manage_engagements` with `polish_provenance.source="skill"` so the two-stage edit diff is captured.
|
|
24
24
|
|
|
25
25
|
Reviewing inline burns tokens fast. Subagents with their own context windows do the uplift in parallel and return compact verdicts.
|
|
26
26
|
|
|
@@ -36,7 +36,7 @@ Falls back to general-purpose subagent with MCP tool names in the prompt if the
|
|
|
36
36
|
|
|
37
37
|
### Step 1: Summarize the queue
|
|
38
38
|
|
|
39
|
-
Call `
|
|
39
|
+
Call `fetch` with type=marketing, subtype=overview to see what's pending. Present a one-liner:
|
|
40
40
|
|
|
41
41
|
"X warming drafts, Y nurture drafts, Z marketing drafts, N content posts pending."
|
|
42
42
|
|
|
@@ -55,7 +55,7 @@ If the user just says "go", default to A.
|
|
|
55
55
|
|
|
56
56
|
### Step 3: Pull sender identity (REQUIRED before dispatch)
|
|
57
57
|
|
|
58
|
-
Call `
|
|
58
|
+
Call `fetch` with type=settings, subtype=profile to get the sender's identity, value prop, industry expertise, and background. Subagents need this to validate that drafts sound like the right person.
|
|
59
59
|
|
|
60
60
|
Include a SENDER PROFILE block in every subagent prompt:
|
|
61
61
|
```
|
|
@@ -71,7 +71,7 @@ Background: {founder_background}
|
|
|
71
71
|
|
|
72
72
|
For each queue type the user selected, call the appropriate list endpoint, get IDs + lightweight context (no full content yet), then dispatch subagents.
|
|
73
73
|
|
|
74
|
-
**Warming / Nurture / Marketing engagements** — call `
|
|
74
|
+
**Warming / Nurture / Marketing engagements** — call `search` with type=engagements, filtered by source (`warming` / `nurture` / `marketing`). Batch 3-5 per subagent.
|
|
75
75
|
|
|
76
76
|
**Content posts** — call `get_content_review` for drafts awaiting approval. Batch 2-3 per subagent (posts are longer and need more careful voice check).
|
|
77
77
|
|
|
@@ -130,15 +130,17 @@ For each engagement:
|
|
|
130
130
|
- KEEP: polished_floor is already strong. Don't edit.
|
|
131
131
|
- FLAG: structurally broken (off-topic, wrong stage, prospect bad fit).
|
|
132
132
|
Recommend skip + plan-stop cascade.
|
|
133
|
-
3. If UPLIFT, call
|
|
133
|
+
3. If UPLIFT, call manage_engagements with:
|
|
134
134
|
action="edit"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
id="<id>"
|
|
136
|
+
payload={
|
|
137
|
+
"content": "<your uplifted comment>",
|
|
138
|
+
"polish_provenance": {
|
|
139
|
+
"source": "skill",
|
|
140
|
+
"model": "<your model — claude-opus-4-7, claude-sonnet-4-6, etc.>",
|
|
141
|
+
"at": "<ISO8601>",
|
|
142
|
+
"rewrite_notes": "<one line — what you changed and why>"
|
|
143
|
+
}
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
IMPORTANT: Do NOT approve or skip engagements. Return recommendations only.
|
|
@@ -36,7 +36,7 @@ If the post makes specific claims (statistics, market sizes), flag as "unverifie
|
|
|
36
36
|
|
|
37
37
|
## Step 3: Edit if needed
|
|
38
38
|
|
|
39
|
-
For structural issues (no line breaks, weak hook), use `
|
|
39
|
+
For structural issues (no line breaks, weak hook), use `manage_content` with action="edit". For brand voice and factual issues, FLAG rather than edit.
|
|
40
40
|
|
|
41
41
|
## Step 4: Return structured summary
|
|
42
42
|
|
|
@@ -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
|
|