@vruum/skills
Advanced tools
| { | ||
| "name": "vruum", | ||
| "version": "0.6.46", | ||
| "version": "0.6.47", | ||
| "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,0 +5,0 @@ "author": { |
| { | ||
| "name": "vruum", | ||
| "version": "0.6.46", | ||
| "version": "0.6.47", | ||
| "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,0 +5,0 @@ "author": { |
+2
-2
| { | ||
| "name": "@vruum/skills", | ||
| "version": "0.6.46", | ||
| "version": "0.6.47", | ||
| "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.", | ||
@@ -44,3 +44,3 @@ "license": "MIT", | ||
| ], | ||
| "contentHash": "642c88b2d663edb9fc50184409cd7f569f412bc8e64bed2961c1802e85d19246" | ||
| "contentHash": "2bf8dd9bb8668bbb6d381225dfba3131bb7352b6ea03a55ab8172ae20c81e56e" | ||
| } |
@@ -42,2 +42,6 @@ --- | ||
| - `company` ← `company`, `company name`, `account`, `organization`, `org`, `employer` | ||
| - `company_id` ← `company_id`, `company id`, `vruum company id` (only accept a valid UUID from a trusted Vruum export) | ||
| - `company_domain` ← `company_domain`, `company domain`, `domain`, `account domain`, `website domain` | ||
| - `company_website` ← `company_website`, `company website`, `account website`, `organization website` | ||
| - `company_linkedin_url` ← `company_linkedin_url`, `company linkedin`, `company linkedin url`, `account linkedin url` | ||
| - `linkedin_url` ← `linkedin`, `linkedin url`, `linkedin_url`, `profile`, `linkedin profile`, `linkedin_profile`, `li_url` | ||
@@ -63,2 +67,3 @@ - `email` ← `email`, `email_address`, `work_email`, `business email` | ||
| - Validate `linkedin_url` matches `^https?://(www\.)?linkedin\.com/in/[^/?]+/?(\?.*)?$`. If invalid (e.g. `https://linkedin.com/company/...`), set to null and log. | ||
| - Normalize `company_domain` to an apex domain (no scheme/path), `company_website` to an http(s) URL, and `company_linkedin_url` to a canonical LinkedIn `/company/` or `/school/` URL. Invalid values become null and are logged; never reinterpret a person LinkedIn URL as a company anchor. | ||
| - Strip query strings from LinkedIn URLs (`?utm_source=...` etc.) — canonicalize to `https://linkedin.com/in/<slug>/`. | ||
@@ -82,2 +87,6 @@ - **Skip rows** where neither `linkedin_url` nor (`name` AND `company`) is present. Log the skipped count. | ||
| company: <company>, | ||
| company_id: <trusted Vruum company UUID or null>, | ||
| company_domain: <normalized apex or null>, | ||
| company_website: <canonical URL or null>, | ||
| company_linkedin_url: <canonical company/school URL or null>, | ||
| linkedin_url: <canonicalized URL or null>, | ||
@@ -96,2 +105,4 @@ email: <email or null>, | ||
| Carry every valid employer anchor through to the engine. Rows with only a company name may still be researched, but they are not eligible for persistence unless Phase B resolves the current employer to a strong anchor. | ||
| The `csv_extra_columns` field keeps unmapped data on the candidate so an operator can later inspect it via `search` type=people if a question comes up about why a particular prospect was imported. | ||
@@ -98,0 +109,0 @@ |
@@ -22,2 +22,6 @@ # Pipeline Fill — Research Engine | ||
| company: string | null, // "Acme Co" — null OK if linkedin_url is set | ||
| company_id: string | null, // canonical Vruum company UUID when already known | ||
| company_domain: string | null, // apex only, e.g. "acme.com" | ||
| company_website: string | null, // canonical http(s) company URL | ||
| company_linkedin_url: string | null, // canonical /company/ or /school/ URL | ||
| linkedin_url: string | null, // canonical /in/ URL — null OK if name+company set | ||
@@ -34,2 +38,4 @@ email: string | null, // null = pending lookup; engine doesn't gate on email presence | ||
| - At minimum, each candidate needs **either** `linkedin_url` **or** (`name`-fields + `company`). Candidates with neither are skipped at Step 3. | ||
| - Company anchors are additive during sourcing/research: preserve every trustworthy `company_id`, `company_domain`, `company_website`, and `company_linkedin_url` through the handoff. A company name is useful research input but is **not** a strong identity anchor. | ||
| - A candidate may enter research without a strong company anchor because Phase B can recover one from the current LinkedIn work-experience record. It may **not** enter a save call without one; Step 7's company-binding invariant is absolute. | ||
| - **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. | ||
@@ -108,3 +114,3 @@ - `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. | ||
| 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. | ||
| 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"]})`. | ||
| 3. **Batch company fixed-field reuse check.** Collect unique company domains from surviving candidates, deriving the apex from `company_website` when necessary. Call `fetch(type="company_research", id=[the domains], filters={"requested_fields":["company_summary","company_stage","current_priorities","funding_data","growth_metrics"]})`. | ||
| - Reuse only values whose field entry has `status="reusable"`. | ||
@@ -133,2 +139,4 @@ - `core_reuse.reusable` means the shared summary core is reusable; it never means the campaign brief is complete. | ||
| domain: {domain} | ||
| website: {company_website or null} | ||
| company_linkedin_url: {company_linkedin_url or null} | ||
| campaign_icp_summary: {one paragraph from the research_playbook fetch} | ||
@@ -171,2 +179,6 @@ acv_floor: {dollars or default $10K} | ||
| company: {company} | ||
| company_id: {company_id or null} | ||
| company_domain: {company_domain or null} | ||
| company_website: {company_website or null} | ||
| company_linkedin_url: {company_linkedin_url or null} | ||
| linkedin_url: {url or null} | ||
@@ -188,2 +200,4 @@ email: {email or null} | ||
| The Phase B result describes the prospect's **current** employer, not merely the company the source guessed. When LinkedIn shows a different current employer, replace the candidate's stale company fields with that current work-experience entry and its anchors before Step 7. If Phase B cannot produce either a `company_id` or `company_name` plus a valid anchor, return `STATUS: company_unresolved`; the orchestrator reports and skips that prospect instead of creating a company-less person. | ||
| **Inter-wave progress line:** | ||
@@ -224,5 +238,6 @@ ``` | ||
| - `harness_gate_status: gate_inconclusive` — Phase A failed for this prospect's company (degraded mode) | ||
| - `harness_gate_status: company_unresolved` — Phase B could not verify a current employer with a canonical company anchor; skip pipeline persistence and enrollment | ||
| - `harness_gate_status: dismiss` — failed criterion 1 (acv) or 3 (junior, no senior swap available); skip backend call entirely | ||
| For non-dismiss outcomes, also set `dismiss_reason` to null and `flag` to the relevant reason (warming|low_priority|gate_inconclusive|null). | ||
| For non-dismiss outcomes, also set `dismiss_reason` to null and `flag` to the relevant reason (warming|low_priority|gate_inconclusive|company_unresolved|null). | ||
@@ -233,4 +248,6 @@ **The gate is declarative prose — not a hardcoded function.** The orchestrator follows the rules above and tags each candidate. If a future criterion changes, edit this section. | ||
| ## Step 7 — Save chain (everyone except harness-gate dismisses) | ||
| ## Step 7 — Save chain (eligible outcomes only; dismiss and company_unresolved skip) | ||
| **Company-binding invariant (load-bearing):** every prospect that reaches pipeline persistence must have a verified current employer expressed as either `company_id` or `company_name` plus at least one strong anchor (`company_domain`, `company_website`, `company_linkedin_url`). A company name alone never qualifies. If the invariant cannot be satisfied after Phase B, mark the item `company_unresolved`, do not call `save_company` with a name-only identity, `save_discovered`, or `manage_outreach` for it, and surface it in Step 8. `save_person` may still refresh an already-saved person without changing their company, but that refresh does not make the item eligible to save or enroll. This is stricter than the candidate admission rule on purpose: research may resolve missing identity, pipeline persistence may not guess it. | ||
| Apply the requested mode before any persistence: | ||
@@ -244,4 +261,4 @@ | ||
| ### a. Save company research (once per company) | ||
| 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>, person_id: <the person's Vruum UUID, when researching an EXISTING person's employer>, 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. | ||
| ### a. Resolve and save the company (once per unique current employer) | ||
| In `save` and `save-and-enroll` modes, reuse a trustworthy candidate `company_id` when one is already known. Otherwise call `research(action="save_company", payload={idempotency_key: <stable run/company save key>, name: <CURRENT COMPANY>, website: <canonical website or domain>, linkedin_url: <canonical LinkedIn company URL>, person_id: <the person's Vruum UUID, when researching an EXISTING person's employer>, company_summary, company_stage, funding_data, growth_metrics, current_priorities: <newline-joined descriptions>, sources_by_field})` once per unique current employer and capture the returned `company_id`. This call is required for identity resolution even when Phase A produced no new fixed research fields; in that case omit those research fields and their evidence, but still send the strongest known company anchor. 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. Reuse the returned `company_id` for every prospect at that employer. | ||
@@ -262,12 +279,12 @@ **Person linkage check (VRU-767):** when the research is about a saved person's employer (triage/authoring-time refresh), ALWAYS pass their UUID as `payload.person_id` and read the response's `person_link`. `matched`/`linked`/`repointed` mean the person's future touches will see this research. `mismatch` means no pin was written because the evidence disagreed — the pin sits on a DIFFERENT company (anchored, or a stub whose name carries more identity than the researched one), or an unpinned person's known positions show no role at the researched company. People can hold multiple positions and researching a secondary employer never switches the primary pin. This research will NOT surface on their touches — verify which company they actually work for before authoring from it. `conflict` is a transient race (the pin changed mid-save): replay the identical payload with the same idempotency key once — `person_id` is exempt from the idempotency hash, so adding it to a replay of an earlier save is also the supported repair path. Never assume a bare `success` means the research reached the person. | ||
| 2. **Prepare company linkage** — the `person` block must identify the company unambiguously, ONE of: | ||
| 2. **Prepare company linkage** — every save must identify the company unambiguously, ONE of: | ||
| **Path A (preferred): `company_id`.** Run the save_company call (`research` action=save_company) first, capture the returned `company_id`. | ||
| **Path B: `company_name` + at least one anchor** (`company_domain`, `company_website`, or `company_linkedin_url`). The data is in the LinkedIn payload you already fetched. The prospect's CURRENT employer is the entry in `work_experience[]` with `end_date: null` — that entry has `company_linkedin_url`. If you ran linkedin_fetch with `include_company: true`, the company response carries `website` and `industry`. **Anchor-less name-only saves are rejected with HTTP 422.** | ||
| **Path B: `company_name` + at least one anchor** (`company_domain`, `company_website`, or `company_linkedin_url`). The data is in the LinkedIn payload you already fetched. The prospect's CURRENT employer is the entry in `work_experience[]` with `end_date: null` — that entry has `company_linkedin_url`. If you ran linkedin_fetch with `include_company: true`, the company response carries `website` and `industry`. **Anchor-less name-only saves are forbidden even if a stale backend would accept them.** | ||
| For a **new** prospect, place the linkage inside `person`. For an **existing** `person_id`, send the same linkage at the top level of `save_discovered` (`company_id`, or `company_name` plus anchors). Never assume an existing person's prior membership is already bound. | ||
| 3. **Refreshing someone ALREADY saved** (e.g. operator pasted a Vruum person UUID, or a triage-time research refresh): call `research(action="save_person", payload={person_id: <uuid>, ...fresh research fields})` — update-in-place, `researched_at` moves, and the response's `updated_fields`/`skipped_fields` tell you exactly what landed (contact fields are backfill-only; corrections go through `manage_person` action=update_contact). NEVER pass the UUID as the facade `id` argument — save_person takes no `id` and will 422. If save_person returns 404 `person_not_found_for_update`, the person isn't saved yet — use the step-c atomic save instead. | ||
| **Old-backend fallback (rollout window only):** if `save_discovered` answers with a bare `person_id: field required` 422, the backend predates this flow — fall back to the old two-step dance (save_person to create, then save_discovered with the returned person_id) until the promote lands. | ||
| ### c. Save discovered person — ONE atomic call (authoritative harness score) | ||
@@ -335,3 +352,3 @@ | ||
| **Person already saved:** `payload={person_id: <uuid>, assessment: <object above>, ...}` — applies the score update-in-place (THE path to score an existing stub). | ||
| **Person already saved:** `payload={person_id: <uuid>, company_id: <resolved company UUID>, assessment: <object above>, ...}` — applies the score update-in-place and atomically binds/promotes the current employer. If no `company_id` was resolved, pass `company_name` plus at least one top-level anchor instead. Never send a bare `person_id` from this harness. | ||
@@ -345,3 +362,3 @@ - `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. | ||
| - Dedupes on canonical anchors: if the person block's email/linkedin match someone already saved (any URL variant — www, trailing slash, encoding), the call continues as a duplicate update instead of creating | ||
| - Returns `person_id` (capture it for step d), `match_score` (0–100), `quality_gate_pass` (bool, true iff `match_score >= 70`), and `warnings[]` naming any failed best-effort side effects | ||
| - Returns `person_id` (capture it for step d), `company_id`, `company_bound`, `match_score` (0–100), `quality_gate_pass` (bool, true iff `match_score >= 70`), and `warnings[]` naming any failed best-effort side effects. Require `company_bound == true` before adding the person to Step 7d's enrollment list; a false value is `company_binding_failed`, must be surfaced, and must never be described as a successful save-and-enroll outcome. | ||
@@ -354,3 +371,3 @@ **Distinguish two failure modes (Codex Finding #9):** | ||
| Collect all `person_id`s where `harness_gate_status == pass` AND backend `quality_gate_pass == true` AND `mode == save-and-enroll`. Then call `manage_outreach(action="start", id=[those person_ids], payload={campaign_id: ...})` ONCE at the end of Step 7. | ||
| Collect all `person_id`s where `harness_gate_status == pass` AND backend `quality_gate_pass == true` AND backend `company_bound == true` AND `mode == save-and-enroll`. Then call `manage_outreach(action="start", id=[those person_ids], payload={campaign_id: ...})` ONCE at the end of Step 7. | ||
@@ -386,2 +403,3 @@ - Per-prospect outcomes are returned (enrolled | skipped | failed). Surface per-prospect failures in the report. | ||
| dismiss : {N} (top reasons: acv_too_low={N}, decision_maker_junior={N}) | ||
| company_unresolved : {N} (not saved — list the people and missing anchors) | ||
@@ -392,4 +410,5 @@ Backend-enforced gate using the authoritative harness score (match_score >= 70): | ||
| request_failed : {N} (retry candidates — surface in next run) | ||
| company_binding_failed : {N} (saved response was not company-bound; never enrolled) | ||
| Enrolled (both gates pass + auto-enroll mode): {N} | ||
| Enrolled (harness gate + backend score gate + company binding pass, in auto-enroll mode): {N} | ||
| Saved but not enrolled: {N} | ||
@@ -416,3 +435,3 @@ | ||
| - **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. | ||
| - **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. | ||
| - **Categorical/numeric divergence** — a categorical `pass` can still score below 70 when evidence strength is weak. Enrollment requires all three confirmations: `harness_gate_status == pass`, backend `quality_gate_pass == true`, and backend `company_bound == true`; surface all three states. | ||
| - **Cached company research >90 days old** — Phase A re-runs the company subagent. Don't trust stale signals for an active fill. | ||
@@ -419,0 +438,0 @@ - **Manual-list cap** — if >100 lines pasted, orchestrator asks "{N} prospects pasted — process all, or first M? (a/N)". |
@@ -143,4 +143,4 @@ --- | ||
| **Contract:** | ||
| - **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). | ||
| - **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. | ||
| - **Input:** a list of companies, each with `company_name` and/or `domain` (at least one), plus any known `company_id`, `company_website`, or `company_linkedin_url`, the campaign's ICP target titles/seniority, and a resolved `buyers_per_account` (see Inputs). | ||
| - **Output:** the canonical person-shaped candidate list defined in `RESEARCH-ENGINE.md`, ready for Step 3. Copy every trustworthy company anchor onto every resolved person (`company_id`, `company_domain`, `company_website`, `company_linkedin_url`) and set `raw_signals.source_company` so the report can group by account. Do not reduce a strongly identified account back to a name-only company during committee resolution. | ||
@@ -204,3 +204,3 @@ **Per company:** | ||
| Discovery-path candidates produced in either path use the canonical shape in `RESEARCH-ENGINE.md` and feed into Step 3 the same way. | ||
| Discovery-path candidates produced in either path use the canonical shape in `RESEARCH-ENGINE.md` and feed into Step 3 the same way. Preserve any company anchors returned by the selected provider. Name-only discoveries may proceed to Phase B, but the engine will not save them unless current-employer research supplies a strong anchor. | ||
@@ -207,0 +207,0 @@ **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?" |
@@ -37,2 +37,5 @@ --- | ||
| - `company` (from profile current company) | ||
| - `company_id` (when the preview already includes the canonical Vruum company UUID; otherwise null) | ||
| - `company_domain` / `company_website` (when present in preview company metadata) | ||
| - `company_linkedin_url` (from the current employer/company result, not the person's `/in/` URL) | ||
| - `linkedin_url` (canonicalize via the LinkedIn URL in the profile) | ||
@@ -44,2 +47,4 @@ - `title` (current title — useful for downstream classification but Phase B re-fetches authoritatively) | ||
| Preserve all available employer anchors even though Phase B re-fetches the profile. A name-only `company` is research context, not sufficient save identity; if neither the preview nor Phase B yields a strong current-employer anchor, the engine reports `company_unresolved` and does not save or enroll that prospect. | ||
| ### Step 4: Hand off to /pipeline-fill (canonical handoff prompt) | ||
@@ -46,0 +51,0 @@ |
@@ -118,2 +118,6 @@ --- | ||
| company: <yc company name>, | ||
| company_id: null, | ||
| company_domain: <apex derived from YC website or null>, | ||
| company_website: <canonical YC website URL or null>, | ||
| company_linkedin_url: null, // Phase B may fill from founder's current role | ||
| linkedin_url: <founder linkedin_url, canonicalized>, | ||
@@ -136,3 +140,3 @@ email: null, // Phase B finds it | ||
| The `yc_news` entries are pre-loaded triggers — Phase A's company subagent uses them in lieu of an extra WebSearch. | ||
| The `yc_news` entries are pre-loaded triggers — Phase A's company subagent uses them in lieu of an extra WebSearch. The YC `website` field is also a first-party company identity anchor: normalize and preserve it instead of handing the engine only the company name. If it is absent or malformed, Phase B must recover a current-employer anchor before the founder can be saved. | ||
@@ -139,0 +143,0 @@ ### Step 7: Pool exhaustion check |
360274
1.88%