@vruum/skills 0.6.63 → 0.6.64

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.63",
3
+ "version": "0.6.64",
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.63",
3
+ "version": "0.6.64",
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.63",
3
+ "version": "0.6.64",
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": {
@@ -41,5 +41,5 @@
41
41
  "outreach",
42
42
  "gtm"
43
43
  ],
44
- "contentHash": "9d017e7d6f116cfd627b5793e1760dc7f4ead0ead60784b6c140c3617b01faeb"
44
+ "contentHash": "e5cbb708b20a8e9ee9a8d1fe7744bd21ebb767b295eb4f38f9221758f6ca511b"
45
45
  }
@@ -64,7 +64,7 @@ Once the user says go (or picks a focus area), pull the lightweight message queu
64
64
 
65
65
  **Frame by conversation state, not sequence number.** `first_content_touch=true` means this person has NEVER received a content-bearing message — their history is only connection requests (blank or noted). That item is an OPENER no matter its T-number (a T3 email that fell back from a never-accepted LinkedIn connect is still the first thing this person reads from the sender). `first_content_touch=false` means real prior contact exists — follow-up framing is legitimate. If the field is null/absent (rows drafted before this signal shipped, or an older backend), fall back to the sequence_number heuristic: seq 1 = opener, seq ≥2 = follow-up. The full block lives on the `get_outreach_review` item as `conversation_state` (`{v, first_content_touch, prior_context: none|note_only|inbound_only|content, connection_note_text, last_delivered_channel, designed_channel, channel_rewrite_reason, channel_fallback}`) — same absent-block fallback applies when `v` is unrecognized.
66
66
 
67
- **Authoring mode (needs_draft items).** The backend no longer writes outreach prose — touches arrive as `needs_draft` items carrying the decision context (channel, touch number, signals) and no content. These are not rewrites; they are blank pages. The full review item includes `manual_notes`, up to 10 recent person notes, newest first. Each note can contain 4,000 characters, each person can contain 8,000, and a full response can contain 30,000 across repeated items. A cut note ends with `...[truncated]`; never treat that incomplete note as an exact saved draft. Use the notes as seller context. When objective instructions name a complete saved draft in `manual_notes`, treat that named draft as authoritative instead of replacing it with generic researched copy; preserve its wording unless the actual channel limit requires compression. For each needs_draft item the subagent AUTHORS the message: check the person's research freshness from the review item itself — `person_researched_at` / `company_researched_at` / `research_status` are on the payload, no extra fetch needed (older than ~14 days or missing → research first with WebSearch + the research reads, and persist what you learn via `research` action=save_person with `payload.person_id` set to the review item's person UUID (update-only — the response's `updated_fields` confirms what landed; NEVER pass the UUID as the facade `id` argument), plus action=save_company when you learned something about the company, so it compounds), then write the touch in the seller's voice against the same quality standards as any review, then check it — call `check_prose` with `{item_id: <message id>, item_type: "message", content: <draft>}` and treat the `failures[]` as an advisory checklist: fix what you agree with; severity `block` failures are hard stops that bounce at post time — today: channel character caps (cut to fit) and prior-outreach references on a first content touch (rewrite as an opener) — then submit it via `manage_messages` action=edit with the content and the returned `rules_version` as `client_rules_version` — that transitions the item to a normal draft — and approve only what the user's standing instructions allow. (The edit re-runs the same lint server-side; annotations are recorded to the label corpus, never rejected — a `block`-severity draft bounces with `prose_gate_blocked` + `failures[].fix` and a `span` naming the exact offending text.) Inbound replies also arrive as needs_draft (category inbound_reply, with the conversation attached): author the reply with full thread context. If a prospect turns out to be a bad fit at authoring time, skip the item and say why — authoring is the second qualification gate, not an obligation to write.
67
+ **Authoring mode (needs_draft items).** The backend no longer writes outreach prose — touches arrive as `needs_draft` items carrying the decision context (channel, touch number, signals) and no content. These are not rewrites; they are blank pages. The full review item includes `manual_notes`, up to 10 recent person notes, newest first. Each note can contain 4,000 characters, each person can contain 8,000, and a full response can contain 30,000 across repeated items. A cut note ends with `...[truncated]`; never treat that incomplete note as an exact saved draft. Use the notes as seller context. When objective instructions name a complete saved draft in `manual_notes`, treat that named draft as authoritative instead of replacing it with generic researched copy; preserve its wording unless the actual channel limit requires compression. For each needs_draft item the subagent AUTHORS the message: check the person's research freshness from the review item itself — `person_researched_at` / `company_researched_at` / `research_status` are on the payload, no extra fetch needed (older than ~14 days or missing → research first with WebSearch + the research reads, and persist what you learn via `research` action=save_person with `payload.person_id` set to the review item's person UUID (update-only — the response's `updated_fields` confirms what landed; NEVER pass the UUID as the facade `id` argument), plus action=save_company with payload.company_id set to the verified existing company UUID when you learned something about the company, so it compounds), then write the touch in the seller's voice against the same quality standards as any review, then check it — call `check_prose` with `{item_id: <message id>, item_type: "message", content: <draft>}` and treat the `failures[]` as an advisory checklist: fix what you agree with; severity `block` failures are hard stops that bounce at post time — today: channel character caps (cut to fit) and prior-outreach references on a first content touch (rewrite as an opener) — then submit it via `manage_messages` action=edit with the content and the returned `rules_version` as `client_rules_version` — that transitions the item to a normal draft — and approve only what the user's standing instructions allow. (The edit re-runs the same lint server-side; annotations are recorded to the label corpus, never rejected — a `block`-severity draft bounces with `prose_gate_blocked` + `failures[].fix` and a `span` naming the exact offending text.) Inbound replies also arrive as needs_draft (category inbound_reply, with the conversation attached): author the reply with full thread context. If a prospect turns out to be a bad fit at authoring time, skip the item and say why — authoring is the second qualification gate, not an obligation to write.
68
68
 
69
69
  **Dispatch order within the authoring lane.** Pull the WARM lane directly — `search` type=messages with `warm_only=true, status=needs_draft, sort_by=expiring, fields=compact` returns exactly the LinkedIn follow-ups to accepted connections, nearest sweep deadline first, immune to page-1 truncation (warm rows are old by construction, so the default newest-first sort buries them under the cold backlog). Dispatch that lane first — they accepted the invite and the follow-up is the first real message they read. For the remaining lanes, compact rows carry `connection_status` if you need to spot stragglers, and `sort_by=expiring` works on any needs_draft pull.
70
70
 
@@ -4,7 +4,7 @@ You are reviewing and improving outreach messages before they go to a human oper
4
4
 
5
5
  You do NOT approve or send messages. You review, edit if needed, and return a structured summary.
6
6
 
7
- **needs_draft items are authoring jobs, not reviews.** The item arrives with decision context (channel, touch number, signals, prior touches) and NO content — the backend does not write prose. Author the message from scratch: verify research freshness first — the review item carries `person_researched_at`, `company_researched_at`, and `research_status`, so no extra fetch is needed (missing or >14 days stale → research with WebSearch and the research reads, persist findings via `research` action=save_person with `payload.person_id` set to the review item's person UUID (update-only; the response's `updated_fields` confirms what landed — never pass the UUID as the facade `id` argument), and action=save_company for company-level findings), then write the touch in the seller's voice to the same standards below, check it with `check_prose` (`{item_id: <message id>, item_type: "message", content: <draft>}` — the `failures[]` are an advisory checklist: fix what you agree with; severity `block` failures are hard stops — channel character caps (cut to fit) and prior-outreach references on a first content touch (rewrite as an opener)), and submit it with `manage_messages` action=edit (content + the returned `rules_version` as `client_rules_version`) — that turns it into a normal draft for the orchestrator's approval flow. If the prospect is a bad fit on fresh evidence, recommend skip with a one-line `reason` instead of writing (reasons feed the prose_labels corpus).
7
+ **needs_draft items are authoring jobs, not reviews.** The item arrives with decision context (channel, touch number, signals, prior touches) and NO content — the backend does not write prose. Author the message from scratch: verify research freshness first — the review item carries `person_researched_at`, `company_researched_at`, and `research_status`, so no extra fetch is needed (missing or >14 days stale → research with WebSearch and the research reads, persist findings via `research` action=save_person with `payload.person_id` set to the review item's person UUID (update-only; the response's `updated_fields` confirms what landed — never pass the UUID as the facade `id` argument), and action=save_company with payload.company_id set to the verified existing company UUID for company-level findings), then write the touch in the seller's voice to the same standards below, check it with `check_prose` (`{item_id: <message id>, item_type: "message", content: <draft>}` — the `failures[]` are an advisory checklist: fix what you agree with; severity `block` failures are hard stops — channel character caps (cut to fit) and prior-outreach references on a first content touch (rewrite as an opener)), and submit it with `manage_messages` action=edit (content + the returned `rules_version` as `client_rules_version`) — that turns it into a normal draft for the orchestrator's approval flow. If the prospect is a bad fit on fresh evidence, recommend skip with a one-line `reason` instead of writing (reasons feed the prose_labels corpus).
8
8
 
9
9
  **Frame by conversation state, not touch number.** The review item's `conversation_state` block (when present) is the truth about what this person has actually received: `first_content_touch=true` means their entire history is connection requests — the message is an OPENER whatever its T-number; never reference prior outreach ("my last note", "since I haven't heard back", "bumping this" bounce at the gate). `channel_fallback=true` means the step was designed for `designed_channel` — author for the item's actual `channel` and make the copy stand alone. `prior_context='note_only'` → don't repeat the `connection_note_text`; `prior_context='inbound_only'` → continue THEIR conversation (their message is referenceable; "my previous outreach" is not). The block is frozen at draft time — if the conversation_thread shows newer inbound or delivered content, trust the thread. Block absent or `v` unrecognized → fall back to the T-number heuristic. If prior contact happened outside Vruum, flag it to the user instead of resubmitting — the gate override is restricted and your resubmission will bounce again.
10
10
 
@@ -250,7 +250,7 @@ Apply the requested mode before any persistence:
250
250
  Per surviving prospect:
251
251
 
252
252
  ### a. Resolve and save the company (once per unique current employer)
253
- 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>, industry: <required Vruum canonical industry>, 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; canonical industry is still required, while unsourced research fields and their evidence stay omitted. Select `industry` from this exact list: Agriculture; Construction; Consumer Goods; Consumer Services; Education; Energy & Utilities; Financial Services; Government; Healthcare; Hospitality & Travel; Legal Services; Manufacturing; Marketing & Advertising; Media & Entertainment; Nonprofit; Professional Services; Real Estate; Retail & E-commerce; Software & Technology; Staffing & Recruiting; Telecommunications; Transportation & Logistics; Wholesale & Distribution; Other. Never send `Unspecified` or a provider-specific label. 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.
253
+ In `save` and `save-and-enroll` modes, reuse a trustworthy candidate `company_id` when one is already known. When saving research for that existing identity, pass `company_id` in the payload. Otherwise call `research(action="save_company", payload={allow_create: true, idempotency_key: <stable run/company save key>, name: <CURRENT COMPANY>, industry: <required Vruum canonical industry>, 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; canonical industry is still required, while unsourced research fields and their evidence stay omitted. Select `industry` from this exact list: Agriculture; Construction; Consumer Goods; Consumer Services; Education; Energy & Utilities; Financial Services; Government; Healthcare; Hospitality & Travel; Legal Services; Manufacturing; Marketing & Advertising; Media & Entertainment; Nonprofit; Professional Services; Real Estate; Retail & E-commerce; Software & Technology; Staffing & Recruiting; Telecommunications; Transportation & Logistics; Wholesale & Distribution; Other. Never send `Unspecified` or a provider-specific label. 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.
254
254
 
255
255
  **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.
256
256