@vruum/skills 0.6.34 → 0.6.36
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/package.json +2 -2
- package/skills/campaign-builder/SKILL.md +15 -3
- package/skills/create-content/SKILL.md +11 -0
- package/skills/demand-gen-loop/SKILL.md +5 -4
- package/skills/pipeline-fill/RESEARCH-ENGINE.md +103 -14
- package/skills/pipeline-fill/SKILL.md +11 -5
- package/skills/pipeline-fill/contracts/examples.json +95 -0
- package/skills/pipeline-fill/contracts/run-progress.schema.json +126 -0
- package/skills/pipeline-fill/contracts/source-policy.schema.json +399 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vruum",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.36",
|
|
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.
|
|
3
|
+
"version": "0.6.36",
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vruum/skills",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.36",
|
|
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": "
|
|
45
|
+
"contentHash": "9271a000625e137db6e7f5bb1daf4663ec97de8f38eac6839f05db100f864021"
|
|
46
46
|
}
|
|
@@ -32,6 +32,18 @@ Call `search` with `type="people"` and the criteria (include `filters={research_
|
|
|
32
32
|
|
|
33
33
|
Iterate with the seller until the cohort is right ("too broad — only the US ones" → add the region attribute). This is the step to get right; everything after is mechanical.
|
|
34
34
|
|
|
35
|
+
### Named-account sourcing handoff
|
|
36
|
+
|
|
37
|
+
If the criteria names organizations/accounts and the preview has fewer people than needed, do not create an empty campaign or pretend the accounts are contacts. Hand off the missing-account cohort to `/pipeline-fill` discovery before Step 3:
|
|
38
|
+
|
|
39
|
+
- Build `source_policy` using `.agents/skills/pipeline-fill/contracts/source-policy.schema.json`.
|
|
40
|
+
- Preserve every explicit provider instruction. Example: "use Clay, no Sales Nav or CSV" becomes `selected_source: "clay"`, `source_mode: "preferred"`, `prohibited_sources: ["sales_nav", "linkedin", "csv"]`, and an ordered `allowed_fallbacks` list.
|
|
41
|
+
- Use the schema defaults unless the seller overrides them: company waves 10, person waves 5, and two retries for transient failures. `source_mode: "exclusive"` requires `allowed_fallbacks: []`.
|
|
42
|
+
- Pass the named organizations and campaign criteria as the discovery ICP brief.
|
|
43
|
+
- Set pipeline-fill `mode: "save"` explicitly. This persists approved people so their IDs can return here, but cannot enroll them or start outreach.
|
|
44
|
+
- Let `/pipeline-fill` source companies first, resolve up to five matching people per company in bounded waves, preview the people, and return their IDs.
|
|
45
|
+
- Resume here only with the approved person IDs. This handoff is sourcing only; it never launches outreach.
|
|
46
|
+
|
|
35
47
|
## Step 3: Create the campaign
|
|
36
48
|
|
|
37
49
|
Two paths — ask which:
|
|
@@ -41,9 +53,9 @@ Two paths — ask which:
|
|
|
41
53
|
|
|
42
54
|
## Step 4: Assign the cohort
|
|
43
55
|
|
|
44
|
-
Collect the person ids from the Step 2 preview (re-run the same `search` with a higher `limit` to get the full cohort if needed — paginate with `offset` for big cohorts) and call `manage_campaign` action=members id=<campaign uuid> payload={action: "
|
|
56
|
+
Collect the person ids from the Step 2 preview (including IDs returned by the named-account handoff; re-run the same `search` with a higher `limit` to get the full cohort if needed — paginate with `offset` for big cohorts) and call `manage_campaign` action=members id=<campaign uuid> payload={action: "assign", person_ids: [...]}.
|
|
45
57
|
|
|
46
|
-
For large cohorts,
|
|
58
|
+
For large cohorts, assign in bounded batches and report requested vs updated counts. If the response contains `requires_confirmation: true`, emit `state: "paused"` with code `research_confirmation_required`, stop, and show the preview to the seller. Never set `confirm: true` without their explicit confirmation. A source-campaign 403 is a visible failed item; a response that updates fewer people than requested is `state: "partial"` with code `research_partial`.
|
|
47
59
|
|
|
48
60
|
## Step 5: Review and launch — CONFIRMATION REQUIRED
|
|
49
61
|
|
|
@@ -59,5 +71,5 @@ If the seller wants a dry run, stop after Step 4 — the campaign exists with me
|
|
|
59
71
|
## Notes
|
|
60
72
|
|
|
61
73
|
- Junk-safe personalization: contacts with garbage first names (single letters, org names) automatically get the no-name greeting variant — you don't need to filter them out of the cohort for that reason.
|
|
62
|
-
- A person can be in many lists but holds ONE campaign assignment;
|
|
74
|
+
- A person can be in many lists but holds ONE campaign assignment; assigning to a campaign moves them. Say so if the cohort overlaps an active campaign — surface counts before Step 4.
|
|
63
75
|
- Never call `manage_outreach` action=start without the Step 5 confirmation, and never auto-approve drafts; the outreach queue review (`/outreach-triage`) stays the quality gate.
|
|
@@ -88,6 +88,17 @@ Then save it **once** with `manage_content` `action="draft"`, payload `{content,
|
|
|
88
88
|
|
|
89
89
|
`action="draft"` creates a new draft row every time it runs. Save **once** per post, then show the seller the saved draft and iterate by editing.
|
|
90
90
|
|
|
91
|
+
## Step 5.5: Attach media (optional — document/PDF carousel, image, or video)
|
|
92
|
+
|
|
93
|
+
Carousels (document posts) are the top-performing organic format. If the seller has (or you produce) a PDF, image, or video for this post:
|
|
94
|
+
|
|
95
|
+
1. Store the asset: `manage_campaign` kind='ad' action='store_creative'. Small files: payload `{asset_base64, filename}`. Real files (PDFs/videos — primary path): `{filename, size_bytes, content_type: 'application/pdf' | 'video/mp4'}` → PUT the file to the returned `upload_url` (`curl --fail-with-body -T <file> '<upload_url>'`), then call store_creative again with `{creative_id}` to finalize.
|
|
96
|
+
2. Attach it: include `attachment_creative_id` in the draft payload (or add it later with `action="edit"`). Explicit `attachment_creative_id: null` on edit detaches.
|
|
97
|
+
3. The stored `filename` renders as the LinkedIn document **title** — name it like a headline, not `export-final-v3.pdf`.
|
|
98
|
+
4. Before publishing, open the `attachment_url` from `get_content_review post_ids=[<post_id>]` and review the actual file — it publishes under the seller's identity.
|
|
99
|
+
|
|
100
|
+
The caption (`content`) is still required — an attachment never replaces the post text. Scheduled posts publish with their attachment automatically.
|
|
101
|
+
|
|
91
102
|
## Step 6: Iterate by editing — never re-save
|
|
92
103
|
|
|
93
104
|
When the seller wants changes (tighten the hook, change the CTA, fix a line), revise the text yourself and update the **existing** draft with `manage_content` using `action="edit"`, passing the updated `content`.
|
|
@@ -46,9 +46,10 @@ Hold the settled audience (the resolved `{facets}` map — shown to the seller a
|
|
|
46
46
|
|
|
47
47
|
**Visual — generate or supply it, then store it as a draft.**
|
|
48
48
|
|
|
49
|
-
- **Image**: generate with your own image tools, then store via `manage_campaign` kind='ad' action='store_creative', payload `{
|
|
50
|
-
- **
|
|
51
|
-
-
|
|
49
|
+
- **Image**: generate with your own image tools, then store via `manage_campaign` kind='ad' action='store_creative', payload `{asset_base64 (raw base64, no data: URL prefix — renamed from image_base64 in VRU-726), generation_prompt, filename?, generation_provenance: {model, tool, generated_at, notes}}`.
|
|
50
|
+
- **Document / PDF carousel** (≤25MB — VRU-726, the top organic format): produce the PDF with your own tools, then store via the same action — small files: `{asset_base64, filename: 'my-deck.pdf'}` (no generation_prompt needed); real files (primary path): `{filename, size_bytes, content_type:'application/pdf'}` → returns a **presigned upload_url**; `curl --fail-with-body -T my-deck.pdf '<upload_url>'`, then call store_creative again with `{creative_id}` to finalize. The `filename` becomes the rendered LinkedIn carousel **title**. Attach to a post with `manage_content` action='draft'|'edit' payload `{attachment_creative_id}`, open the `attachment_url` from `get_content_review post_ids=[...]` to review the actual file, then publish.
|
|
51
|
+
- **Video** (mp4, ≤200MB, 3s–30min): store via the same action — `{media_url: <public https url>}` for a hosted file, or `{filename, size_bytes, content_type:'video/mp4'}` for the presigned flow above. Optional `{thumbnail_base64}`. **media_url stores are async** — poll `fetch type='ads' subtype='creative' id=<creative_id>` until `upload_status` leaves `'uploading'`; a `'failed'` status with a probe-code error means re-export the file, not retry. Videos attach to posts the same way (`attachment_creative_id` at draft/edit — the old publish-time `creative_id` param is retired), which also makes **scheduled video posts** carry their media.
|
|
52
|
+
- A stored creative (document/PDF, image, or video) attaches to the organic post at **draft/edit** (`manage_content` payload `{attachment_creative_id}`; explicit null detaches) — one asset serves the organic post AND the ad, and attachments persist on the row so scheduled posts publish with their media.
|
|
52
53
|
|
|
53
54
|
A video creative can ALSO run without any post as **Direct Sponsored Content** (`boost` with `creative_id` instead of `content_post_id`) — but DSC is **metrics-only**: no organic post means no engager bridge. Prefer the published-post path when the bridge matters.
|
|
54
55
|
|
|
@@ -76,7 +77,7 @@ If the seller hesitates on any of the four, stop at draft and leave the loop res
|
|
|
76
77
|
Only after the Step 4 approvals:
|
|
77
78
|
|
|
78
79
|
0. **Identity prerequisites (first run only)** — a Page campaign needs the Company Page set (`manage_campaign` kind='ad' action='set_page'; call with no organization_urn to discover the candidates); a Thought Leader Ad needs the author authorized (action='authorize_author'). Errors from boost name the exact fixing call — run it and retry rather than improvising.
|
|
79
|
-
1. **Publish the organic post** — `manage_content action=
|
|
80
|
+
1. **Publish the organic post** — first make sure the approved media is attached to the draft (`manage_content action=edit` payload `{attachment_creative_id}` — the old publish-time `creative_id` param is retired and now returns a 400), then `manage_content action=publish` on the draft from `/create-content`. This inherits `/create-content`'s author guard: if the chosen author's LinkedIn account isn't connected/healthy, publish fails hard rather than posting under another identity — surface that to the seller, don't retry blindly. **Wait for the post to actually be `published`** (a video publish transfers media and can take a while — re-read the post before boosting; never boost a still-publishing post).
|
|
80
81
|
2. **Boost the published post** — `manage_campaign` kind='ad' action='boost', payload `{content_post_id: <the just-published post id>, vehicle?, objective: <the approved objective>, budget: {daily_budget_cents | total_budget_cents}, audience: {facets} OR {matched_audience_id}, duration_days?, approval_mode}`. Vehicle is inferred from the post's identity — pass the objective explicitly (the default is BRAND_AWARENESS, which is NOT what a click campaign wants). Use the `approval_mode` the seller authorized in Step 4 — `draft` unless they explicitly approved the budget for `auto`. The boost double-submit case is handled for you (idempotent per source + audience + vehicle + objective), so don't paper over a retry with a second call. (DSC alternative: `creative_id` instead of `content_post_id` runs the video without a post — metrics-only, no bridge.)
|
|
81
82
|
3. **If the campaign involves a video ad**, the LinkedIn media upload runs in the background after approval — the response tells you; poll `fetch type='ads' subtype='campaign' id=<campaign_id>` (~every 30s) until it reports live or a failure with its cause.
|
|
82
83
|
4. **Thought Leader boosts return a Campaign Manager handoff, not a live campaign** — LinkedIn's public API cannot attach a member's post to the campaign (verified live), so the boost/approve response comes back with `tla_manual_attach_required` plus a Campaign Manager deep link and the exact attach steps (Ads → Add ad → Sponsor existing content → Thought leader content → pick the post; the member approves sponsorship once → Launch). Relay the link + steps to the seller verbatim; this is the expected TLA flow, not an error. **While in Campaign Manager, have them verify bidding** — the default can be manual CPM (~$10/1k impressions), which barely delivers; switch to Maximum delivery. After they launch, run `manage_campaign` kind='ad' action='resume' id=<campaign_id> to sync the local status to live. Page-identity boosts are unaffected (fully API-driven end to end).
|
|
@@ -32,6 +32,43 @@ All harness source skills produce candidate lists matching this shape exactly. T
|
|
|
32
32
|
- At minimum, each candidate needs **either** `linkedin_url` **or** (`name`-fields + `company`). Candidates with neither are skipped at Step 3.
|
|
33
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
|
+
- `source_policy`, candidate examples, and progress events have executable schemas under `contracts/`. Validate handoffs against them before provider calls.
|
|
36
|
+
|
|
37
|
+
## Source-policy and recovery contract
|
|
38
|
+
|
|
39
|
+
The canonical source policy is `contracts/source-policy.schema.json`. Validate it before
|
|
40
|
+
inventorying providers. A selected source may not also be prohibited; exclusive mode
|
|
41
|
+
has no fallbacks; ordered fallbacks may not contain prohibited sources. An explicitly
|
|
42
|
+
selected disconnected source stops before the first external call.
|
|
43
|
+
|
|
44
|
+
Every wave emits an object matching `contracts/run-progress.schema.json`, including
|
|
45
|
+
`state`, `phase`, `wave`, `wave_count`, `completed`, `total`, `failed`,
|
|
46
|
+
`not_attempted`, `safe_retry_items`, `ambiguous_items`, and `code`. A server 5xx puts
|
|
47
|
+
the failing mutation in `ambiguous_items` because its commit status is unknown; only
|
|
48
|
+
later not-attempted items go in `safe_retry_items`. Completed items are never replayed.
|
|
49
|
+
|
|
50
|
+
Stable operator-visible codes:
|
|
51
|
+
|
|
52
|
+
| Code | Meaning | Recovery |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `source_policy_invalid` | contradictory or out-of-range policy | correct the named fields before any provider call |
|
|
55
|
+
| `source_unavailable` | selected source is not connected | connect it or explicitly choose another source |
|
|
56
|
+
| `source_prohibited` | attempted source violates policy | remove the call; never override implicitly |
|
|
57
|
+
| `linkedin_identity_unresolvable` | profile is invalid/private/not found | use the next allowed structured fallback with the same `person_id` |
|
|
58
|
+
| `linkedin_temporarily_unavailable` | timeout/429 after bounded retries | retry only the returned item later; do not switch silently |
|
|
59
|
+
| `linkedin_auth_required` | LinkedIn account is disconnected/expired | reconnect LinkedIn |
|
|
60
|
+
| `company_identity_conflict` | exact evidence points to different companies | correct the evidence; never auto-merge |
|
|
61
|
+
| `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
|
+
| `person_not_visible` | supplied person is outside the caller's tenant | use a tenant-visible person or omit `person_id` |
|
|
64
|
+
| `person_not_found` | tenant membership points to a missing person | refresh the candidate list |
|
|
65
|
+
| `person_identity_conflict` | fallback identifier belongs to another person | remove the conflicting identifier and review the provider result |
|
|
66
|
+
| `person_research_save_failed` | person research persistence failed with unknown commit status | inspect stored rows before any replay |
|
|
67
|
+
| `source_campaign_forbidden` | caller cannot remove people from their current campaign | ask the source-campaign owner to move them |
|
|
68
|
+
| `research_confirmation_required` | assignment requires explicit approval | pause and show the preview; never self-confirm |
|
|
69
|
+
| `research_partial` | some items failed or were not attempted | resume only `safe_retry_items` |
|
|
70
|
+
|
|
71
|
+
Backend response details link to `backend/app/domains/people/README.md#named-account-source-errors`.
|
|
35
72
|
|
|
36
73
|
---
|
|
37
74
|
|
|
@@ -85,11 +122,11 @@ acv_floor: {dollars or default $10K}
|
|
|
85
122
|
Run your workflow (a–i) and return the structured output block.
|
|
86
123
|
```
|
|
87
124
|
|
|
88
|
-
Each subagent returns: `
|
|
125
|
+
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.
|
|
89
126
|
|
|
90
127
|
**Wait for the wave to complete before Phase B.** Phase B inputs depend on Phase A's signals (or null if failed).
|
|
91
128
|
|
|
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
|
|
129
|
+
**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 tags them `harness_gate_status: gate_inconclusive`; the Step 7 rubric gives zero company/ACV and outbound points, so their authoritative score cannot exceed 50 and the backend-enforced gate cannot enroll them. Save them for operator review and surface them in the final report so the operator can re-run the failed companies later.
|
|
93
130
|
|
|
94
131
|
**Inter-wave progress line.** After each wave (5–10 subagents):
|
|
95
132
|
```
|
|
@@ -103,6 +140,8 @@ Helps operators distinguish "still working" from "stuck."
|
|
|
103
140
|
|
|
104
141
|
**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
142
|
|
|
143
|
+
**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` to `research(action="save_person")`. 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.
|
|
144
|
+
|
|
106
145
|
Dispatch one `vruum-prospect-deep-researcher` per surviving candidate. Subagent file at `.claude/agents/vruum-prospect-deep-researcher.md`.
|
|
107
146
|
|
|
108
147
|
Dispatch prompt template:
|
|
@@ -128,7 +167,7 @@ acv_floor: {dollars}
|
|
|
128
167
|
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
168
|
```
|
|
130
169
|
|
|
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.
|
|
170
|
+
Each subagent returns: `first_name`, `last_name`, `email`, `linkedin_url`, `title`, `company_name`, `company_domain`, `company_website`, `company_linkedin_url`, `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`. Every `recent_posts` item uses the backend shape `{text, posted_at?, share_url?, reaction_count?, comment_count?}`; never send the retired `content`, `url`, `excerpt`, or `date` keys. Note: `person_id` is NOT returned here — identity resolution happens in Step 7.
|
|
132
171
|
|
|
133
172
|
**Inter-wave progress line:**
|
|
134
173
|
```
|
|
@@ -139,7 +178,7 @@ Each subagent returns: `topics_of_interest`, `recent_posts`, `opening_hooks[]` (
|
|
|
139
178
|
|
|
140
179
|
## Step 6 — Harness pre-filter gate (orchestrator-side, pre-save)
|
|
141
180
|
|
|
142
|
-
This is
|
|
181
|
+
This is the categorical first half of the harness-authoritative gate. It avoids wasted backend saves for obvious dismisses and feeds the deterministic numeric assessment in Step 7c. The backend does not re-score a supplied assessment; it records the harness score and mechanically enforces `match_score >= 70`. `MatchAnalysisAgent` is fallback-only for newly added people when callers omit assessment; duplicates retain their stored score unless a campaign move enqueues an asynchronous re-score.
|
|
143
182
|
|
|
144
183
|
Per surviving prospect, evaluate four criteria using the campaign's playbook ICP and the Phase A + Phase B signals:
|
|
145
184
|
|
|
@@ -149,7 +188,7 @@ Per surviving prospect, evaluate four criteria using the campaign's playbook ICP
|
|
|
149
188
|
|
|
150
189
|
### 2. Outbound motion or hiring signal?
|
|
151
190
|
- `outbound_motion_score > 0` OR explicit hiring trigger present → pass
|
|
152
|
-
- If no → flag `warming_candidate` (still call `manage_person` action=save_discovered — operator may want to warm-track them;
|
|
191
|
+
- If no → flag `warming_candidate` (still call `manage_person` action=save_discovered — operator may want to warm-track them; the Step 7 rubric records the weaker fit honestly)
|
|
153
192
|
|
|
154
193
|
### 3. Decision-maker level senior?
|
|
155
194
|
- `decision_maker_level == senior` → pass
|
|
@@ -175,10 +214,16 @@ For non-dismiss outcomes, also set `dismiss_reason` to null and `flag` to the re
|
|
|
175
214
|
|
|
176
215
|
## Step 7 — Save chain (everyone except harness-gate dismisses)
|
|
177
216
|
|
|
217
|
+
Apply the requested mode before any persistence:
|
|
218
|
+
|
|
219
|
+
- `research-only`: stop before Step 7a. Return the researched preview and do not call `save_company`, `save_person`, `save_discovered`, or `manage_outreach`.
|
|
220
|
+
- `save`: run Steps 7a–7c, but call `save_discovered` **without** `campaign_id`. This persists the tenant-visible prospect and gate result without assigning a campaign or starting outreach.
|
|
221
|
+
- `save-and-enroll`: run the full chain. Pass `campaign_id` to `save_discovered`, then include passing prospects in Step 7d.
|
|
222
|
+
|
|
178
223
|
Per surviving prospect:
|
|
179
224
|
|
|
180
225
|
### a. Save company research (once per company)
|
|
181
|
-
If the prospect's company isn't already cached and Phase A produced fresh research, call `research(action="save_company", payload={
|
|
226
|
+
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.
|
|
182
227
|
|
|
183
228
|
### b. Identity resolution + person research (load-bearing — corrects Codex Finding #6)
|
|
184
229
|
|
|
@@ -215,12 +260,56 @@ If the prospect's company isn't already cached and Phase A produced fresh resear
|
|
|
215
260
|
- 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.
|
|
216
261
|
- The response includes the `person_id`. Capture it for step c.
|
|
217
262
|
|
|
218
|
-
### c. Save discovered person (
|
|
263
|
+
### c. Save discovered person (authoritative harness score, backend-enforced gate)
|
|
264
|
+
|
|
265
|
+
Build the authoritative `assessment` from the campaign playbook plus Phase A/B evidence. Score mechanically so reruns agree:
|
|
266
|
+
|
|
267
|
+
- Company/ACV fit: 30 points when the known ACV class meets the campaign floor; a known miss is a harness dismiss and never reaches Step 7.
|
|
268
|
+
- Buying authority: 25 senior, 15 mid; a junior with no senior replacement is dismissed.
|
|
269
|
+
- Outbound/hiring motion: 20 when present, otherwise 0 and tag `warming`.
|
|
270
|
+
- Recent timing trigger: 15 when present, otherwise 0 and tag `low_priority`.
|
|
271
|
+
- Evidence strength: 10 for a verified profile plus at least two cited sources, 5 for partial cited evidence, 0 for unverified evidence.
|
|
272
|
+
- `gate_inconclusive` gets 0 for unknown company/ACV and outbound criteria, so it cannot exceed 50 without fresh company evidence.
|
|
273
|
+
|
|
274
|
+
The score is the sum (0–100); 70+ passes. Send this exact shape:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"match_score": 85,
|
|
279
|
+
"match_summary": "Two or three evidence-backed sentences against this campaign's ICP.",
|
|
280
|
+
"alignment_points": [
|
|
281
|
+
{
|
|
282
|
+
"point": "Specific alignment",
|
|
283
|
+
"evidence": "Cited fact and URL",
|
|
284
|
+
"confidence": 0.8,
|
|
285
|
+
"source_type": "harness_research"
|
|
286
|
+
}
|
|
287
|
+
],
|
|
288
|
+
"concerns": [
|
|
289
|
+
{
|
|
290
|
+
"concern": "Specific gap",
|
|
291
|
+
"evidence": "Cited or explicitly missing evidence",
|
|
292
|
+
"severity": "blocker|warning|minor"
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
"why_now": "Timing rationale with source",
|
|
296
|
+
"recommended_approach": "Campaign-relevant approach",
|
|
297
|
+
"overall_confidence": 0.8,
|
|
298
|
+
"scored_by": "harness:pipeline-fill"
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`match_summary` must be non-empty. Alignment items require `point` and `evidence`; concern items require `concern` and `evidence`. Confidence values are 0–1 and concern severity is exactly `blocker`, `warning`, or `minor`.
|
|
303
|
+
|
|
304
|
+
Then call `manage_person(action="save_discovered", payload={person_id: <from b>, assessment: <object above>, ...})`:
|
|
305
|
+
|
|
306
|
+
- `mode == save`: add `assessment_campaign_id: <campaign>` so the score is recorded against the campaign ICP, and omit `campaign_id` so no assignment or move occurs. New rows remain unassigned; duplicates keep their existing campaign assignment.
|
|
307
|
+
- `mode == save-and-enroll`: add `campaign_id: <campaign>`; the backend uses it for both assessment provenance and assignment. Omit `assessment_campaign_id` unless it is the same campaign.
|
|
219
308
|
|
|
220
|
-
|
|
221
|
-
-
|
|
309
|
+
This:
|
|
310
|
+
- Records the harness assessment as authoritative and skips the backend LLM scorer
|
|
222
311
|
- Returns `match_score` (0–100) and `quality_gate_pass` (bool, true iff `match_score >= 70`)
|
|
223
|
-
- Writes the `company_people` row
|
|
312
|
+
- Writes the tenant's `company_people` row; campaign assignment happens only when the payload includes `campaign_id`
|
|
224
313
|
|
|
225
314
|
**Distinguish two failure modes (Codex Finding #9):**
|
|
226
315
|
- **Request failure (5xx, timeout, network):** retry once with 2s backoff. If still failing, leave the prospect in `discovery_failed` status and surface in the final report. **Don't** claim "saved as gate-fail" — the row was never written.
|
|
@@ -256,7 +345,7 @@ Harness pre-filter gate:
|
|
|
256
345
|
gate_inconclusive : {N}
|
|
257
346
|
dismiss : {N} (top reasons: acv_too_low={N}, decision_maker_junior={N})
|
|
258
347
|
|
|
259
|
-
Backend authoritative
|
|
348
|
+
Backend-enforced gate using the authoritative harness score (match_score >= 70):
|
|
260
349
|
passed : {N}
|
|
261
350
|
failed : {N} (saved with research; operator can review via /enrich-prospect)
|
|
262
351
|
request_failed : {N} (retry candidates — surface in next run)
|
|
@@ -281,9 +370,9 @@ For multi-campaign runs, group the report by campaign and include a totals summa
|
|
|
281
370
|
## Edge cases + failure handling reference
|
|
282
371
|
|
|
283
372
|
- **Source returns empty after dedup** — orchestrator says "All {N} candidates already in pipeline, nothing to research" and exits cleanly.
|
|
284
|
-
- **Mid-flight cancellation** (operator Ctrl+C
|
|
285
|
-
- **Subagent timeout cascade** — Phase A failed for a company → Phase B runs degraded → harness
|
|
286
|
-
- **
|
|
373
|
+
- **Mid-flight cancellation** (operator Ctrl+C before Step 7) — no new Phase A/B research has been persisted. Re-running `/pipeline-fill` reuses pre-existing fresh cache entries but repeats unfinished research waves. Note this honestly in the cancellation message.
|
|
374
|
+
- **Subagent timeout cascade** — Phase A failed for a company → Phase B runs degraded → harness marks `gate_inconclusive` → Step 7 score is capped below the backend threshold. See Step 4.
|
|
375
|
+
- **Categorical/numeric divergence** — a categorical `pass` can still score below 70 when evidence strength is weak. Enrollment requires both `harness_gate_status == pass` and backend `quality_gate_pass == true`; surface both states.
|
|
287
376
|
- **Cached company research >90 days old** — Phase A re-runs the company subagent. Don't trust stale signals for an active fill.
|
|
288
377
|
- **Manual-list cap** — if >100 lines pasted, orchestrator asks "{N} prospects pasted — process all, or first M? (a/N)".
|
|
289
378
|
- **CSV >200 rows** — same prompt at Step 5 of csv-pipeline-fill.
|
|
@@ -14,7 +14,7 @@ You are a source-agnostic pipeline filler. You pick campaigns to fill, pick a so
|
|
|
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),
|
|
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.
|
|
18
18
|
|
|
19
19
|
## Where the heavy logic lives
|
|
20
20
|
|
|
@@ -51,13 +51,15 @@ The orchestrator's MCP precheck at the top of Step 3 (the `fetch` type=research_
|
|
|
51
51
|
## Inputs
|
|
52
52
|
|
|
53
53
|
- `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
|
+
- `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.
|
|
54
55
|
- `campaign(s)`: target campaign(s); multi-campaign supported.
|
|
55
56
|
- `mode`: `research-only` | `save` | `save-and-enroll` (default: `save-and-enroll`).
|
|
56
|
-
- `gate_threshold`: minimum backend `match_score` to enroll (default: campaign's existing quality_gate).
|
|
57
57
|
|
|
58
58
|
## Workflow — Step 1: Show pipeline status & pick campaigns
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Always call `fetch(type="stats", subtype="outreach")` for queue depth and `search(type="campaigns")` for campaign status. Call `import_prospects(action="sales_nav_searches", payload={action: "list"})` **only** when the operator explicitly selected Sales Nav and `source_policy.prohibited_sources` does not contain `sales_nav` or `linkedin`. A generic status check must never touch Sales Nav.
|
|
61
|
+
|
|
62
|
+
Present a numbered table with **per-campaign ETA**:
|
|
61
63
|
|
|
62
64
|
```
|
|
63
65
|
Pipeline status:
|
|
@@ -141,12 +143,16 @@ Operator gives a brief like "Series A-C SaaS founders, US, 50-500 ppl" or "direc
|
|
|
141
143
|
- **Email finder** — Hunter via `search type=companies {domain, seniority}`, or the provider's own email step — to fill the contact emails Phase B needs.
|
|
142
144
|
- **Web** (`WebSearch` / `WebFetch`) — always available; the universal fallback and a strong long-tail *company* finder (funding announcements, Crunchbase/PitchBook, vertical directories) even when a data provider is connected.
|
|
143
145
|
|
|
144
|
-
Announce the
|
|
146
|
+
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.
|
|
145
147
|
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.
|
|
146
148
|
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.
|
|
147
149
|
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.
|
|
148
150
|
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.
|
|
149
151
|
|
|
152
|
+
Emit progress objects matching `contracts/run-progress.schema.json` after every bounded wave. Company-provider actions run in waves of at most 10; person/LinkedIn/provider contact actions run in waves of at most 5. Never submit a mixed unbounded batch and wait without a progress update.
|
|
153
|
+
|
|
154
|
+
Defaults when `source_policy` is omitted: `selected_source: null` (inventory connected discovery tools), `source_mode: "preferred"`, `prohibited_sources: []`, `allowed_fallbacks: ["web"]`, `company_wave_size: 10`, `person_wave_size: 5`, and `transient_retry_attempts: 2`. Operator language such as "no Sales Nav" or "no CSV" is parsed into `prohibited_sources` before validation and overrides defaults.
|
|
155
|
+
|
|
150
156
|
Discovery-path candidates produced in either path use the canonical shape in `RESEARCH-ENGINE.md` and feed into Step 3 the same way.
|
|
151
157
|
|
|
152
158
|
**Path detection:** if the first non-comment line looks like a URL or has commas (paste-shaped), use Path A. If it's prose without URLs/commas and >40 chars, use Path B. If ambiguous, ask: "paste, or describe the ICP and I discover?"
|
|
@@ -161,5 +167,5 @@ Do not duplicate the engine logic in this skill — link operators back to the e
|
|
|
161
167
|
|
|
162
168
|
- **Composability** with source skills: source skills produce candidate lists; this orchestrator runs the research engine. Both directions allowed (operator can run a source skill standalone or run /pipeline-fill as the front door).
|
|
163
169
|
- **Real money costs** are in Phase B (LinkedIn API + Hunter calls + OpenAI tokens for the prospect subagent). Phase A is mostly WebFetch/WebSearch which is operator-network. The batch primitives in Step 3 keep dedup latency low (~2s vs 12s pre-batch).
|
|
164
|
-
- **Harness offload framing**: deep research
|
|
170
|
+
- **Harness offload framing**: deep research and the authoritative campaign score run in your IDE (your tokens). The backend validates the payload, records provenance, and mechanically enforces `match_score >= 70`; `MatchAnalysisAgent` is fallback-only for newly added people when a caller omits assessment. Duplicates retain their stored score unless a campaign move enqueues an asynchronous re-score.
|
|
165
171
|
- **Audit trail**: every run writes to `.context/runs/pipeline-fill-{ISO-timestamp}.md`. Useful weeks later for "what did the YC fill on Apr 12 import?"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source_policies": {
|
|
3
|
+
"clay_without_sales_nav": {
|
|
4
|
+
"version": "1",
|
|
5
|
+
"selected_source": "clay",
|
|
6
|
+
"source_mode": "preferred",
|
|
7
|
+
"prohibited_sources": [
|
|
8
|
+
"sales_nav",
|
|
9
|
+
"linkedin",
|
|
10
|
+
"csv"
|
|
11
|
+
],
|
|
12
|
+
"allowed_fallbacks": [
|
|
13
|
+
"web",
|
|
14
|
+
"hunter"
|
|
15
|
+
],
|
|
16
|
+
"company_wave_size": 10,
|
|
17
|
+
"person_wave_size": 5,
|
|
18
|
+
"transient_retry_attempts": 2,
|
|
19
|
+
"reason": "Named-account sourcing requested through Clay."
|
|
20
|
+
},
|
|
21
|
+
"clay_exclusive": {
|
|
22
|
+
"version": "1",
|
|
23
|
+
"selected_source": "clay",
|
|
24
|
+
"source_mode": "exclusive",
|
|
25
|
+
"prohibited_sources": [
|
|
26
|
+
"sales_nav",
|
|
27
|
+
"linkedin",
|
|
28
|
+
"csv"
|
|
29
|
+
],
|
|
30
|
+
"allowed_fallbacks": [],
|
|
31
|
+
"company_wave_size": 10,
|
|
32
|
+
"person_wave_size": 5,
|
|
33
|
+
"transient_retry_attempts": 2,
|
|
34
|
+
"reason": "Use only Clay; stop if it is unavailable."
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"progress": {
|
|
38
|
+
"company_wave": {
|
|
39
|
+
"version": "1",
|
|
40
|
+
"state": "running",
|
|
41
|
+
"phase": "company_research",
|
|
42
|
+
"provider": "clay",
|
|
43
|
+
"wave": 1,
|
|
44
|
+
"wave_count": 1,
|
|
45
|
+
"completed": 7,
|
|
46
|
+
"total": 7,
|
|
47
|
+
"failed": 0,
|
|
48
|
+
"not_attempted": 0,
|
|
49
|
+
"safe_retry_items": [],
|
|
50
|
+
"ambiguous_items": [],
|
|
51
|
+
"code": null,
|
|
52
|
+
"elapsed_seconds": 34,
|
|
53
|
+
"eta_seconds": 0
|
|
54
|
+
},
|
|
55
|
+
"person_wave": {
|
|
56
|
+
"version": "1",
|
|
57
|
+
"state": "partial",
|
|
58
|
+
"phase": "person_research",
|
|
59
|
+
"provider": "clay",
|
|
60
|
+
"wave": 2,
|
|
61
|
+
"wave_count": 4,
|
|
62
|
+
"completed": 9,
|
|
63
|
+
"total": 20,
|
|
64
|
+
"failed": 1,
|
|
65
|
+
"not_attempted": 10,
|
|
66
|
+
"safe_retry_items": [
|
|
67
|
+
"person-11",
|
|
68
|
+
"person-12",
|
|
69
|
+
"person-13",
|
|
70
|
+
"person-14",
|
|
71
|
+
"person-15",
|
|
72
|
+
"person-16",
|
|
73
|
+
"person-17",
|
|
74
|
+
"person-18",
|
|
75
|
+
"person-19",
|
|
76
|
+
"person-20"
|
|
77
|
+
],
|
|
78
|
+
"ambiguous_items": [
|
|
79
|
+
"person-10"
|
|
80
|
+
],
|
|
81
|
+
"code": "research_partial",
|
|
82
|
+
"elapsed_seconds": 58,
|
|
83
|
+
"eta_seconds": null
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"recent_posts": [
|
|
87
|
+
{
|
|
88
|
+
"text": "We are opening a new clinic.",
|
|
89
|
+
"posted_at": "2026-07-20T14:00:00Z",
|
|
90
|
+
"share_url": "https://www.linkedin.com/feed/update/urn:li:activity:1",
|
|
91
|
+
"reaction_count": 12,
|
|
92
|
+
"comment_count": 3
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://vruum.ai/contracts/pipeline-fill/run-progress.schema.json",
|
|
4
|
+
"title": "Pipeline Fill Run Progress",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"version",
|
|
9
|
+
"state",
|
|
10
|
+
"phase",
|
|
11
|
+
"wave",
|
|
12
|
+
"wave_count",
|
|
13
|
+
"completed",
|
|
14
|
+
"total",
|
|
15
|
+
"failed",
|
|
16
|
+
"not_attempted",
|
|
17
|
+
"safe_retry_items",
|
|
18
|
+
"ambiguous_items",
|
|
19
|
+
"code"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"version": {
|
|
23
|
+
"const": "1"
|
|
24
|
+
},
|
|
25
|
+
"state": {
|
|
26
|
+
"enum": [
|
|
27
|
+
"running",
|
|
28
|
+
"completed",
|
|
29
|
+
"partial",
|
|
30
|
+
"paused",
|
|
31
|
+
"failed"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"phase": {
|
|
35
|
+
"enum": [
|
|
36
|
+
"company_research",
|
|
37
|
+
"person_research",
|
|
38
|
+
"assignment"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"provider": {
|
|
42
|
+
"type": [
|
|
43
|
+
"string",
|
|
44
|
+
"null"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"wave": {
|
|
48
|
+
"type": "integer",
|
|
49
|
+
"minimum": 1
|
|
50
|
+
},
|
|
51
|
+
"wave_count": {
|
|
52
|
+
"type": "integer",
|
|
53
|
+
"minimum": 1
|
|
54
|
+
},
|
|
55
|
+
"completed": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"minimum": 0
|
|
58
|
+
},
|
|
59
|
+
"total": {
|
|
60
|
+
"type": "integer",
|
|
61
|
+
"minimum": 0
|
|
62
|
+
},
|
|
63
|
+
"failed": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 0
|
|
66
|
+
},
|
|
67
|
+
"not_attempted": {
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"minimum": 0
|
|
70
|
+
},
|
|
71
|
+
"safe_retry_items": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": {
|
|
74
|
+
"oneOf": [
|
|
75
|
+
{
|
|
76
|
+
"type": "string",
|
|
77
|
+
"minLength": 1
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "integer",
|
|
81
|
+
"minimum": 0
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"type": "object"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"ambiguous_items": {
|
|
90
|
+
"type": "array",
|
|
91
|
+
"description": "Failed mutations whose commit status is unknown; inspect before replaying.",
|
|
92
|
+
"items": {
|
|
93
|
+
"oneOf": [
|
|
94
|
+
{
|
|
95
|
+
"type": "string",
|
|
96
|
+
"minLength": 1
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "integer",
|
|
100
|
+
"minimum": 0
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "object"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"code": {
|
|
109
|
+
"type": [
|
|
110
|
+
"string",
|
|
111
|
+
"null"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"elapsed_seconds": {
|
|
115
|
+
"type": "number",
|
|
116
|
+
"minimum": 0
|
|
117
|
+
},
|
|
118
|
+
"eta_seconds": {
|
|
119
|
+
"type": [
|
|
120
|
+
"number",
|
|
121
|
+
"null"
|
|
122
|
+
],
|
|
123
|
+
"minimum": 0
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://vruum.ai/contracts/pipeline-fill/source-policy.schema.json",
|
|
4
|
+
"title": "Pipeline Fill Source Policy",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"version",
|
|
9
|
+
"selected_source",
|
|
10
|
+
"source_mode",
|
|
11
|
+
"prohibited_sources",
|
|
12
|
+
"allowed_fallbacks",
|
|
13
|
+
"company_wave_size",
|
|
14
|
+
"person_wave_size",
|
|
15
|
+
"transient_retry_attempts"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"version": {
|
|
19
|
+
"const": "1"
|
|
20
|
+
},
|
|
21
|
+
"selected_source": {
|
|
22
|
+
"oneOf": [
|
|
23
|
+
{
|
|
24
|
+
"$ref": "#/$defs/provider"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "null"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"default": null
|
|
31
|
+
},
|
|
32
|
+
"source_mode": {
|
|
33
|
+
"enum": [
|
|
34
|
+
"preferred",
|
|
35
|
+
"exclusive"
|
|
36
|
+
],
|
|
37
|
+
"default": "preferred"
|
|
38
|
+
},
|
|
39
|
+
"prohibited_sources": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {
|
|
42
|
+
"$ref": "#/$defs/provider"
|
|
43
|
+
},
|
|
44
|
+
"uniqueItems": true,
|
|
45
|
+
"default": []
|
|
46
|
+
},
|
|
47
|
+
"allowed_fallbacks": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": {
|
|
50
|
+
"$ref": "#/$defs/provider"
|
|
51
|
+
},
|
|
52
|
+
"uniqueItems": true,
|
|
53
|
+
"default": [
|
|
54
|
+
"web"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"company_wave_size": {
|
|
58
|
+
"type": "integer",
|
|
59
|
+
"minimum": 1,
|
|
60
|
+
"maximum": 10,
|
|
61
|
+
"default": 10
|
|
62
|
+
},
|
|
63
|
+
"person_wave_size": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 1,
|
|
66
|
+
"maximum": 5,
|
|
67
|
+
"default": 5
|
|
68
|
+
},
|
|
69
|
+
"transient_retry_attempts": {
|
|
70
|
+
"type": "integer",
|
|
71
|
+
"minimum": 0,
|
|
72
|
+
"maximum": 2,
|
|
73
|
+
"default": 2
|
|
74
|
+
},
|
|
75
|
+
"reason": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"minLength": 1
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"$defs": {
|
|
81
|
+
"provider": {
|
|
82
|
+
"enum": [
|
|
83
|
+
"clay",
|
|
84
|
+
"sales_nav",
|
|
85
|
+
"linkedin",
|
|
86
|
+
"csv",
|
|
87
|
+
"yc",
|
|
88
|
+
"web",
|
|
89
|
+
"hunter",
|
|
90
|
+
"manual"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"allOf": [
|
|
95
|
+
{
|
|
96
|
+
"if": {
|
|
97
|
+
"properties": {
|
|
98
|
+
"source_mode": {
|
|
99
|
+
"const": "exclusive"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"required": [
|
|
103
|
+
"source_mode"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"then": {
|
|
107
|
+
"properties": {
|
|
108
|
+
"allowed_fallbacks": {
|
|
109
|
+
"maxItems": 0
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"not": {
|
|
116
|
+
"anyOf": [
|
|
117
|
+
{
|
|
118
|
+
"properties": {
|
|
119
|
+
"selected_source": {
|
|
120
|
+
"const": "clay"
|
|
121
|
+
},
|
|
122
|
+
"prohibited_sources": {
|
|
123
|
+
"contains": {
|
|
124
|
+
"const": "clay"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"required": [
|
|
129
|
+
"selected_source",
|
|
130
|
+
"prohibited_sources"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"properties": {
|
|
135
|
+
"selected_source": {
|
|
136
|
+
"const": "sales_nav"
|
|
137
|
+
},
|
|
138
|
+
"prohibited_sources": {
|
|
139
|
+
"contains": {
|
|
140
|
+
"const": "sales_nav"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"required": [
|
|
145
|
+
"selected_source",
|
|
146
|
+
"prohibited_sources"
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"properties": {
|
|
151
|
+
"selected_source": {
|
|
152
|
+
"const": "linkedin"
|
|
153
|
+
},
|
|
154
|
+
"prohibited_sources": {
|
|
155
|
+
"contains": {
|
|
156
|
+
"const": "linkedin"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"required": [
|
|
161
|
+
"selected_source",
|
|
162
|
+
"prohibited_sources"
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"properties": {
|
|
167
|
+
"selected_source": {
|
|
168
|
+
"const": "csv"
|
|
169
|
+
},
|
|
170
|
+
"prohibited_sources": {
|
|
171
|
+
"contains": {
|
|
172
|
+
"const": "csv"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"required": [
|
|
177
|
+
"selected_source",
|
|
178
|
+
"prohibited_sources"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"properties": {
|
|
183
|
+
"selected_source": {
|
|
184
|
+
"const": "yc"
|
|
185
|
+
},
|
|
186
|
+
"prohibited_sources": {
|
|
187
|
+
"contains": {
|
|
188
|
+
"const": "yc"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"required": [
|
|
193
|
+
"selected_source",
|
|
194
|
+
"prohibited_sources"
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"properties": {
|
|
199
|
+
"selected_source": {
|
|
200
|
+
"const": "web"
|
|
201
|
+
},
|
|
202
|
+
"prohibited_sources": {
|
|
203
|
+
"contains": {
|
|
204
|
+
"const": "web"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"required": [
|
|
209
|
+
"selected_source",
|
|
210
|
+
"prohibited_sources"
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"properties": {
|
|
215
|
+
"selected_source": {
|
|
216
|
+
"const": "hunter"
|
|
217
|
+
},
|
|
218
|
+
"prohibited_sources": {
|
|
219
|
+
"contains": {
|
|
220
|
+
"const": "hunter"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"required": [
|
|
225
|
+
"selected_source",
|
|
226
|
+
"prohibited_sources"
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"properties": {
|
|
231
|
+
"selected_source": {
|
|
232
|
+
"const": "manual"
|
|
233
|
+
},
|
|
234
|
+
"prohibited_sources": {
|
|
235
|
+
"contains": {
|
|
236
|
+
"const": "manual"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"required": [
|
|
241
|
+
"selected_source",
|
|
242
|
+
"prohibited_sources"
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"not": {
|
|
250
|
+
"anyOf": [
|
|
251
|
+
{
|
|
252
|
+
"properties": {
|
|
253
|
+
"allowed_fallbacks": {
|
|
254
|
+
"contains": {
|
|
255
|
+
"const": "clay"
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"prohibited_sources": {
|
|
259
|
+
"contains": {
|
|
260
|
+
"const": "clay"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"required": [
|
|
265
|
+
"allowed_fallbacks",
|
|
266
|
+
"prohibited_sources"
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"properties": {
|
|
271
|
+
"allowed_fallbacks": {
|
|
272
|
+
"contains": {
|
|
273
|
+
"const": "sales_nav"
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
"prohibited_sources": {
|
|
277
|
+
"contains": {
|
|
278
|
+
"const": "sales_nav"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"required": [
|
|
283
|
+
"allowed_fallbacks",
|
|
284
|
+
"prohibited_sources"
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"properties": {
|
|
289
|
+
"allowed_fallbacks": {
|
|
290
|
+
"contains": {
|
|
291
|
+
"const": "linkedin"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"prohibited_sources": {
|
|
295
|
+
"contains": {
|
|
296
|
+
"const": "linkedin"
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
"required": [
|
|
301
|
+
"allowed_fallbacks",
|
|
302
|
+
"prohibited_sources"
|
|
303
|
+
]
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"properties": {
|
|
307
|
+
"allowed_fallbacks": {
|
|
308
|
+
"contains": {
|
|
309
|
+
"const": "csv"
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
"prohibited_sources": {
|
|
313
|
+
"contains": {
|
|
314
|
+
"const": "csv"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
"required": [
|
|
319
|
+
"allowed_fallbacks",
|
|
320
|
+
"prohibited_sources"
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"properties": {
|
|
325
|
+
"allowed_fallbacks": {
|
|
326
|
+
"contains": {
|
|
327
|
+
"const": "yc"
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
"prohibited_sources": {
|
|
331
|
+
"contains": {
|
|
332
|
+
"const": "yc"
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
"required": [
|
|
337
|
+
"allowed_fallbacks",
|
|
338
|
+
"prohibited_sources"
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"properties": {
|
|
343
|
+
"allowed_fallbacks": {
|
|
344
|
+
"contains": {
|
|
345
|
+
"const": "web"
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
"prohibited_sources": {
|
|
349
|
+
"contains": {
|
|
350
|
+
"const": "web"
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
"required": [
|
|
355
|
+
"allowed_fallbacks",
|
|
356
|
+
"prohibited_sources"
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"properties": {
|
|
361
|
+
"allowed_fallbacks": {
|
|
362
|
+
"contains": {
|
|
363
|
+
"const": "hunter"
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
"prohibited_sources": {
|
|
367
|
+
"contains": {
|
|
368
|
+
"const": "hunter"
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
"required": [
|
|
373
|
+
"allowed_fallbacks",
|
|
374
|
+
"prohibited_sources"
|
|
375
|
+
]
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"properties": {
|
|
379
|
+
"allowed_fallbacks": {
|
|
380
|
+
"contains": {
|
|
381
|
+
"const": "manual"
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
"prohibited_sources": {
|
|
385
|
+
"contains": {
|
|
386
|
+
"const": "manual"
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
"required": [
|
|
391
|
+
"allowed_fallbacks",
|
|
392
|
+
"prohibited_sources"
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
}
|