@vruum/skills 0.6.36 → 0.6.37
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/README.md +8 -0
- package/install.js +66 -8
- package/package.json +2 -2
- package/skills/campaign-builder/SKILL.md +20 -3
- package/skills/outreach-triage/SKILL.md +23 -2
- package/skills/pipeline-fill/SKILL.md +2 -0
- package/skills/vruum-guide/SKILL.md +2 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vruum",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.37",
|
|
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.37",
|
|
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
|
|
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
|
|
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.
|
|
180
|
-
fs.
|
|
181
|
-
|
|
182
|
-
|
|
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(`
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.6.37",
|
|
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": "9562cd3662884c9fb44cb61950299f9569a13ce38dc64101dc196b36e26d1c43"
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
@@ -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
|
|
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
|
-
|
|
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 |
|