@vruum/skills 0.6.36 → 0.6.38

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.36",
3
+ "version": "0.6.38",
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.36",
3
+ "version": "0.6.38",
4
4
  "description": "Vruum AI skills + remote MCP server for B2B GTM teams. Skills for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis, paired with the full Vruum MCP tool surface over OAuth 2.1.",
5
5
  "author": {
6
6
  "name": "Vruum AI",
package/README.md CHANGED
@@ -63,6 +63,14 @@ Download and upload via **Settings → Customize → Plugins → "+"**.
63
63
  npx @vruum/skills install --target /path/to/skills/dir
64
64
  ```
65
65
 
66
+ ## Coexisting with operator skills
67
+
68
+ The public and operator skill bundles can coexist in the same harness. For
69
+ overlapping skill names, a valid operator-owned installation takes precedence.
70
+ Public installs and uninstalls preserve the operator bundle's links and update
71
+ checker. If an operator link becomes stale, the next public install restores
72
+ the public skill instead of leaving a broken link.
73
+
66
74
  ## Skills
67
75
 
68
76
  <!-- generated:skills-begin -->
package/install.js CHANGED
@@ -37,6 +37,7 @@ const VRUUM_ROOT = path.join(os.homedir(), '.vruum');
37
37
  const VRUUM_SKILLS = path.join(VRUUM_ROOT, 'skills');
38
38
  const VRUUM_AGENTS = path.join(VRUUM_ROOT, 'agents');
39
39
  const VRUUM_BIN = path.join(VRUUM_ROOT, 'bin');
40
+ const VRUUM_OPERATOR_ROOT = path.join(VRUUM_ROOT, 'skills-operator');
40
41
  const VRUUM_VERSION_FILE = path.join(VRUUM_ROOT, 'VERSION');
41
42
  const PKG_BIN = path.join(PACKAGE_ROOT, 'bin');
42
43
 
@@ -154,7 +155,7 @@ function syncVruumRoot({ dryRun }) {
154
155
  `would sync ${PKG_SKILLS} -> ${VRUUM_SKILLS} (with auto-update prelude)`,
155
156
  ];
156
157
  if (fs.existsSync(PKG_AGENTS)) rows.push(`would sync ${PKG_AGENTS} -> ${VRUUM_AGENTS}`);
157
- if (fs.existsSync(PKG_BIN)) rows.push(`would sync ${PKG_BIN} -> ${VRUUM_BIN}`);
158
+ if (fs.existsSync(PKG_BIN)) rows.push(`would merge ${PKG_BIN} -> ${VRUUM_BIN}`);
158
159
  rows.push(`would write ${VRUUM_VERSION_FILE} = ${VERSION}`);
159
160
  return rows;
160
161
  }
@@ -174,14 +175,18 @@ function syncVruumRoot({ dryRun }) {
174
175
  rows.push(`synced ${VRUUM_AGENTS}`);
175
176
  }
176
177
 
177
- // Bin scripts update-check needs to land at a stable path and be exec.
178
+ // Bin scripts share ~/.vruum/bin with the operator installer. Copy only this
179
+ // package's files; replacing the directory would delete the operator update
180
+ // checker and make that bundle permanently stale.
178
181
  if (fs.existsSync(PKG_BIN)) {
179
- fs.rmSync(VRUUM_BIN, { recursive: true, force: true });
180
- fs.cpSync(PKG_BIN, VRUUM_BIN, { recursive: true });
181
- for (const entry of fs.readdirSync(VRUUM_BIN)) {
182
- fs.chmodSync(path.join(VRUUM_BIN, entry), 0o755);
182
+ fs.mkdirSync(VRUUM_BIN, { recursive: true });
183
+ for (const entry of fs.readdirSync(PKG_BIN)) {
184
+ const src = path.join(PKG_BIN, entry);
185
+ const dst = path.join(VRUUM_BIN, entry);
186
+ fs.cpSync(src, dst, { recursive: true, force: true });
187
+ fs.chmodSync(dst, 0o755);
183
188
  }
184
- rows.push(`synced ${VRUUM_BIN}`);
189
+ rows.push(`merged ${PKG_BIN} -> ${VRUUM_BIN}`);
185
190
  }
186
191
 
187
192
  // VERSION file — read by vruum-skills-update-check as the local version.
@@ -205,6 +210,25 @@ function isOurLink(dst, linkTarget) {
205
210
  return resolved === VRUUM_SKILLS || resolved.startsWith(VRUUM_SKILLS + path.sep);
206
211
  }
207
212
 
213
+ // Operator skills are a strict superset with multi-company scoping. When both
214
+ // installers are present, operator links win for overlapping names regardless
215
+ // of install order. Public-only names still link normally.
216
+ function isOperatorLink(dst, linkTarget) {
217
+ const resolved = path.isAbsolute(linkTarget)
218
+ ? path.resolve(linkTarget)
219
+ : path.resolve(path.dirname(dst), linkTarget);
220
+ return resolved === VRUUM_OPERATOR_ROOT
221
+ || resolved.startsWith(VRUUM_OPERATOR_ROOT + path.sep);
222
+ }
223
+
224
+ function hasSkillFile(dir) {
225
+ try {
226
+ return fs.statSync(path.join(dir, 'SKILL.md')).isFile();
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
208
232
  // Prune pass — after relinking current skills, remove links for skills no
209
233
  // longer in the package (renamed or deleted), but ONLY symlinks this installer
210
234
  // owns (target under ~/.vruum/skills/). Non-symlinks (real user dirs/files) and
@@ -276,6 +300,22 @@ function linkSkill({ name, srcAbs, target, dryRun }) {
276
300
  if (existing?.kind === 'symlink' && existing.target === srcAbs) {
277
301
  return { name, target, action: 'already-linked' };
278
302
  }
303
+ const resolvedExistingTarget = existing?.kind === 'symlink'
304
+ ? (path.isAbsolute(existing.target)
305
+ ? path.resolve(existing.target)
306
+ : path.resolve(path.dirname(dst), existing.target))
307
+ : null;
308
+ const operatorSkillIsUsable = resolvedExistingTarget
309
+ && isOperatorLink(dst, existing.target)
310
+ && hasSkillFile(resolvedExistingTarget);
311
+ if (operatorSkillIsUsable) {
312
+ return {
313
+ name,
314
+ target,
315
+ action: 'operator-kept',
316
+ reason: 'operator bundle takes precedence for overlapping skills',
317
+ };
318
+ }
279
319
  if (existing && existing.kind !== 'symlink') {
280
320
  return {
281
321
  name,
@@ -441,7 +481,7 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
441
481
  // Only clean up our own subdirectories — ~/.vruum/ is a shared state dir
442
482
  // (e.g. the .agents/ vruum-update-check keeps config.yaml + snooze state
443
483
  // there, and both installers share that config).
444
- for (const dir of [VRUUM_SKILLS, VRUUM_AGENTS, VRUUM_BIN]) {
484
+ for (const dir of [VRUUM_SKILLS, VRUUM_AGENTS]) {
445
485
  if (!fs.existsSync(dir)) continue;
446
486
  if (dryRun) {
447
487
  console.log(`${prefix}would remove ${dir}`);
@@ -450,6 +490,23 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
450
490
  console.log(`${prefix}removed ${dir}`);
451
491
  }
452
492
  }
493
+ // Remove only binaries shipped by the public package. The operator update
494
+ // checker in the same directory belongs to the operator installer.
495
+ if (fs.existsSync(PKG_BIN) && fs.existsSync(VRUUM_BIN)) {
496
+ for (const entry of fs.readdirSync(PKG_BIN)) {
497
+ const dst = path.join(VRUUM_BIN, entry);
498
+ if (!fs.existsSync(dst)) continue;
499
+ if (dryRun) {
500
+ console.log(`${prefix}would remove ${dst}`);
501
+ } else {
502
+ fs.rmSync(dst, { recursive: true, force: true });
503
+ console.log(`${prefix}removed ${dst}`);
504
+ }
505
+ }
506
+ if (!dryRun && fs.readdirSync(VRUUM_BIN).length === 0) {
507
+ fs.rmdirSync(VRUUM_BIN);
508
+ }
509
+ }
453
510
  if (fs.existsSync(VRUUM_VERSION_FILE)) {
454
511
  if (dryRun) {
455
512
  console.log(`${prefix}would remove ${VRUUM_VERSION_FILE}`);
@@ -543,6 +600,7 @@ module.exports = {
543
600
  listAvailableSkills,
544
601
  linkSkill,
545
602
  isOurLink,
603
+ isOperatorLink,
546
604
  pruneStaleLinks,
547
605
  pruneLegacyCodexLinks,
548
606
  commandInstall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vruum/skills",
3
- "version": "0.6.36",
3
+ "version": "0.6.38",
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": "9271a000625e137db6e7f5bb1daf4663ec97de8f38eac6839f05db100f864021"
45
+ "contentHash": "8f29f37c388adede78f8ddc54b535374ceb48a4520bd4cf06616c3acfac81e97"
46
46
  }
@@ -36,8 +36,9 @@ Iterate with the seller until the cohort is right ("too broad — only the US on
36
36
 
37
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
38
 
39
- - Build `source_policy` using `.agents/skills/pipeline-fill/contracts/source-policy.schema.json`.
39
+ - Build `source_policy` using `pipeline-fill/contracts/source-policy.schema.json` from the installed skills bundle.
40
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
+ - CSV and Sales Nav remain fully supported sources when the seller explicitly selects them. Source prohibitions are scoped to this run only; never turn one seller's preference into a global capability restriction or silently substitute a prohibited source.
41
42
  - 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
43
  - Pass the named organizations and campaign criteria as the discovery ICP brief.
43
44
  - Set pipeline-fill `mode: "save"` explicitly. This persists approved people so their IDs can return here, but cannot enroll them or start outreach.
@@ -62,12 +63,28 @@ For large cohorts, assign in bounded batches and report requested vs updated cou
62
63
  Show the seller a launch summary before anything sends:
63
64
  - Campaign name, source of messaging (cloned from X / fresh)
64
65
  - Cohort size and criteria
65
- - Channels and cadence (from the campaign config)
66
+ - Channels, cadence, and maximum touches (from the campaign config). Enrollment schedules the first action immediately; spacing between later touches must already be represented in the campaign cadence.
66
67
 
67
- Then ask explicitly: "Launch outreach to these N people?" Only after a clear yes, call `manage_outreach` action=start id=[person uuids] (native bulk; payload optional {max_touches, allowed_channels, start_immediately}).
68
+ Then ask explicitly: "Launch outreach to these N people?" Only after a clear yes, call `manage_outreach` action=start id=[person uuids] (native bulk; payload optional `{max_touches, allowed_channels}`). Do not pass `start_immediately`; the MCP intentionally ignores it.
68
69
 
69
70
  If the seller wants a dry run, stop after Step 4 — the campaign exists with members and nothing sends until plans start.
70
71
 
72
+ ### Existing Gmail scheduling
73
+
74
+ If Gmail already contains scheduled or sent campaign emails, reconcile them before approving, drafting, or starting replacement email touches:
75
+
76
+ 1. Call `fetch` with `type=settings subtype=channel_status`. Select the intended sender mailbox from `channels.email.accounts[]` and use its public `id` as `account_id`; never invent or ask the seller for an internal provider id.
77
+ 2. Call `manage_messages` action=`reconcile_external_email`, id=<campaign uuid>, payload=`{account_id, action: "preview", after?, before?}`.
78
+ 3. Show exact matched, ambiguous, and unmatched counts. A preview is read-only.
79
+ 4. When the seller asked to synchronize—or explicitly approves the preview—apply that exact snapshot with payload=`{account_id, action: "apply", preview_id}`. Applying creates/finalizes Vruum reservations; it never sends or resends email.
80
+ 5. Pull payload=`{account_id, action: "exceptions"}` for the exception-first rescue queue. Never guess a recipient or silently release a reservation.
81
+
82
+ Provider-scheduled rows are protected from duplicate dispatch and excluded from actionable review. Surface `externally_scheduled_count` when verifying the campaign.
83
+
84
+ ### Repairing already-created plans
85
+
86
+ If the seller changes maximum touches or allowed channels after plans exist, update the cohort through `manage_outreach` action=`update`, id=[plan uuids], payload=`{max_touches?, allowed_channels?}`. This preserves each plan's active/paused state. Report per-plan success, error, and not-attempted counts; never patch outreach-plan rows directly in the database.
87
+
71
88
  ## Notes
72
89
 
73
90
  - 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.
@@ -101,9 +101,12 @@ After presenting results, the user can request actions. Execute them using MCP t
101
101
  - **Close deal** → `manage_deal` action=won or action=lost (payload carries win_factors / loss_reason)
102
102
  - **Reopen deal** → `manage_deal` action=reopen with payload={stage}
103
103
  - **Mark stalled** → `manage_deal` action=stalled (records the stalled outcome; payload optional)
104
+ - **Update account state** → `manage_account` action=state with id=<company_id> and payload={account_stage?, health_score?, arr_current?, arr_potential?, renewal_at?, notes?}. Do this whenever the review surfaced account-level facts: a stage that no longer matches reality (the backfilled stages have never been updated), a health read from the conversation, or ARR/renewal numbers the seller confirmed. The account row is what the impact scoreboard and deal_360 read — a stale stage there misleads every later review.
104
105
 
105
106
  For batch actions ("advance all deals in proposal"), confirm with the user before executing.
106
107
 
108
+ **Account hygiene (every run):** for each reviewed deal's account, compare `account_state.account_stage` against what the deal review just showed (an `engaged`-stage account with a closed-won deal, or a `prospect` account with an active opportunity, is stale). Propose the corrected stage in the Step 3 summary and write it via `manage_account` action=state on approval. This is the write half of the accounts loop — the read half (scoreboard, deal_360) only works if reviews maintain it.
109
+
107
110
  ## Error handling
108
111
 
109
112
  - If a subagent fails (LLM rate limit, timeout, tool error): present results for successful subagents, note failures
@@ -93,7 +93,8 @@ For each approved transcript:
93
93
  Source: <transcript filename> (Google Drive)
94
94
  ```
95
95
  The `[vruum-meeting:<doc_id>]` marker is what makes re-runs idempotent (Step 5 scans for it). It **must be the very first thing in the summary** — `get_person_360` truncates the activity description to ~200 chars, so a marker placed at the end is cut off and the dedup scan silently fails (re-runs would create duplicate meetings). Keep it verbatim, at the front.
96
- 2. **Create each approved task** — `manage_tasks` action=create with:
96
+ 2. **Record the impact event** — call `manage_account` with action=record_impact, id=<the person's company_id> (from `get_person_360`), and payload={practice: "meeting", event_type: "meeting_held", person_id, summary: <the 1–2 sentence recap, WITHOUT the marker>}. This stamps the account's `first/last_impact_at` and feeds the impact scoreboard — a held meeting is exactly the "value delivered" moment that table exists to record. Skip silently if the person has no linked company.
97
+ 3. **Create each approved task** — `manage_tasks` action=create with:
97
98
  - `title` (the action item), `person_id` (+ `deal_id` if there is one)
98
99
  - `priority`, and `due_at` as ISO-8601 **only if** a date was actually parseable (omit otherwise)
99
100
  - `assigned_to` = the rep running this (leave to self; only assign a teammate if you know their Vruum user id)
@@ -28,9 +28,16 @@ For small queues (5 or fewer) or when subagents can't access MCP, review directl
28
28
 
29
29
  ### Step 1: Get the lay of the land
30
30
 
31
- Call `fetch` with type=stats and subtype=outreach to see the pending queue shape. The response carries `needs_draft_count` (unauthored touches awaiting authoring) alongside `draft_count` (authored, awaiting approval) surface both so the authoring backlog is visible up front. Present a quick summary:
31
+ Call `fetch` with type=stats and subtype=outreach to see the pending queue shape, then call `get_outreach_review` with `content_length=preview, limit=1` for authoritative review-state counts. Surface:
32
32
 
33
- "You have N to author (needs_draft, W of them WARM accepted-connection follow-ups), X reply responses, Y pending T1s, Z T2+ follow-ups. [Any critical alerts.] Want me to run full triage or focus on a specific category?"
33
+ - `needs_draft_count`: unauthored touches awaiting authoring
34
+ - `draft_count`: authored touches awaiting approval
35
+ - `approved_pending_send_count`: approved Vruum sends waiting on cadence/cooldown
36
+ - `externally_scheduled_count`: Gmail/provider-scheduled rows protected by an external-send reservation
37
+
38
+ Externally scheduled rows are intentionally excluded from actionable items. Never re-draft, approve, or manually resend them. Present a quick summary:
39
+
40
+ "You have N to author (W WARM accepted-connection follow-ups), D drafts to review, A approved sends waiting on cadence, and E externally scheduled in Gmail. [Any critical alerts.] Want me to run full triage or focus on a specific category?"
34
41
 
35
42
  **Warm follow-ups outrank everything except replies.** The stats payload carries `needs_draft_warm_count`: unauthored `linkedin_message` touches to people who ACCEPTED the connection request. These are the highest-EV rows in the queue — a person who said yes to the invite is waiting on a first real message. When non-zero, lead the summary with it and default the triage order to: inbound replies → warm follow-ups → everything else.
36
43
 
@@ -38,6 +45,19 @@ Keep it short. The user knows their queue — they just need the numbers to deci
38
45
 
39
46
  **needs_draft items EXPIRE.** A nightly backend sweep (03:20 UTC) rejects any `needs_draft` row older than 14 days from creation — intended garbage collection, not an operator action. An expired touch is not lost forever (the plan reschedules and mints a fresh row about a week later), but the authoring work is deferred a cycle and the queue silently shrinks. The tools tell you: the stats payload carries `needs_draft_expiring_soon_count` (rows within 3 days of the sweep — surface it in the summary when non-zero), and each needs_draft item carries `expires_at` (its sweep deadline). Consequence for triage: author oldest-first, and if the queue is too big to clear in one session, clear the items closest to `expires_at` rather than sampling the freshest.
40
47
 
48
+ ### Gmail/Vruum reconciliation lane
49
+
50
+ When the seller asks to audit or synchronize Gmail scheduling, do this before normal triage:
51
+
52
+ 1. Call `fetch` with `type=settings subtype=channel_status`. Select the intended sender mailbox from `channels.email.accounts[]` and use its public `id` as `account_id`; never invent or ask the seller for an internal provider id.
53
+ 2. Call `manage_messages` action=`reconcile_external_email`, id=<campaign uuid>, payload=`{account_id, action: "preview", after?, before?}`.
54
+ 3. Present matched, ambiguous, and unmatched counts. Preview is provider-read-only and mutation-free.
55
+ 4. If synchronization was requested or the seller approves the preview, call the same action with payload=`{account_id, action: "apply", preview_id}`. Apply only the exact preview; it creates/finalizes reservations without sending or resending anything.
56
+ 5. Call payload=`{account_id, action: "exceptions"}` and lead with blocked, ambiguous, unmatched, failed, or unverified contacts.
57
+ 6. Re-read `get_outreach_review` and verify `externally_scheduled_count` plus the actionable queue. A Gmail-scheduled row must never remain actionable.
58
+
59
+ Manual Gmail edits require a fresh preview. Use guarded `hold`, `release`, or `mark_cancelled` only with the reservation id, a reason, and affirmative cancellation evidence or explicit human instruction. Reauthorization must preserve the provider identity and existing reservation; never create a replacement touch merely because the connector was refreshed.
60
+
41
61
  ### Step 2: Build the dispatch list and categorize
42
62
 
43
63
  Once the user says go (or picks a focus area), pull the lightweight message queue via `search` with type=messages, `fields=compact` and limit=100 — make THREE cheap calls: `warm_only=true, status=needs_draft, sort_by=expiring` (the WARM authoring lane — LinkedIn follow-ups to accepted connections, nearest deadline first), `status=needs_draft` (the full authoring lane; warm rows appear here too — dedupe by message_id, warm lane wins), and `status=draft` (the review lane). `fields=compact` returns message_id, person_name, category, sequence_number, channel, status, match_score, touches_completed, campaign_id, first_content_touch, and channel_rewrite_reason WITHOUT message content — very cheap on tokens. Tag each item with its status so dispatch routes it to the right mode: `needs_draft` → authoring, `draft` → review. (Omitting the status filter returns the default actionable set — needs_draft + draft + approved — but pull the two lanes explicitly so already-approved messages awaiting send don't enter triage.)
@@ -202,4 +222,5 @@ After outreach messages are processed, ask if the user wants to review the engag
202
222
  - **User wants to review a specific person:** pull that person's conversation with `fetch` (type=conversation) and review directly. No batch workflow.
203
223
  - **Subagent can't reach MCP tools:** fall back to inline review.
204
224
  - **Homogeneous T1 pattern:** if the first T1 batch all had the identical issue, fix the remaining in bulk with a single `manage_messages` call passing an id array (same action applied to every id, max 50 per call). Confirm first.
225
+ - **Campaign plan settings drifted:** repair `max_touches` or `allowed_channels` through `manage_outreach` action=`update` with plan-id arrays. This preserves execution state; never update plan rows directly.
205
226
 
@@ -90,6 +90,8 @@ ETA estimates: ~2s for batch Step 3 dedup + ~30s/wave Phase A + ~60s/wave Phase
90
90
 
91
91
  > Sourcing {campaign_name} via discovery (ICP-based, long-tail). Reply `sales-nav`, `yc`, `csv`, or `picker` to switch.
92
92
 
93
+ CSV and Sales Nav are fully supported when selected. `source_policy` is a per-run routing contract: an explicit "use CSV" or "use Sales Nav" selects that capability; an explicit "no CSV" or "no Sales Nav" prohibits it only for this run. Never persist a seller's personal source preference as a tenant-wide capability restriction.
94
+
93
95
  Why discovery is the default: keyword/Sales-Nav sources keep returning the same marquee names, which collide with already-enrolled prospects as a campaign matures — the Step 3 dedup then throws most of the batch away. Discovery anchors on the campaign's own ICP and reaches the long tail, deduping *before* research instead of after. Only render the full picker below when the operator asks to choose (`picker`), names a non-discovery source, or the discovery handler can't proceed.
94
96
 
95
97
  Per selected campaign, when the operator wants to choose the source explicitly, prompt:
@@ -33,6 +33,7 @@ Build "your revenue engine today" from live reads — never from memory or assum
33
33
  - `search` type=people limit=1 filters={research_status: "all"} → total contacts (read the total, not the rows)
34
34
  - `search` type=deals limit=5 → deal pipeline existence
35
35
  - `fetch` type=stats subtype=outreach → sends, replies, meetings
36
+ - `get_outreach_review` content_length=preview limit=1 → actionable, approved-pending-send, and externally scheduled counts
36
37
  - `search` type=content → whether an organic content motion is active
37
38
 
38
39
  Present a compact snapshot (5-8 lines, their numbers), positioned on the revenue-motion map (Step 2). If the company record or knowledge base shows a referral source ("referred by X"), acknowledge it and skip intake questions that referral context already answers.
@@ -64,6 +65,7 @@ Important boundaries: outreach/reply/content/comment prose is authored in the ha
64
65
  | Signal | Recommendation |
65
66
  |---|---|
66
67
  | Contacts sitting unenrolled | enroll into a campaign (`/campaign-builder` or `manage_campaign` action=members) |
68
+ | Gmail/provider-scheduled rows or reconciliation exceptions | `/outreach-triage` Gmail/Vruum reconciliation lane; never recreate the email |
67
69
  | Outreach queue has pending drafts | `/outreach-triage` |
68
70
  | Engagement queue non-empty | `/engagement-triage` |
69
71
  | Replies without follow-up | `/diagnose-reply` on the interesting ones, then respond |