@vruum/skills 0.6.40 → 0.6.42

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.40",
3
+ "version": "0.6.42",
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.40",
3
+ "version": "0.6.42",
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,7 +86,7 @@ the public skill instead of leaving a broken link.
86
86
  - `/expansion-fill` — Source expansion-ready customers for outreach. Finds closed-won customers >60 days old with no open follow-on deal and surfaces them for an expansion play. Use when: expand customer, find upsell opportunities, NRR play, expansion opportunities, customers ripe for expansion, who can we expand to.
87
87
  - `/ingest-meetings` — Pull meeting transcripts from your connected Google Drive into Vruum. Attaches each transcript to the right person and deal as a meeting on their timeline, and turns its action items into tasks that surface in your daily briefing. You review every attach before anything is written; safe to re-run. Use when: ingest meetings, import meeting notes, pull transcripts, log my meetings, action items from meetings, Gemini notes, Read.ai transcripts, turn meetings into tasks.
88
88
  - `/outreach-triage` — Review and approve your pending outreach drafts across LinkedIn and email. Use when: triage, review queue, morning review, check messages, approve outreach, what needs review.
89
- - `/pipeline-fill` — Source-agnostic pipeline orchestrator. Picks a source per campaign (Sales Nav / YC / CSV / discovery), runs harness deep research, applies a pre-filter gate, then saves into the campaign via the backend authoritative match_score>=70 gate. Use when: fill pipeline, import prospects, daily imports, need more prospects, discover prospects from scratch, deep research before import.
89
+ - `/pipeline-fill` — Source-agnostic pipeline orchestrator. Picks a source per campaign (Sales Nav / YC / CSV / account list / discovery), runs harness deep research, applies a pre-filter gate, then saves into the campaign via the backend authoritative match_score>=70 gate. Use when: fill pipeline, import prospects, daily imports, need more prospects, discover prospects from scratch, find buyers at these companies, deep research before import.
90
90
  - `/sales-nav-deep-fill` — Sales Nav harness source for /pipeline-fill. Pre-filters Sales Nav profiles via vruum-pipeline-filter, produces a candidate list, hands off to /pipeline-fill for deep research and import. Use when: sales nav with deep research, sales nav harness mode, in-chat sales nav.
91
91
  - `/vruum-guide` — Guide to running Vruum from your own AI harness. First run: guided onboarding from empty account to first reviewed outreach draft. After: reads live account state, recommends the single next most valuable action, hands off to the right skill. Use when: get started, onboarding, how do I use vruum, what should I do next, where do I start.
92
92
  - `/vruum-skills-upgrade` — Upgrade @vruum/skills to the latest npm version and re-sync ~/.vruum/. Use when: upgrade vruum skills, update vruum, pull latest vruum skills, or when the preamble reports UPGRADE_AVAILABLE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vruum/skills",
3
- "version": "0.6.40",
3
+ "version": "0.6.42",
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": {
@@ -42,5 +42,5 @@
42
42
  "outreach",
43
43
  "gtm"
44
44
  ],
45
- "contentHash": "2d1d42147392e5fc34a8eb81caa215c216163d5d7ac85c15557be06d115776f4"
45
+ "contentHash": "116c4499218d3234185bf864ba4b84c1233ee375f967f9c66c4a34b97d325294"
46
46
  }
@@ -47,6 +47,8 @@ If `name` is mapped, defer the first/last split to the engine doc's Step 7 (cano
47
47
 
48
48
  ### Step 4: Resolve ambiguous mappings
49
49
 
50
+ **Company-only CSV → route to the `account_list` source.** If `company` maps but no person-identity column does (no `name`/first+last, no `linkedin_url`, no `email`), this is an account list, not a contact list. Don't ask the operator to force a mapping — hand the company column to `/pipeline-fill`'s `account-list` source, whose committee-resolution step resolves `buyers_per_account` ICP-matching buyers per company. Say so in one line: "No contact columns — treating this as an account list ({N} companies); /pipeline-fill will resolve the buying committee."
51
+
50
52
  If any required field can't be auto-mapped (`name`/(first+last) AND `company`, OR `linkedin_url`), show the operator the detected headers + sample row and ask which column maps to which field. Don't guess silently — silent guessing is the source of "why did my CSV import 50 prospects with the wrong company" bugs.
51
53
 
52
54
  For multi-column ambiguity (e.g. two columns matching `email`), pick the leftmost match and note it in the operator output.
@@ -113,6 +115,7 @@ Continue automatically? (y/n)
113
115
  - **Duplicate rows** — in-CSV dedup before handoff.
114
116
  - **LinkedIn URLs with tracking params** — query string stripped during canonicalization.
115
117
  - **Mixed name format** (some rows have full_name, others have first/last) — engine handles both via Step 7's identity-resolution flow.
118
+ - **Company-only CSV** (no contact columns) — routed to `/pipeline-fill`'s `account-list` source at Step 4; never imported as contacts.
116
119
  - **Empty CSV** — orchestrator says "CSV has no data rows after dedup; nothing to research" and exits.
117
120
 
118
121
  ## Notes
@@ -30,6 +30,7 @@ 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
+ - **A company-only row (name/domain but no person) is not a valid candidate.** Sources holding companies must run the shared **Committee resolution** step in `SKILL.md` (companies → people, capped at `buyers_per_account`) before handing off to this engine. Do not improvise buyer selection out of company-research prose — that reintroduces marquee-name skew and an undocumented depth of 1 per account.
33
34
  - `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
35
  - Field additions are additive only. Removing a field is a breaking change for source skills.
35
36
  - `source_policy`, candidate examples, and progress events have executable schemas under `contracts/`. Validate handoffs against them before provider calls.
@@ -59,7 +60,12 @@ Stable operator-visible codes:
59
60
  | `linkedin_auth_required` | LinkedIn account is disconnected/expired | reconnect LinkedIn |
60
61
  | `company_identity_conflict` | exact evidence points to different companies | correct the evidence; never auto-merge |
61
62
  | `company_resolution_failed` | company resolver returned no canonical row | retry once, then inspect resolver logs and evidence |
62
- | `company_research_save_failed` | company research persistence failed with unknown commit status | inspect stored rows before any replay |
63
+ | `company_research_commit_unknown` | company research persistence response was lost | replay only the identical payload with the same idempotency key |
64
+ | `company_research_serialization_conflict` | bounded transaction retries were exhausted | replay only the identical payload with the same idempotency key |
65
+ | `idempotency_key_reused` | a save key was reused with different content | keep the original payload or generate a new key |
66
+ | `company_research_invalid_value` | a company anchor or research value is malformed | correct the named value before retrying |
67
+ | `company_research_source_invalid` | a shared field lacks admissible evidence | add/correct sources_by_field before retrying |
68
+ | `company_research_save_failed` | save failed outside the atomic receipt boundary | inspect the correlation ID before deciding whether to retry |
63
69
  | `person_not_visible` | supplied person is outside the caller's tenant | use a tenant-visible person or omit `person_id` |
64
70
  | `person_not_found_for_update` | save_person anchors matched no one in your pipeline (update-only) | search and pass `payload.person_id`, or use save_discovered's `person` block for a new prospect |
65
71
  | `identity_unverifiable` | person has neither a LinkedIn URL nor an email | include `person.linkedin_url` or `person.email` |
@@ -97,9 +103,11 @@ Per campaign's candidate list:
97
103
 
98
104
  1. **MCP precheck + ICP load** (above) — abort run on failure.
99
105
  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.
100
- 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}]`.
101
- - Cache hit (`cached_research != null` AND `age_days <= 90`) → company skips Phase A; the cached research carries forward.
102
- - Cache miss or stale (`age_days > 90`) company joins the Phase A research queue.
106
+ 3. **Batch company fixed-field reuse check.** Collect unique company domains from surviving candidates. Call `fetch(type="company_research", id=[the domains], filters={"requested_fields":["company_summary","company_stage","current_priorities","funding_data","growth_metrics"]})`.
107
+ - Reuse only values whose field entry has `status="reusable"`.
108
+ - `core_reuse.reusable` means the shared summary core is reusable; it never means the campaign brief is complete.
109
+ - Missing, unsourced, stale, invalid, and absent fields remain null inputs. Never carry a raw stored value forward.
110
+ - **Every company still runs Phase A** for campaign-relative outbound motion, ACV class, sales-cycle inference, and triggers. Reusable fixed values are inputs that avoid redundant fetching, not a Phase A skip signal.
103
111
  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.
104
112
 
105
113
  **Latency:** ~2s for batch dedup + ~1s for batch company cache, regardless of list size. (Per-prospect iteration was ~12s for 60 prospects pre-batch primitives.)
@@ -110,7 +118,7 @@ Per campaign's candidate list:
110
118
 
111
119
  **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.
112
120
 
113
- Dispatch one `vruum-company-deep-researcher` per unique uncached company. Subagent file at `.claude/agents/vruum-company-deep-researcher.md` defines the workflow + tools.
121
+ Dispatch one `vruum-company-deep-researcher` per unique company. Subagent file at `.claude/agents/vruum-company-deep-researcher.md` defines the workflow + tools. Include the reusable fixed-field values and their evidence in the prompt; the researcher must still compute campaign-relative outputs.
114
122
 
115
123
  Dispatch prompt template (fill in placeholders):
116
124
 
@@ -125,7 +133,7 @@ acv_floor: {dollars or default $10K}
125
133
  Run your workflow (a–i) and return the structured output block.
126
134
  ```
127
135
 
128
- Each subagent returns: `company_name`, `domain`, `funding_data`, `growth_metrics`, `current_priorities`, `outbound_motion_score` (0/1/2), `acv_class` (smb/mid/ent), `sales_cycle_inference` (short/medium/long), `triggers[]`, `STATUS: ok | failed`, `CACHE_HIT`. Subagents never persist; the orchestrator resolves `company_id` in Step 7 when the requested mode permits writes.
136
+ Each subagent returns: `company_name`, `domain`, `company_summary`, `company_stage`, `funding_data`, `growth_metrics`, `current_priorities`, `sources_by_field` (`{field:[{url,title?,observed_at}]}`), `outbound_motion_score` (0/1/2), `acv_class` (smb/mid/ent), `sales_cycle_inference` (short/medium/long), `triggers[]`, `STATUS: ok | failed`, and the list of reused fixed fields. Subagents never persist; the orchestrator resolves `company_id` in Step 7 when the requested mode permits writes.
129
137
 
130
138
  **Wait for the wave to complete before Phase B.** Phase B inputs depend on Phase A's signals (or null if failed).
131
139
 
@@ -145,7 +153,7 @@ Helps operators distinguish "still working" from "stuck."
145
153
 
146
154
  **Malformed LinkedIn fallback:** if the selected candidate already has a Vruum `person_id` and LinkedIn returns an invalid/malformed-profile result, preserve that `person_id` and retry the enrichment once through the first allowed structured provider in `source_policy` (Clay when selected/connected). Pass the same `person_id` in the PAYLOAD to `research(action="save_person", payload={person_id: ..., ...})` — update-only; never as the facade `id` argument. This is a provider fallback for one identity, not a new-person discovery. Never fall back on LinkedIn 429/rate-limit responses or timeouts; surface those for a later retry. If the fallback's email or LinkedIn URL belongs to another person, the backend returns `person_identity_conflict`; stop and surface it rather than dropping `person_id` and creating a duplicate.
147
155
 
148
- Dispatch one `vruum-prospect-deep-researcher` per surviving candidate. Subagent file at `.claude/agents/vruum-prospect-deep-researcher.md`.
156
+ Dispatch one `vruum-prospect-deep-researcher` per surviving candidate. Subagent file at `.claude/agents/vruum-prospect-deep-researcher.md`. The fan-out is strictly 1:1 with the surviving candidate list — depth per account is decided upstream by `buyers_per_account` in the committee-resolution step, never by Phase B adding or trimming people per company.
149
157
 
150
158
  Dispatch prompt template:
151
159
 
@@ -226,7 +234,7 @@ Apply the requested mode before any persistence:
226
234
  Per surviving prospect:
227
235
 
228
236
  ### a. Save company research (once per company)
229
- If the prospect's company isn't already cached and Phase A produced fresh research, call `research(action="save_company", payload={name: <Phase A COMPANY>, website: <Phase A DOMAIN or canonical URL>, funding_data, growth_metrics, current_priorities: <newline-joined descriptions + source URLs>})`. The API field is `name`, not `company_name`; it accepts `website`, not `domain`; and `current_priorities` is one string, so serialize the Phase A object list instead of passing the list through. Skip if `CACHE_HIT: true` for that company.
237
+ When Phase A produced any newly researched fixed fields, call `research(action="save_company", payload={idempotency_key: <stable run/company save key>, name: <Phase A COMPANY>, website: <Phase A DOMAIN or canonical URL>, company_summary, company_stage, funding_data, growth_metrics, current_priorities: <newline-joined descriptions>, sources_by_field})`. The API field is `name`, not `company_name`; it accepts `website`, not `domain`; and `current_priorities` is one string. Omit reusable fields that were not revalidated so the atomic patch preserves them. Explicit null deliberately clears a field, so do not send null merely because Phase A did not research it. `sources_by_field` keys must equal exactly the supplied non-null research fields. Preserve the identical idempotency key and payload for unknown-commit replay; every bulk item needs its own key.
230
238
 
231
239
  ### b. Identity prep (names + company linkage for the atomic save)
232
240
 
@@ -347,9 +355,13 @@ Pipeline fill complete: {campaign_name} (source: {source}, mode: {harness|platfo
347
355
  Candidates flow:
348
356
  source : {N from source skill output}
349
357
  pre-flight : {after dedup, after company-cache hit}
350
- phase A : {company subagents fired} ({cached_skip} skipped via cache)
358
+ phase A : {company subagents fired} (0 skipped via cache; fixed fields reused: {count})
351
359
  phase B : {prospect subagents fired} ({linkedin_unavailable} dismissed)
352
360
 
361
+ For runs that went through committee resolution (account_list, discovery Path B), also report:
362
+ accounts unresolved : {N} (list the companies + providers tried — from the committee-resolution hand-off)
363
+ and group the per-prospect outcomes by `raw_signals.source_company` so the operator reads results per account.
364
+
353
365
  Harness pre-filter gate:
354
366
  pass : {N}
355
367
  warming : {N}
@@ -2,25 +2,27 @@
2
2
  name: pipeline-fill
3
3
  description: >-
4
4
  Source-agnostic pipeline orchestrator. Picks a source per campaign (Sales Nav
5
- / YC / CSV / discovery), runs harness deep research, applies a pre-filter
6
- gate, then saves into the campaign via the backend authoritative
5
+ / YC / CSV / account list / discovery), runs harness deep research, applies a
6
+ pre-filter gate, then saves into the campaign via the backend authoritative
7
7
  match_score>=70 gate. Use when: fill pipeline, import prospects, daily
8
- imports, need more prospects, discover prospects from scratch, deep research
9
- before import.
8
+ imports, need more prospects, discover prospects from scratch, find buyers at
9
+ these companies, deep research before import.
10
10
  ---
11
11
  # Pipeline Fill
12
12
 
13
- You are a source-agnostic pipeline filler. You pick campaigns to fill, pick a source per campaign (Sales Nav / YC / CSV / discovery), and orchestrate harness deep research that gates against the campaign ICP before saving prospects into the backend pipeline.
13
+ You are a source-agnostic pipeline filler. You pick campaigns to fill, pick a source per campaign (Sales Nav / YC / CSV / account list / discovery), and orchestrate harness deep research that gates against the campaign ICP before saving prospects into the backend pipeline.
14
14
 
15
15
  ## Why this skill exists
16
16
 
17
- Filling your pipeline by source-of-the-day is normal. Sales Nav drying up doesn't mean you're stuck — pick YC, paste a CSV, or run discovery (paste candidates OR describe an ICP and the harness sources them via WebSearch + Vruum MCP + LinkedIn search). This skill orchestrates deep research per prospect in your IDE (your compute), scores against campaign ICP, then lets the backend enforce the fixed `match_score >= 70` gate.
17
+ Filling your pipeline by source-of-the-day is normal. Sales Nav drying up doesn't mean you're stuck — pick YC, paste a CSV, hand over a list of target accounts (the harness resolves the buying committee per account), or run discovery (paste candidates OR describe an ICP and the harness sources them via WebSearch + Vruum MCP + LinkedIn search). This skill orchestrates deep research per prospect in your IDE (your compute), scores against campaign ICP, then lets the backend enforce the fixed `match_score >= 70` gate.
18
18
 
19
19
  ## Where the heavy logic lives
20
20
 
21
21
  Steps 3–8 (pre-flight, Phase A research, Phase B research, harness gate, save chain, audit-log report) are defined in `RESEARCH-ENGINE.md` (in this same skill directory). This skill owns:
22
22
  - Step 1: campaign picker (with ETA)
23
23
  - Step 2: source picker (conditional PLATFORM block + always-visible HARNESS block)
24
+ - The committee-resolution shared step (companies → people; used by every company-producing source)
25
+ - The account-list handler (company names/domains in, candidate list out)
24
26
  - The discovery-mode handler (paste candidates inline OR describe an ICP and source via harness tools)
25
27
  - The multi-campaign grammar
26
28
 
@@ -53,6 +55,7 @@ The orchestrator's MCP precheck at the top of Step 3 (the `fetch` type=research_
53
55
  - `prospect_list` (optional): pre-built candidate list matching the canonical shape in `RESEARCH-ENGINE.md`. If provided, skip the source-picker step and go straight to Step 3 (pre-flight). This is how source skills hand off.
54
56
  - `source_policy` (optional): machine-readable provider policy matching `contracts/source-policy.schema.json`. It owns `selected_source`, `source_mode`, `prohibited_sources`, ordered `allowed_fallbacks`, bounded wave sizes, and transient retry attempts. Treat prohibited providers as unavailable: do not call status/list/search endpoints for them.
55
57
  - `campaign(s)`: target campaign(s); multi-campaign supported.
58
+ - `buyers_per_account` (optional): how many buying-committee members to resolve per company when the source produces companies rather than people. Range 1–5. Per-source defaults: `discovery` → 2 (the surface is unqualified — optimize for reach, spread across more accounts), `account_list` → 3 (the account is already qualified — optimize for depth on the committee). Precedence: explicit operator value > a target stated in the campaign description (e.g. "~2 per account") > the per-source default. Sources that produce people directly (Sales Nav, YC, contact CSVs, discovery Path A) ignore this input.
56
59
  - `mode`: `research-only` | `save` | `save-and-enroll` (default: `save-and-enroll`).
57
60
 
58
61
  ## Workflow — Step 1: Show pipeline status & pick campaigns
@@ -82,13 +85,23 @@ ETA estimates: ~2s for batch Step 3 dedup + ~30s/wave Phase A + ~60s/wave Phase
82
85
  - Mark campaigns already at target with ✓ and don't number them
83
86
  - Show per-campaign ETA so operator can budget time
84
87
 
85
- **Wait for the user's response.** Parse: "all", "1, 3", "skip 2", "just the CFO ones", etc. Only proceed with the selected campaigns.
88
+ **Buyers-per-account line (company-producing sources only).** When the source is already known to produce companies at Step 1 — the operator handed an account list, or named discovery with an ICP brief — append one line under the table so depth is an explicit decision, never a silent default:
89
+
90
+ ```
91
+ Buyers per account: 3 (account-list default; range 1–5 — reply "buyers N" to change)
92
+ ```
93
+
94
+ If the source isn't known yet at Step 1 (the common "fill my pipeline" path), defer this line to source-resolution time — the account-list handler and discovery Path B each confirm it before resolving. Wherever it renders, resolve the shown number via the `buyers_per_account` precedence in Inputs (explicit value > campaign-description target > per-source default), and name which rule produced it (e.g. "2 — campaign description says '~2 per account'").
95
+
96
+ **Wait for the user's response.** Parse: "all", "1, 3", "skip 2", "just the CFO ones", "buyers 3", etc. Only proceed with the selected campaigns.
86
97
 
87
98
  ## Workflow — Step 2: Pick source per campaign (only if `prospect_list` not provided)
88
99
 
89
100
  **Default to `discovery`.** Unless the operator named a source (in their prompt or a prior turn), don't lead with the picker — default to the `discovery` source (the describe-an-ICP path: source against the campaign's own ICP via WebSearch + Vruum MCP + LinkedIn search) and announce it in one line so it stays overridable, e.g.:
90
101
 
91
- > Sourcing {campaign_name} via discovery (ICP-based, long-tail). Reply `sales-nav`, `yc`, `csv`, or `picker` to switch.
102
+ > Sourcing {campaign_name} via discovery (ICP-based, long-tail). Reply `sales-nav`, `yc`, `csv`, `account-list`, or `picker` to switch.
103
+
104
+ **Account-list auto-detect:** if the operator's input is a list of company names or domains with no person identities (no personal names, no `/in/` LinkedIn URLs, no emails — e.g. pasted company lines, or a spreadsheet whose only mappable column is `company`), that is the `account_list` source. Announce it ("Reading this as an account list — {N} companies; I'll resolve the buying committee per account") instead of defaulting to discovery or misreading the rows as contacts.
92
105
 
93
106
  CSV and Sales Nav are fully supported when selected. `source_policy` is a per-run routing contract: an explicit "use CSV" or "use Sales Nav" selects that capability; an explicit "no CSV" or "no Sales Nav" prohibits it only for this run. Never persist a seller's personal source preference as a tenant-wide capability restriction.
94
107
 
@@ -96,7 +109,7 @@ Why discovery is the default: keyword/Sales-Nav sources keep returning the same
96
109
 
97
110
  Per selected campaign, when the operator wants to choose the source explicitly, prompt:
98
111
 
99
- In **public mode** (the package builder strips the PLATFORM block from this skill before publishing), the picker shows only HARNESS modes, renumbered 1–4:
112
+ In **public mode** (the package builder strips the PLATFORM block from this skill before publishing), the picker shows only HARNESS modes, renumbered 1–5:
100
113
 
101
114
  ```
102
115
  Source for {campaign_name}?
@@ -104,10 +117,11 @@ Source for {campaign_name}?
104
117
  1. sales-nav-deep — Sales Nav profiles + harness deep research
105
118
  2. yc — scrape YC directory with filters you provide
106
119
  3. csv — read a CSV file (path next), harness deep research
107
- 4. discovery — paste candidates inline OR describe an ICP and I'll source them via WebSearch + Vruum MCP + LinkedIn search
120
+ 4. account-list — paste company names/domains (or a company-only CSV); I resolve the buying committee per account
121
+ 5. discovery — paste candidates inline OR describe an ICP and I'll source them via WebSearch + Vruum MCP + LinkedIn search
108
122
  ```
109
123
 
110
- The conditional rendering happens at package-build time, not at skill-runtime — when the orchestrator runs in operator mode it sees the 6-option block; when it runs in public mode (stripped package) it sees only the 4-option block. Source-skill dispatch logic below uses option labels (`sales-nav-platform`, `yc`, etc.), not numbers, so the renumbering is cosmetic.
124
+ The conditional rendering happens at package-build time, not at skill-runtime — when the orchestrator runs in operator mode it sees the 7-option block; when it runs in public mode (stripped package) it sees only the 5-option block. Source-skill dispatch logic below uses option labels (`sales-nav-platform`, `yc`, etc.), not numbers, so the renumbering is cosmetic.
111
125
 
112
126
  Per source pick, dispatch:
113
127
 
@@ -115,11 +129,42 @@ Per source pick, dispatch:
115
129
  - `csv-platform` → invoke `/csv-platform-fill` (calls `import_prospects` action=csv_start; backend handles everything; same — skip Steps 3-8).
116
130
  - `sales-nav-deep` → invoke `/sales-nav-deep-fill` to produce a candidate list, then continue to Step 3 with it.
117
131
  - `yc` → invoke `/yc-pipeline-fill` to produce a candidate list, then continue to Step 3 with it.
118
- - `csv` → invoke `/csv-pipeline-fill` to produce a candidate list, then continue to Step 3 with it.
132
+ - `csv` → invoke `/csv-pipeline-fill` to produce a candidate list, then continue to Step 3 with it. (A company-only CSV — no contact columns — routes to `account-list` instead; `/csv-pipeline-fill` documents the same redirect.)
133
+ - `account-list` → use the account-list handler below (parse companies → committee resolution → candidate list), then continue to Step 3 with it.
119
134
  - `discovery` → use the discovery-mode handler below to produce a candidate list (handler branches: paste-shaped input → parse, prose ICP brief → harness sources via WebSearch + Vruum MCP + LinkedIn search), then continue to Step 3 with it.
120
135
 
121
136
  **Multi-campaign behavior:** campaigns run sequentially. Campaign 1's Step 7 (save chain + bulk enroll) completes before campaign 2's Step 3 starts. Predictable rate-limit behavior, simple progress narrative. Trade-off: 3-campaign fills are ~37min wall-clock vs ~22min if Phase A/B were overlapped across campaigns. Cross-campaign overlap is a v2.
122
137
 
138
+ ## Committee resolution (shared step: companies → people)
139
+
140
+ The canonical candidate shape in `RESEARCH-ENGINE.md` is person-shaped — a company-only row is invalid by construction. This step is the single route from companies to people. **Every source that ends up holding companies runs it** (`account_list` always; `discovery` Path B after sourcing companies; a company-only CSV redirected from `/csv-pipeline-fill`). Sources that produce people directly skip it, and YC is a deliberate exemption: it is founder-first by design — the founder *is* the buyer, so `/yc-pipeline-fill` keeps its own founder extraction. Never improvise around this step by hand-picking a buyer out of research prose — see the anti-skew rule below.
141
+
142
+ **Contract:**
143
+ - **Input:** a list of companies, each with `company_name` and/or `domain` (at least one), plus the campaign's ICP target titles/seniority and a resolved `buyers_per_account` (see Inputs).
144
+ - **Output:** the canonical person-shaped candidate list defined in `RESEARCH-ENGINE.md`, ready for Step 3. Set `raw_signals.source_company` so the report can group by account.
145
+
146
+ **Per company:**
147
+ 1. Pull up to `buyers_per_account` people matching the campaign's ICP titles/seniority, using the first available provider in this order (same order as discovery sourcing; apply `source_policy` before any call):
148
+ - **Structured B2B provider** — e.g. Clay `find-and-enrich-contacts-at-company` with the title/seniority filter.
149
+ - **LinkedIn / Sales Nav** people-at-known-company via `import_prospects action=sales_nav_search` — fine here because the company is fixed; the marquee-name skew applies to company discovery, not to enumerating a known committee. Mind LinkedIn quota.
150
+ - **Email finder** — Hunter via `search type=companies {domain, seniority}`.
151
+ - **Web** — `WebSearch` for "{company} {title}" + team/about pages; the universal fallback.
152
+ 2. **Select by title fit, not visibility.** When the provider returns more than `buyers_per_account` matches, rank by ICP title/seniority fit — never by follower count, press coverage, or how often the name appears in research prose. The buyer with no public profile is often the one actually running the function; picking the famous name per account is the same marquee-name skew this skill warns about for Sales Nav.
153
+ 3. **Companies that resolve to zero people** are reported by this step at hand-off ("no ICP-matching contacts found at {company} via {providers tried}") and counted in the engine's Step 8 report (`accounts unresolved`), never silently dropped. They cannot enter the engine (the candidate shape is person-shaped), so the hand-off summary is where the gap surfaces.
154
+
155
+ Provider calls run in the standard bounded waves (company-level actions ≤10, person/LinkedIn actions ≤5) with progress objects per `contracts/run-progress.schema.json`, using `phase: "committee_resolution"`.
156
+
157
+ ## Account-list handler (for `account-list` source)
158
+
159
+ Input is company names or domains — pasted lines, or a company-only CSV/xlsx redirected from `/csv-pipeline-fill`. This is a thin wrapper around the shared committee-resolution step:
160
+
161
+ 1. **Parse companies.** One company per line (or per row). A line that looks like a domain (`acme.com`) sets `domain`; otherwise it's `company_name`. Drop blanks and `#` comments; dedupe case-insensitively. Above 100 accounts, confirm: "{N} accounts — process all, or first M? (a/N)".
162
+ 2. **Anchor on campaign ICP.** Read the campaign's ICP (via `fetch` type=campaign and `fetch` type=settings subtype=profile) to get target titles/seniority. Show a one-line synthesis and the resolved `buyers_per_account` — full precedence per Inputs: an explicit operator value wins, else a campaign-description target like "~2 per account", else this source's default of 3 (the accounts are already qualified, go deeper) — and confirm before resolving.
163
+ 3. **Run committee resolution** (shared step above) across the account list.
164
+ 4. **Show the resolved list** grouped by account — `Company → Name (title) [source]` — and get a "go" / "drop X" before continuing to Step 3.
165
+
166
+ The territory/fit gate on the *companies* themselves still happens in Phase A + the harness gate — the handler doesn't pre-judge accounts, it only turns them into people.
167
+
123
168
  ## Discovery-mode handler (for `discovery` source)
124
169
 
125
170
  Discovery mode covers two paths off the same prompt:
@@ -147,7 +192,7 @@ Operator gives a brief like "Series A-C SaaS founders, US, 50-500 ppl" or "direc
147
192
 
148
193
  Apply `source_policy` before inventorying or calling providers. Validate the entire object against `contracts/source-policy.schema.json` before the first provider call. If `selected_source` is disconnected, stop with code `source_unavailable`; exclusive mode never substitutes, while preferred mode may use only the first connected entry in `allowed_fallbacks`. Announce the resolved policy in one line ("Sourcing via Clay — firmographic pull + committee enrichment; web as allowed backup; Sales Nav prohibited") so the operator can redirect.
149
194
  3. **Source companies first, by firmographics — aim past the obvious names** — use the chosen tool to pull companies matching the merged ICP by stage / headcount / vertical / geo, NOT by marquee-name lookup (the saturated set IS the famous names). With a data provider, run the firmographic query directly; with web only, work funding announcements + directories.
150
- 4. **Resolve the buying committee per company** — for each candidate company, pull ICP-matching titles via the same provider's contact enrichment (e.g. Clay `find-and-enrich-contacts-at-company`) or `search type=companies {domain, seniority}` (Hunter). Cap ~5 people/company to spread the surface.
195
+ 4. **Resolve the buying committee per company** — run the shared **Committee resolution** step above on the sourced companies, with `buyers_per_account` resolved per Inputs (discovery default 2 the surface is unqualified, so spread it across more accounts rather than going deep on any one).
151
196
  5. **Dedup against existing pipeline** — for each discovered person, check `search` type=people with a name/company keyword query so you don't research someone the campaign already has. This is where saturated names drop out, cheaply, before any research spend.
152
197
  6. **Show the discovered list to the operator** before handoff. Format: `Name (title) — Company [source] [linkedin]`. Cap the surface at 2x daily_target so we don't over-source. Get a "go" / "drop X" before continuing.
153
198
 
@@ -32,6 +32,24 @@
32
32
  "person_wave_size": 5,
33
33
  "transient_retry_attempts": 2,
34
34
  "reason": "Use only Clay; stop if it is unavailable."
35
+ },
36
+ "account_list_committee_depth": {
37
+ "version": "1",
38
+ "selected_source": "clay",
39
+ "source_mode": "preferred",
40
+ "prohibited_sources": [
41
+ "sales_nav",
42
+ "linkedin"
43
+ ],
44
+ "allowed_fallbacks": [
45
+ "hunter",
46
+ "web"
47
+ ],
48
+ "company_wave_size": 10,
49
+ "person_wave_size": 5,
50
+ "transient_retry_attempts": 2,
51
+ "buyers_per_account": 3,
52
+ "reason": "Operator-supplied account list; resolve a 3-person buying committee per account without touching LinkedIn quota."
35
53
  }
36
54
  },
37
55
  "progress": {
@@ -52,6 +70,23 @@
52
70
  "elapsed_seconds": 34,
53
71
  "eta_seconds": 0
54
72
  },
73
+ "committee_wave": {
74
+ "version": "1",
75
+ "state": "running",
76
+ "phase": "committee_resolution",
77
+ "provider": "clay",
78
+ "wave": 1,
79
+ "wave_count": 3,
80
+ "completed": 5,
81
+ "total": 15,
82
+ "failed": 0,
83
+ "not_attempted": 10,
84
+ "safe_retry_items": [],
85
+ "ambiguous_items": [],
86
+ "code": null,
87
+ "elapsed_seconds": 21,
88
+ "eta_seconds": 42
89
+ },
55
90
  "person_wave": {
56
91
  "version": "1",
57
92
  "state": "partial",
@@ -34,6 +34,7 @@
34
34
  "phase": {
35
35
  "enum": [
36
36
  "company_research",
37
+ "committee_resolution",
37
38
  "person_research",
38
39
  "assignment"
39
40
  ]
@@ -72,6 +72,12 @@
72
72
  "maximum": 2,
73
73
  "default": 2
74
74
  },
75
+ "buyers_per_account": {
76
+ "type": "integer",
77
+ "minimum": 1,
78
+ "maximum": 5,
79
+ "description": "Buying-committee members to resolve per company in the shared committee-resolution step. No schema-level default on purpose: the default is per-source (discovery 2, account_list 3) and resolved by the orchestrator; a campaign-description target overrides the per-source default, an explicit operator value overrides both."
80
+ },
75
81
  "reason": {
76
82
  "type": "string",
77
83
  "minLength": 1
@@ -79,6 +85,7 @@
79
85
  },
80
86
  "$defs": {
81
87
  "provider": {
88
+ "$comment": "account_list is deliberately absent: it is operator-supplied input, not a callable provider. Committee resolution for an account list runs through the providers listed here under this same policy.",
82
89
  "enum": [
83
90
  "clay",
84
91
  "sales_nav",