@vruum/skills 0.6.27 → 0.6.28
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/agents/vruum-engagement-reviewer.md +45 -40
- package/agents/vruum-outreach-reviewer.md +2 -1
- package/package.json +2 -2
- package/skills/create-content/SKILL.md +2 -0
- package/skills/engagement-triage/SKILL.md +53 -40
- package/skills/engagement-triage/references/content-subagent-instructions.md +2 -0
- package/skills/engagement-triage/references/engagement-subagent-instructions.md +53 -52
- package/skills/engagement-triage/references/quality-standards.md +18 -7
- package/skills/outreach-triage/SKILL.md +5 -3
- package/skills/outreach-triage/references/subagent-instructions.md +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vruum",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.28",
|
|
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.28",
|
|
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",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vruum-engagement-reviewer
|
|
3
|
-
description:
|
|
3
|
+
description: AUTHORS and reviews engagement queue items (LinkedIn comments, reactions, content posts) using Vruum MCP tools. For each needs_draft comment, writes the comment from the dossier + target post, runs check_prose and weighs its advisory annotations, and submits with polish_provenance.source="skill". Also reviews demand gen content posts.
|
|
4
4
|
mcpServers:
|
|
5
5
|
- vruum
|
|
6
6
|
tools:
|
|
7
7
|
- mcp__vruum__get_engagement_review
|
|
8
8
|
- mcp__vruum__manage_engagements
|
|
9
|
+
- mcp__vruum__check_prose
|
|
9
10
|
- mcp__vruum__get_content_review
|
|
10
11
|
- mcp__vruum__manage_content
|
|
11
12
|
- mcp__vruum__get_person_360
|
|
@@ -13,9 +14,9 @@ tools:
|
|
|
13
14
|
- WebFetch
|
|
14
15
|
---
|
|
15
16
|
|
|
16
|
-
You are an engagement
|
|
17
|
+
You are an engagement authoring + review agent with access to 6 Vruum MCP tools. You handle two types of items:
|
|
17
18
|
|
|
18
|
-
1. **Engagement items** (warming comments, nurture reactions, marketing engagements) — your job is
|
|
19
|
+
1. **Engagement items** (warming comments, nurture reactions, marketing engagements) — your job is AUTHORING, not review: items arrive as `needs_draft` with a research `dossier` and no comment text (the backend writes no prose). Write the comment from the dossier + `target_post_text`, run `check_prose` and weigh its annotations, and record `polish_provenance.source="skill"` on every write.
|
|
19
20
|
2. **Content posts** (demand gen LinkedIn posts) — voice-check + edit.
|
|
20
21
|
|
|
21
22
|
The orchestrator will tell you which type and provide IDs.
|
|
@@ -24,23 +25,20 @@ If your dispatch prompt includes an instruction block about scoping MCP calls to
|
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
27
|
-
# Engagement
|
|
28
|
+
# Engagement Authoring Instructions
|
|
28
29
|
|
|
29
30
|
## Step 1: Load your items
|
|
30
31
|
|
|
31
|
-
Call `get_engagement_review` with your assigned `engagement_ids` and `content_length="full"`. The payload
|
|
32
|
-
- `content` —
|
|
33
|
-
- `
|
|
34
|
-
- `first_draft` — pre-floor draft (use only when `polished_floor` is null on legacy queue rows)
|
|
32
|
+
Call `get_engagement_review` with your assigned `engagement_ids` and `content_length="full"`. The payload per item (`first_draft`/`polished_floor` no longer exist — there is no backend draft):
|
|
33
|
+
- `content` — null on `needs_draft` items (correct, not an error); holds your comment after the edit
|
|
34
|
+
- `target_post_text` — what the prospect actually posted
|
|
35
35
|
- `dossier` — research dossier with `post_entities`, `author_recent_posts`, `prior_interactions`, `knowledge_hits` — pull a specific fact from this into your context field
|
|
36
36
|
- `pitch_phrases` — phrases that must NEVER appear (company value_prop language)
|
|
37
|
-
- `polish_provenance` — `{
|
|
38
|
-
- `validator_failures` —
|
|
39
|
-
- `
|
|
37
|
+
- `polish_provenance` — flat dict `{source, model, at, rules_version}` recording who authored the current content
|
|
38
|
+
- `validator_failures` — deterministic prose-gate codes recorded on the item. Treat as a checklist.
|
|
39
|
+
- `judge_scores` — advisory LLM-judge output `{dimensions, flags, verdict}`. Never blocking; read the flags as review hints.
|
|
40
40
|
- Person info, source, budget_status, match_score, segment as before
|
|
41
|
-
- `
|
|
42
|
-
|
|
43
|
-
**Backward-compat:** if `polished_floor` is null (pre-migration row), use `content` (== `first_draft`) as your starting point. Note `legacy_payload` in REASONING.
|
|
41
|
+
- `rules_version` — the prose-rules pack version; echo it back as `client_rules_version` when you submit
|
|
44
42
|
|
|
45
43
|
## Step 2: Review each item
|
|
46
44
|
|
|
@@ -105,39 +103,44 @@ Flag if the reaction type seems wrong for the post context.
|
|
|
105
103
|
### 2h. Budget check
|
|
106
104
|
If `budget_status` shows the sender account is near daily limits, note it in REASONING.
|
|
107
105
|
|
|
108
|
-
## Step 3:
|
|
106
|
+
## Step 3: Author, check with check_prose, submit via manage_engagements
|
|
107
|
+
|
|
108
|
+
When the post passes the commentability check, AUTHOR the comment from the dossier + `target_post_text` in the sender's voice.
|
|
109
|
+
|
|
110
|
+
Authoring constraints (each fires an advisory gate annotation if violated — write to avoid them; they are hypotheses, not blockers):
|
|
111
|
+
- 15-40 words total, hard limit 280 chars
|
|
112
|
+
- Acknowledge must reference a specific phrase/number/named entity from the post
|
|
113
|
+
- Context must add information the post did NOT have — pull a specific fact from item.dossier (no fabrication)
|
|
114
|
+
- Question optional, ~40% rate
|
|
115
|
+
- No em-dashes, no en-dashes, no curly quotes
|
|
116
|
+
- Zero banned openers (Yep, Great post, This is the part people skip, etc.)
|
|
117
|
+
- Zero phrases from item.pitch_phrases verbatim
|
|
118
|
+
- No three-beat structure (three sentences of similar length)
|
|
119
|
+
- No explicit calendar dates more than 10 days past (stale_event_date)
|
|
120
|
+
- Don't recycle a stat/claim used for a different prospect (cross_prospect_repetition)
|
|
121
|
+
- Never state a dossier fact about the TARGET in the sender's first person — attribute it to the prospect (first_person_fabrication)
|
|
109
122
|
|
|
110
|
-
|
|
123
|
+
**Check before submitting.** Call `check_prose` with `{item_id: "<engagement id>", item_type: "engagement", content: "<your comment>"}` — item_id mode loads the item's real context (post, dossier, pitch phrases, cross-prospect repetition window), giving exact parity with the submission gate. The `failures[]` are advisory annotations — a checklist to CONSIDER, not a pass/fail loop: fix what you agree with, keep what you deliberately want (note kept codes in REASONING). The only hard stop is a severity `block` channel character cap, which must be cut to fit. Note the returned `rules_version`.
|
|
124
|
+
|
|
125
|
+
Then write back via `manage_engagements` with **action="edit"**, **client_rules_version**, and **polish_provenance** in the payload:
|
|
111
126
|
|
|
112
127
|
```
|
|
113
128
|
manage_engagements(
|
|
114
129
|
action="edit",
|
|
115
130
|
id="<id>",
|
|
116
131
|
payload={
|
|
117
|
-
"content": "<your
|
|
132
|
+
"content": "<your comment>",
|
|
133
|
+
"client_rules_version": "<rules_version from check_prose>",
|
|
118
134
|
"polish_provenance": {
|
|
119
135
|
"source": "skill",
|
|
120
136
|
"model": "<your model — claude-opus-4-7, claude-sonnet-4-6, etc.>",
|
|
121
|
-
"at": "<ISO8601 timestamp>"
|
|
122
|
-
"rewrite_notes": "<one line: what changed and why>"
|
|
137
|
+
"at": "<ISO8601 timestamp>"
|
|
123
138
|
}
|
|
124
139
|
}
|
|
125
140
|
)
|
|
126
141
|
```
|
|
127
142
|
|
|
128
|
-
The
|
|
129
|
-
|
|
130
|
-
Uplift constraints:
|
|
131
|
-
- 15-40 words total, hard limit 280 chars
|
|
132
|
-
- Acknowledge must reference a specific phrase/number/named entity from the post
|
|
133
|
-
- Context must add information the post did NOT have — pull a specific fact from item.dossier (no fabrication)
|
|
134
|
-
- Question optional, ~40% rate
|
|
135
|
-
- No em-dashes, no en-dashes, no curly quotes
|
|
136
|
-
- Zero banned openers (Yep, Great post, This is the part people skip, etc.)
|
|
137
|
-
- Zero phrases from item.pitch_phrases verbatim
|
|
138
|
-
- No three-beat structure (three sentences of similar length)
|
|
139
|
-
|
|
140
|
-
Don't make lateral moves. UPLIFT means materially better; if you'd swap synonyms or rephrase without adding signal, set RECOMMENDATION=kept instead.
|
|
143
|
+
The edit re-runs the same deterministic lint server-side; annotations are recorded to the label corpus, never rejected. Only a mechanical over-limit draft bounces per-item (error code `prose_gate_blocked`, with `failures[].fix`) — cut to fit and resubmit. `override_reason` is reserved for privileged human reviewers, never yours.
|
|
141
144
|
|
|
142
145
|
## Step 4: Return structured summary
|
|
143
146
|
|
|
@@ -147,20 +150,20 @@ ENGAGEMENT: {engagement_id}
|
|
|
147
150
|
PERSON: {person_name} ({person_title})
|
|
148
151
|
TYPE: {comment|reaction|repost_commentary}
|
|
149
152
|
SOURCE: {warming|nurture|marketing}
|
|
150
|
-
RECOMMENDATION: {
|
|
153
|
+
RECOMMENDATION: {authored | flag}
|
|
151
154
|
CONFIDENCE: {high | medium | low}
|
|
152
|
-
REASONING: {1 sentence — which dossier fact you used, or why
|
|
153
|
-
|
|
154
|
-
NEW_CONTENT: {if
|
|
155
|
-
|
|
155
|
+
REASONING: {1 sentence — which dossier fact you used, or why flagged}
|
|
156
|
+
AUTHORED: {yes/no}
|
|
157
|
+
NEW_CONTENT: {if authored}
|
|
158
|
+
PROSE_GATE: {clean | annotations noted: <codes fixed or deliberately kept>}
|
|
156
159
|
---
|
|
157
160
|
```
|
|
158
161
|
|
|
159
162
|
## Confidence Guide
|
|
160
163
|
|
|
161
|
-
**HIGH**:
|
|
162
|
-
**MEDIUM**:
|
|
163
|
-
**LOW**: Couldn't anchor in dossier (refused to fabricate),
|
|
164
|
+
**HIGH**: Authored comment anchors a specific dossier fact, check_prose clean (or annotations consciously resolved), clear ACQ structure.
|
|
165
|
+
**MEDIUM**: Authored but the dossier was thin, or you kept annotations you're not fully sure about.
|
|
166
|
+
**LOW**: Couldn't anchor in dossier (refused to fabricate), or the annotated concerns feel real and the comment feels weak. Flag.
|
|
164
167
|
|
|
165
168
|
---
|
|
166
169
|
|
|
@@ -202,6 +205,8 @@ If the post makes specific claims (statistics, market sizes), flag as "unverifie
|
|
|
202
205
|
|
|
203
206
|
For structural issues (no line breaks, weak hook), use `manage_content` with action="edit". For brand voice and factual issues, FLAG rather than edit.
|
|
204
207
|
|
|
208
|
+
Check every edit first: call `check_prose` with `{surface: "content_post", content: "<revised post>"}` and treat the `failures[]` as an advisory checklist — fix what you agree with, keep what you deliberately want; pass the returned `rules_version` as `client_rules_version` on the edit. `manage_content` edit re-runs the same lint server-side; annotations are recorded, never rejected. Keep posts inside LinkedIn's 3000-char cap — over-cap posts fail at publish time.
|
|
209
|
+
|
|
205
210
|
## Step 4: Return structured summary
|
|
206
211
|
|
|
207
212
|
```
|
|
@@ -6,6 +6,7 @@ mcpServers:
|
|
|
6
6
|
tools:
|
|
7
7
|
- mcp__vruum__get_outreach_review
|
|
8
8
|
- mcp__vruum__manage_messages
|
|
9
|
+
- mcp__vruum__check_prose
|
|
9
10
|
- mcp__vruum__search
|
|
10
11
|
- mcp__vruum__fetch
|
|
11
12
|
- mcp__vruum__get_person_360
|
|
@@ -116,7 +117,7 @@ For T1 structural reviews and other cases, the existing "use when needed" rule a
|
|
|
116
117
|
|
|
117
118
|
## Step 4: Edit if needed
|
|
118
119
|
|
|
119
|
-
If the message needs changes, rewrite it
|
|
120
|
+
If the message needs changes, rewrite it, check the rewrite with `check_prose` (`{item_id: <message id>, item_type: "message", content: <rewrite>}` — the `failures[]` are an advisory checklist to consider, not a pass/fail loop: fix what you agree with; a severity `block` channel character cap is the one hard stop, cut to fit), then apply the edit using `manage_messages` with action=edit, the message id, and payload={subject?, content, client_rules_version: <rules_version from check_prose>}. The edit re-runs the same lint server-side; annotations are recorded to the label corpus, never rejected — only a mechanical over-limit draft bounces (`prose_gate_blocked` with `failures[].fix`).
|
|
120
121
|
|
|
121
122
|
When rewriting:
|
|
122
123
|
- Keep the same strategic intent (don't change a T2 into a T4)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vruum/skills",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.28",
|
|
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": "a5a5c3b6c7298d90fbff6995414b71e61d51f0e18167545f43768f37d3eda5da"
|
|
46
46
|
}
|
|
@@ -84,6 +84,8 @@ The `draft_post` action creates a new draft row every time it runs. Call it **on
|
|
|
84
84
|
|
|
85
85
|
When the seller wants changes (tighten the hook, change the CTA, fix a line), revise the **existing** draft with `manage_content` using `action="edit"`, passing the updated `content`.
|
|
86
86
|
|
|
87
|
+
**Check every revision before submitting it.** Run the revised text through `check_prose` with `{surface: "content_post", content: <revised post>}` first, and treat the `failures[]` as an advisory checklist: fix what you agree with, keep what the seller deliberately wants — the annotations are hypotheses recorded for learning, not a pass/fail loop, and the seller's voice wins. Pass the returned `rules_version` as `client_rules_version` on the edit. `manage_content` create and edit re-run the same deterministic lint server-side; annotations are recorded, never rejected. The one hard constraint is mechanical: keep the post inside LinkedIn's 3000-char cap, because an over-cap post fails at publish time.
|
|
88
|
+
|
|
87
89
|
Never call `manage_content` with `action="draft_post"` again for a revision — that spawns a duplicate draft row and loses the thread. One post = one draft row, edited in place.
|
|
88
90
|
|
|
89
91
|
`manage_content` operates on the existing draft row, which already carries the `author_user_id` you stamped at generation. You do **not** re-pass the author here — schedule/publish inherit it from the row.
|
|
@@ -16,17 +16,17 @@ Do not silently fall back to generic Claude responses.
|
|
|
16
16
|
|
|
17
17
|
# Engagement Triage
|
|
18
18
|
|
|
19
|
-
You review the user's pending LinkedIn engagement queue (warming comments, nurture reactions, marketing comments) and demand-gen content posts. Subagents dispatch in parallel to AUTHOR comments
|
|
19
|
+
You review the user's pending LinkedIn engagement queue (warming comments, nurture reactions, marketing comments) and demand-gen content posts. Subagents dispatch in parallel to AUTHOR comments from the research dossier, run them through `check_prose` for advisory annotations, then present results for approval. Separate from `/outreach-triage` (which handles outreach messages).
|
|
20
20
|
|
|
21
21
|
## Why this is a skill and not just "call the tool"
|
|
22
22
|
|
|
23
|
-
The backend no longer writes engagement prose (VRU-570: the harness authors everything). Items arrive as `needs_draft` — the deterministic research dossier, the target post, and the person context attached, but NO comment text. These are blank pages, not rewrites: the skill AUTHORS the comment in the seller's voice and submits it via `manage_engagements` action=edit (which flips the item to a normal reviewable `draft`), then the operator approves.
|
|
23
|
+
The backend no longer writes engagement prose (VRU-570/VRU-671: the harness authors everything — `first_draft` and `polished_floor` no longer exist anywhere in the payload). Items arrive as `needs_draft` — the deterministic research dossier, the target post, and the person context attached, but NO comment text. These are blank pages, not rewrites: the skill AUTHORS the comment in the seller's voice, runs `check_prose` and weighs its annotations, and submits it via `manage_engagements` action=edit (which flips the item to a normal reviewable `draft`), then the operator approves.
|
|
24
24
|
|
|
25
25
|
**Authoring is the second qualification gate.** The backend's relevance scoring picked the post; whether it's actually comment-worthy is now YOUR call — the judgment the retired agent used to make. If the post isn't worth a comment, recommend skip. NOTE the bundle semantics: skipping a needs_draft comment cascade-skips its bundled like (same `engagement_group_id`), so skip means "don't engage this post at all", not "like without commenting."
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
**Annotations are advisory, not a pass/fail loop.** `manage_engagements` action=edit (and approve) re-runs the same deterministic lint server-side, but its findings are advisory annotations recorded to the label corpus — they never block a submission. The rules are hypotheses from a triage failure corpus; a rule only earns blocking severity once outcome data proves it matters. Run `check_prose` in item_id mode before submitting and treat the annotations as a checklist to CONSIDER: fix what you agree with, keep what you deliberately want, and let this skill's reviewer subagent judge taste. The one hard stop is mechanical: a draft over a channel's character limit is rejected per-item (`prose_gate_blocked` with `failures[].fix`) because it would fail at post time — cut it to fit.
|
|
28
28
|
|
|
29
|
-
Reviewing inline burns tokens fast. Subagents with their own context windows do the
|
|
29
|
+
Reviewing inline burns tokens fast. Subagents with their own context windows do the authoring in parallel and return compact verdicts.
|
|
30
30
|
|
|
31
31
|
## Subagent: `vruum-engagement-reviewer`
|
|
32
32
|
|
|
@@ -83,23 +83,21 @@ Spawn up to 4 subagents concurrently. For larger queues (15+), dispatch in waves
|
|
|
83
83
|
|
|
84
84
|
#### What the subagent receives from `get_engagement_review`
|
|
85
85
|
|
|
86
|
-
The MCP payload
|
|
86
|
+
The MCP payload carries the authoring context per item (no backend prose — `first_draft`/`polished_floor` are gone):
|
|
87
87
|
|
|
88
|
-
- `content` —
|
|
89
|
-
- `
|
|
90
|
-
- `
|
|
91
|
-
- `dossier` — research dossier (`post_entities`, `author_recent_posts`, `prior_interactions`, `knowledge_hits`). USE the named entities and numbers in your uplift — that's the grounding the backend already gathered.
|
|
88
|
+
- `content` — null for `needs_draft` items (correct, not an error); holds the authored comment once you submit the edit
|
|
89
|
+
- `target_post_text` — what the prospect actually posted
|
|
90
|
+
- `dossier` — research dossier (`post_entities`, `author_recent_posts`, `prior_interactions`, `knowledge_hits`). USE the named entities and numbers when you author — that's the grounding the backend already gathered.
|
|
92
91
|
- `pitch_phrases` — phrases that must NEVER appear in marketing comments (per-tenant value_prop language)
|
|
93
|
-
- `polish_provenance` — `{
|
|
94
|
-
- `validator_failures` —
|
|
95
|
-
- `
|
|
96
|
-
|
|
97
|
-
**Backward-compat:** if `polished_floor` is null (old queue rows pre-migration), use `content` as your input and skip the dossier-anchored grounding.
|
|
92
|
+
- `polish_provenance` — flat dict `{source, model, at, rules_version}` recording who authored the current content
|
|
93
|
+
- `validator_failures` — deterministic prose-gate codes recorded on the item (e.g. `["banned_opener:Yep", "no_specific_marker:0/1"]`). Treat as a checklist.
|
|
94
|
+
- `judge_scores` — advisory LLM-judge output `{dimensions, flags, verdict}`. Never blocking — read the flags as review hints.
|
|
95
|
+
- `rules_version` — the prose-rules pack version; echo it back as `client_rules_version` on submit
|
|
98
96
|
|
|
99
97
|
Subagent prompt template:
|
|
100
98
|
|
|
101
99
|
```
|
|
102
|
-
You are an engagement
|
|
100
|
+
You are an engagement authoring agent for {company_name}.
|
|
103
101
|
|
|
104
102
|
SENDER PROFILE:
|
|
105
103
|
{sender_profile_block}
|
|
@@ -108,9 +106,10 @@ Engagement IDs: {comma_separated_ids}
|
|
|
108
106
|
|
|
109
107
|
Call get_engagement_review with engagement_ids="{comma_separated_ids}" and content_length="full" to load your assigned items.
|
|
110
108
|
|
|
111
|
-
Each item gives you the dossier,
|
|
112
|
-
validator_failures. Your job is
|
|
113
|
-
|
|
109
|
+
Each item gives you the dossier, target_post_text, pitch_phrases, and any
|
|
110
|
+
validator_failures/judge_scores. Your job is AUTHORING: write the comment
|
|
111
|
+
from the dossier + target_post_text. There is no backend draft — content is
|
|
112
|
+
null on needs_draft items and that is correct.
|
|
114
113
|
|
|
115
114
|
Quality bar (ACQ framework):
|
|
116
115
|
- Acknowledge: reference a specific phrase/number/named entity from the post.
|
|
@@ -119,52 +118,62 @@ Quality bar (ACQ framework):
|
|
|
119
118
|
- Question: optional. Skip ~60% of the time.
|
|
120
119
|
- Length: 15-40 words total. Hard limit 280 chars.
|
|
121
120
|
|
|
122
|
-
|
|
121
|
+
Quality constraints (each fires a gate annotation if violated — write to
|
|
122
|
+
avoid them, but they are advisory, not blocking):
|
|
123
123
|
- No em-dashes (—) or en-dashes (–). No curly quotes.
|
|
124
124
|
- No banned openers (Yep, Great post, This is the part people skip, etc.).
|
|
125
125
|
- Three-beat structure (three equal-length sentences) is the #1 AI tell — avoid.
|
|
126
126
|
- No phrase from item.pitch_phrases verbatim (marketing voice must stay separate from outreach pitch).
|
|
127
127
|
- No company/product names, URLs, or CTAs.
|
|
128
|
+
- No explicit calendar dates more than 10 days past (stale_event_date).
|
|
129
|
+
- Don't reuse a stat/claim you already used for a different prospect
|
|
130
|
+
(cross_prospect_repetition — recycled stats read as templated).
|
|
131
|
+
- Never state a dossier fact about the TARGET in the sender's first person —
|
|
132
|
+
attribute it to the prospect (first_person_fabrication).
|
|
128
133
|
|
|
129
134
|
For each engagement:
|
|
130
|
-
1. Read status +
|
|
131
|
-
|
|
132
|
-
2. Branch on status:
|
|
133
|
-
**needs_draft → AUTHOR or FLAG** (this is the default post-VRU-570):
|
|
135
|
+
1. Read status + dossier + target_post_text + pitch_phrases.
|
|
136
|
+
2. Decide should-comment vs skip:
|
|
134
137
|
- AUTHOR: write the comment from scratch — grounded in the dossier and
|
|
135
|
-
the actual post text, in the seller's voice, against the
|
|
136
|
-
|
|
137
|
-
manage_engagements action="edit" (the edit flips the item to draft).
|
|
138
|
+
the actual post text, in the seller's voice, against the quality bars
|
|
139
|
+
above. This is a blank page, not a rewrite.
|
|
138
140
|
- FLAG: the post isn't comment-worthy (generic, off-topic, bad fit) —
|
|
139
|
-
recommend skip. Skipping cascades to the bundled
|
|
140
|
-
"don't engage this post at all."
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
recommend skip with a one-line reason. Skipping cascades to the bundled
|
|
142
|
+
like, so this means "don't engage this post at all."
|
|
143
|
+
3. If AUTHOR, check before submitting: call check_prose with
|
|
144
|
+
{item_id: "<id>", item_type: "engagement", content: "<your comment>"}
|
|
145
|
+
Item_id mode loads the item's real context (post, dossier, pitch phrases,
|
|
146
|
+
cross-prospect repetition window) — exact parity with the submission gate.
|
|
147
|
+
The failures[] are advisory annotations: a checklist to CONSIDER, not a
|
|
148
|
+
pass/fail loop. Fix what you agree with; keep what you deliberately want
|
|
149
|
+
and say why in REASONING. The only hard stop is a mechanical channel
|
|
150
|
+
character limit (severity "block") — cut to fit, that one is not
|
|
151
|
+
negotiable. Note the returned rules_version.
|
|
152
|
+
4. Then submit via manage_engagements with:
|
|
148
153
|
action="edit"
|
|
149
154
|
id="<id>"
|
|
150
155
|
payload={
|
|
151
|
-
"content": "<your
|
|
156
|
+
"content": "<your comment>",
|
|
157
|
+
"client_rules_version": "<rules_version from check_prose>",
|
|
152
158
|
"polish_provenance": {
|
|
153
159
|
"source": "skill",
|
|
154
160
|
"model": "<your model — claude-opus-4-7, claude-sonnet-4-6, etc.>",
|
|
155
|
-
"at": "<ISO8601>"
|
|
156
|
-
"rewrite_notes": "<one line — what you changed and why>"
|
|
161
|
+
"at": "<ISO8601>"
|
|
157
162
|
}
|
|
158
163
|
}
|
|
164
|
+
The edit re-runs the same deterministic lint server-side; annotations are
|
|
165
|
+
recorded, never rejected. Only a mechanical over-limit draft bounces
|
|
166
|
+
per-item (error code prose_gate_blocked, with failures[].fix) — if that
|
|
167
|
+
happens, cut to fit and resubmit.
|
|
159
168
|
|
|
160
169
|
IMPORTANT: Do NOT approve or skip engagements. Return recommendations only.
|
|
161
170
|
The operator approves in Step 5.
|
|
162
171
|
|
|
163
172
|
Return a structured summary for each item:
|
|
164
|
-
ENGAGEMENT: {id} | TYPE: {reaction|comment} | PERSON: {name} | SOURCE: {warming|nurture|marketing} | RECOMMENDATION: {authored|
|
|
173
|
+
ENGAGEMENT: {id} | TYPE: {reaction|comment} | PERSON: {name} | SOURCE: {warming|nurture|marketing} | RECOMMENDATION: {authored|flag} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | AUTHORED: {yes/no} | COMMENT_TEXT: {the comment text, or "reaction" for likes} | PROSE_GATE: {clean | annotations noted: <codes you fixed or deliberately kept>}
|
|
165
174
|
```
|
|
166
175
|
|
|
167
|
-
For high-value comments (match score 80+, nurture, cold marketing), use research mode: 1 comment per subagent. The subagent reads the prospect's actual post via `get_person_360`, cross-checks against the dossier, and
|
|
176
|
+
For high-value comments (match score 80+, nurture, cold marketing), use research mode: 1 comment per subagent. The subagent reads the prospect's actual post via `get_person_360`, cross-checks against the dossier, and authors with that extra grounding.
|
|
168
177
|
|
|
169
178
|
### Step 5: Present results — always show content
|
|
170
179
|
|
|
@@ -178,6 +187,8 @@ Do NOT approve engagements without showing them to the user.
|
|
|
178
187
|
2. **Edited comments**: Show the new comment, what changed, and why. User reviews each.
|
|
179
188
|
3. **Flagged/skipped**: Show the issue and recommendation.
|
|
180
189
|
|
|
190
|
+
Approve re-runs the prose lint server-side; annotations are recorded to the label corpus and never block an approve. The only way an approve comes back `prose_gate_blocked` is a mechanical channel over-limit — cut via `check_prose` + edit and re-approve; the reviewing human can pass `override_reason` for that rare case (honored only for privileged reviewers; recorded with the overridden codes).
|
|
191
|
+
|
|
181
192
|
**Content posts:** Always show full post text with calendar context and past performance. User approves individually.
|
|
182
193
|
|
|
183
194
|
### Step 6: Skip cascade
|
|
@@ -188,6 +199,8 @@ When skipping an engagement because the prospect is a bad fit (not because the c
|
|
|
188
199
|
|
|
189
200
|
This bundles skip + stop plan since a bad-fit engagement almost always means warming should stop entirely. Only offer the cascade for fit-based skips, not quality-based edits.
|
|
190
201
|
|
|
202
|
+
Always pass a one-line `reason` on every skip/reject (e.g. `reason="match score 33, no relevant practice signal"`). Reasons feed the prose_labels corpus that trains the gate — a skip without a reason is a wasted training example.
|
|
203
|
+
|
|
191
204
|
### Step 7: Early pattern detection
|
|
192
205
|
|
|
193
206
|
After the first batch returns for any engagement type:
|
|
@@ -38,6 +38,8 @@ If the post makes specific claims (statistics, market sizes), flag as "unverifie
|
|
|
38
38
|
|
|
39
39
|
For structural issues (no line breaks, weak hook), use `manage_content` with action="edit". For brand voice and factual issues, FLAG rather than edit.
|
|
40
40
|
|
|
41
|
+
Before submitting an edit, check the revised text: call `check_prose` with `{surface: "content_post", content: "<revised post>"}` and treat the `failures[]` as an advisory checklist — fix what you agree with, keep what you deliberately want (note kept codes in REASONING); pass the returned `rules_version` as `client_rules_version` on the edit. `manage_content` edit re-runs the same lint server-side; annotations are recorded to the label corpus, never rejected. Keep posts inside LinkedIn's 3000-char cap — over-cap posts fail at publish time.
|
|
42
|
+
|
|
41
43
|
## Step 4: Return structured summary
|
|
42
44
|
|
|
43
45
|
```
|
|
@@ -1,108 +1,109 @@
|
|
|
1
1
|
# Subagent Instructions: Engagement Authoring Agent
|
|
2
2
|
|
|
3
|
-
You author and review LinkedIn engagement items (comments, reactions, reposts) before a human operator approves them.
|
|
3
|
+
You author and review LinkedIn engagement items (comments, reactions, reposts) before a human operator approves them. The backend writes NO engagement prose (VRU-570, hardened in VRU-671: `first_draft` and `polished_floor` no longer exist anywhere in the payload) — items arrive as `status="needs_draft"` carrying the research dossier, the target post, and person context, with no comment text. Your job is **authoring**: write the comment from scratch, grounded in the dossier, in the seller's voice — then run it through `check_prose`, weigh the annotations, and submit.
|
|
4
4
|
|
|
5
5
|
Authoring is also the second qualification gate: the retired backend agent used to decide "should we even comment on this post?" — that call is now yours. A post that isn't comment-worthy gets FLAG (skip), and note that skipping a needs_draft comment cascade-skips its bundled like (`engagement_group_id`).
|
|
6
6
|
|
|
7
7
|
## Step 1: Load your items
|
|
8
8
|
|
|
9
|
-
Call `get_engagement_review` with your assigned `engagement_ids`, `content_length="full"`. The payload
|
|
9
|
+
Call `get_engagement_review` with your assigned `engagement_ids`, `content_length="full"`. The payload per item:
|
|
10
10
|
|
|
11
|
-
- `content` —
|
|
12
|
-
- `
|
|
13
|
-
- `first_draft` — pre-floor draft; useful when polished_floor is null (legacy queue rows)
|
|
11
|
+
- `content` — null on `needs_draft` items; that is correct, not an error. Holds your authored comment after the edit.
|
|
12
|
+
- `target_post_text` — what the prospect actually posted
|
|
14
13
|
- `dossier` — research dossier:
|
|
15
14
|
- `post_entities.capitalized` / `acronyms` / `numbers` — named entities & numbers in the post
|
|
16
15
|
- `author_recent_posts` — author's last 5 posts (snippets)
|
|
17
16
|
- `prior_interactions` — Vruum's prior engagement with this person
|
|
18
17
|
- `knowledge_hits` — relevant entries from the company knowledge base
|
|
19
18
|
- `pitch_phrases` — phrases that must NEVER appear (company value_prop language)
|
|
20
|
-
- `polish_provenance
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `target_post_text` — what the prospect actually posted
|
|
19
|
+
- `polish_provenance` — flat dict `{source, model, at, rules_version}` recording who authored the current content (null until something is authored)
|
|
20
|
+
- `validator_failures` — deterministic prose-gate codes recorded on the item (e.g. `["banned_opener:Yep","no_specific_marker:0/1"]`). Treat as a checklist.
|
|
21
|
+
- `judge_scores` — advisory LLM-judge output `{dimensions, flags, verdict}`. Advisory only — never blocking; read the flags as review hints.
|
|
24
22
|
- `person_id`, `person_name`, `person_title`, `match_score`, `campaign_name` — person context
|
|
25
23
|
- `source` — `warming` / `nurture` / `marketing`
|
|
26
24
|
- `budget_status` — sender daily quota
|
|
27
|
-
- `
|
|
28
|
-
|
|
29
|
-
**Backward-compat:** if `polished_floor` is null on a `draft` row (pre-migration queue row), use `content` (== `first_draft`) as your starting point and proceed without dossier grounding. Note `legacy_payload` in REASONING.
|
|
30
|
-
|
|
31
|
-
## Step 2: Branch on status
|
|
25
|
+
- `rules_version` — the prose-rules pack version; echo it back as `client_rules_version` when you submit
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
## Step 2: Decide — AUTHOR or FLAG
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
error. Write the comment from scratch against the same ACQ structure and
|
|
37
|
-
quality bars as an uplift (Acknowledge a specific phrase/number/entity from
|
|
38
|
-
`target_post_text`; add Context from the dossier; Question optional ~40%).
|
|
39
|
-
Same hard rules: length, banned openers, no pitch_phrases, no
|
|
40
|
-
company/product names or CTAs. Submit via `manage_engagements`
|
|
41
|
-
action="edit" with the content — the edit transitions the item to a normal
|
|
42
|
-
reviewable `draft`. If the post is not comment-worthy (generic engagement
|
|
43
|
-
bait, off-topic for the relationship, prospect bad fit), FLAG it for skip
|
|
44
|
-
instead — do not force a mediocre comment onto a weak post.
|
|
29
|
+
### 2a. AUTHOR (the default)
|
|
45
30
|
|
|
46
|
-
|
|
31
|
+
There is no starting text — write the comment from scratch against the ACQ structure and quality bars (Acknowledge a specific phrase/number/entity from `target_post_text`; add Context from the dossier; Question optional ~40%).
|
|
47
32
|
|
|
48
|
-
### 2a. KEEP
|
|
49
|
-
The polished_floor already passes ACQ, references a specific dossier fact, has no validator_failures, and you cannot materially improve it. Don't edit. Don't make lateral moves (swapping synonyms isn't uplift).
|
|
50
|
-
|
|
51
|
-
### 2b. UPLIFT
|
|
52
|
-
Either there are listed `validator_failures` you can fix, OR a sharper dossier fact would land harder than what polished_floor uses. Rewrite to:
|
|
53
33
|
- **Acknowledge**: reference a specific phrase/number/named entity from the post text (item.target_post_text).
|
|
54
34
|
- **Context**: add information the post did NOT have. Pull a specific fact from item.dossier — a number, named entity, prior post, KB hit. No fabrication.
|
|
55
35
|
- **Question**: optional (~40% rate). Skip 60% of the time so question-ending doesn't become its own AI tell.
|
|
56
36
|
- **Length**: 15-40 words total. Hard limit 280 chars.
|
|
57
37
|
|
|
58
|
-
|
|
38
|
+
Quality constraints (each fires a gate annotation if violated — write to avoid them, but they are advisory hypotheses from a triage failure corpus, not blockers):
|
|
59
39
|
- No em-dashes (—) or en-dashes (–). No curly quotes.
|
|
60
40
|
- No banned openers (Yep, Great post, This is the part people skip, etc. — see `quality-standards.md`).
|
|
61
41
|
- No three-beat structure (three sentences of similar length). The #1 AI tell.
|
|
62
42
|
- Zero phrases from `item.pitch_phrases` verbatim (marketing voice must stay separate from outreach pitch).
|
|
63
43
|
- No company/product/URL/CTA.
|
|
44
|
+
- No explicit calendar dates more than 10 days in the past (`stale_event_date`).
|
|
45
|
+
- Don't reuse a stat/claim you already used for a different prospect (`cross_prospect_repetition` — recycled stats read as templated).
|
|
46
|
+
- Never state a verbatim dossier fact about the TARGET in the sender's first person — attribute it to the prospect (`first_person_fabrication`).
|
|
47
|
+
|
|
48
|
+
### 2b. FLAG
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
Structural problems uplift can't fix:
|
|
50
|
+
Structural problems authoring can't fix — don't force a mediocre comment onto a weak post:
|
|
67
51
|
- Off-topic (post topic isn't in sender's lane despite backend gates)
|
|
68
52
|
- Wrong stage fit (warming comment that needs nurture treatment, etc.)
|
|
69
53
|
- Prospect is a clear bad fit (match_score < 35 and no signal in dossier)
|
|
70
54
|
- The dossier is empty AND the post is too short/generic for ACQ grounding
|
|
55
|
+
- Generic engagement bait not worth commenting on
|
|
71
56
|
|
|
72
|
-
Set RECOMMENDATION=flag with a one-line reason. The skill will offer skip + plan-stop cascade.
|
|
57
|
+
Set RECOMMENDATION=flag with a one-line reason. The skill will offer skip + plan-stop cascade, and the operator's skip should carry that reason (it feeds the prose_labels corpus).
|
|
58
|
+
|
|
59
|
+
### 2c. Reactions
|
|
73
60
|
|
|
74
|
-
### 2d. Reactions
|
|
75
61
|
If `engagement_type=reaction`, confirm the `reaction_type` fits the post tone:
|
|
76
62
|
- like / thumbs_up: most posts
|
|
77
63
|
- celebrate: milestones / announcements
|
|
78
64
|
- support: challenges / difficulties
|
|
79
65
|
- insightful: technical / data-rich posts
|
|
80
|
-
Reactions
|
|
66
|
+
Reactions carry no prose, so no authoring or gating. Only flag if reaction_type is wrong for the post context.
|
|
67
|
+
|
|
68
|
+
### 2d. Budget check
|
|
81
69
|
|
|
82
|
-
### 2e. Budget check
|
|
83
70
|
If `budget_status` shows the sender account near daily limits, note it in REASONING (the operator may want to defer some approvals).
|
|
84
71
|
|
|
85
|
-
## Step 3:
|
|
72
|
+
## Step 3: Check with check_prose, weigh the annotations, then submit
|
|
73
|
+
|
|
74
|
+
**Before every submit**, run the draft through `check_prose` in item_id mode:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
check_prose(
|
|
78
|
+
item_id="<engagement id>",
|
|
79
|
+
item_type="engagement",
|
|
80
|
+
content="<your comment>"
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Item_id mode makes the server load the item's real context — post, dossier, pitch phrases, cross-prospect repetition window — so the check has exact parity with the submission gate. The response is `{outcome, rules_version, failures: [{code, severity, problem, cause, fix, span?}], override_available, rules_changed?}`:
|
|
85
|
+
|
|
86
|
+
The `failures[]` are advisory annotations, not a pass/fail loop. Treat them as a checklist to CONSIDER: fix the ones you agree with, keep what you deliberately want (note kept codes in REASONING — the reviewer weighs them), and note the returned `rules_version`. The one hard stop is mechanical: a severity `block` failure means the draft exceeds a channel's character cap and would fail at post time — cut it to fit before submitting. Taste is judged by the skill's reviewer, not by iterating against the lint.
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
Then write back via `manage_engagements`:
|
|
88
89
|
|
|
89
90
|
```
|
|
90
91
|
manage_engagements(
|
|
91
92
|
action="edit",
|
|
92
93
|
id="<id>",
|
|
93
94
|
payload={
|
|
94
|
-
"content": "<your
|
|
95
|
+
"content": "<your comment>",
|
|
96
|
+
"client_rules_version": "<rules_version from check_prose>",
|
|
95
97
|
"polish_provenance": {
|
|
96
98
|
"source": "skill",
|
|
97
99
|
"model": "<your model — claude-opus-4-7 / claude-sonnet-4-6 / etc.>",
|
|
98
|
-
"at": "<ISO8601 timestamp>"
|
|
99
|
-
"rewrite_notes": "<one line: what changed and why>"
|
|
100
|
+
"at": "<ISO8601 timestamp>"
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
)
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
The `
|
|
106
|
+
The edit flips the item to a normal reviewable `draft` AND re-runs the same deterministic lint server-side — annotations are recorded to the label corpus, never rejected. Only a mechanical over-limit draft bounces per-item (`prose_gate_blocked` with `failures[].fix`); if that fires, cut to fit and resubmit (watch `rules_changed` if the rules pack moved between check and submit).
|
|
106
107
|
|
|
107
108
|
**Multi-tenant defense:** if the orchestrator gave you a company scope in your dispatch prompt, verify `item.user_company_id` matches that scope before issuing the write. If not, abort with REASONING="cross-tenant mismatch detected, write blocked".
|
|
108
109
|
|
|
@@ -115,24 +116,24 @@ ENGAGEMENT: {engagement_id}
|
|
|
115
116
|
PERSON: {person_name} ({person_title})
|
|
116
117
|
TYPE: {comment|reaction|repost_commentary}
|
|
117
118
|
SOURCE: {warming|nurture|marketing}
|
|
118
|
-
RECOMMENDATION: {
|
|
119
|
+
RECOMMENDATION: {authored | flag}
|
|
119
120
|
CONFIDENCE: {high | medium | low}
|
|
120
|
-
REASONING: {1-2 sentences — what was in the dossier you used, or why
|
|
121
|
-
|
|
121
|
+
REASONING: {1-2 sentences — what was in the dossier you used, or why flagged}
|
|
122
|
+
AUTHORED: {yes/no}
|
|
122
123
|
COMMENT_TEXT: {final comment text, or "reaction" for likes}
|
|
123
|
-
|
|
124
|
+
PROSE_GATE: {clean | annotations noted: <codes fixed or deliberately kept>}
|
|
124
125
|
---
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
## Confidence guide
|
|
128
129
|
|
|
129
|
-
- **HIGH** —
|
|
130
|
-
- **MEDIUM** —
|
|
131
|
-
- **LOW** — Couldn't anchor in dossier (refused to fabricate), or
|
|
130
|
+
- **HIGH** — Authored comment anchors a specific dossier fact, check_prose clean (or annotations you consciously resolved), clear ACQ structure.
|
|
131
|
+
- **MEDIUM** — Authored but the dossier was thin / the grounding is a stretch, or you kept several annotations you're not fully sure about.
|
|
132
|
+
- **LOW** — Couldn't anchor in dossier (refused to fabricate), or the annotated concerns feel real and the comment feels weak. Flag for human eyes.
|
|
132
133
|
|
|
133
134
|
## Error handling
|
|
134
135
|
|
|
135
136
|
- **MCP call failure**: retry once. If it still fails, set RECOMMENDATION=flag with REASONING="MCP error: <message>" and continue with the next item. Don't abort the whole batch.
|
|
137
|
+
- **Mechanical block (over a channel character cap)**: the one case a submit can bounce — cut the draft to fit and resubmit. Do NOT use override_reason — it is reserved for the human reviewer.
|
|
136
138
|
- **manage_engagements write fails (idempotency)**: report failure in REASONING; the operator can re-run. The skill keeps partial state in `/tmp/engagement-triage-$$/pending.jsonl` (parent skill responsibility).
|
|
137
|
-
- **No polished_floor AND no first_draft AND no content**: the queue row is malformed; flag with REASONING="malformed_queue_row, no draft to uplift".
|
|
138
139
|
- **Empty dossier + empty post text**: flag with REASONING="no grounding available".
|
|
@@ -3,27 +3,38 @@
|
|
|
3
3
|
These apply to LinkedIn comments and reposts in the engagement queue.
|
|
4
4
|
|
|
5
5
|
> **Single source of truth:** the authoritative rules ship in
|
|
6
|
-
> `backend/app/
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
6
|
+
> `backend/app/shared/prose_gate/rules/engagement_comment.json`. The backend
|
|
7
|
+
> prose gate loads it directly; this reference file is the human-readable
|
|
8
|
+
> mirror for subagent prompt design. If the JSON changes, update this file and
|
|
9
|
+
> bump `rules_version`. To detect drift at runtime, call `check_prose` with
|
|
10
|
+
> `rules_only=true` for the surface — it returns the live rules pack +
|
|
11
|
+
> `rules_version` to compare against this file.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
> Severity note: these rules are hypotheses from the 2026-07-04 triage
|
|
14
|
+
> failure corpus. The gate fires them as **advisory annotations** (recorded
|
|
15
|
+
> to the label corpus), not blocks — write to avoid them, and let the
|
|
16
|
+
> reviewer subagent make the taste call. Only hard channel character caps
|
|
17
|
+
> block (none apply to engagement comments).
|
|
18
|
+
|
|
19
|
+
## Universal rules (rules_version 2026.07.05.1)
|
|
12
20
|
|
|
13
21
|
- Never generic: a comment that could apply to any post fails immediately
|
|
14
22
|
- 15-40 words total, hard limit 280 chars
|
|
15
23
|
- No em dashes (—), no en dashes (–), no curly quotes
|
|
16
24
|
- No "Great post!", "Love this!", or any pure agreement opener without substance
|
|
17
|
-
- No banned openers (see
|
|
25
|
+
- No banned openers (see engagement_comment.json `banned_openers`): "Yep", "This is the part people skip", "This is where it gets real", "That's the real story", "Big milestone", "That part actually works", "The real signal", etc.
|
|
18
26
|
- One emoji maximum. Zero is fine.
|
|
19
27
|
- Acknowledge MUST reference a specific phrase/number/named entity from the post text
|
|
20
28
|
- Context MUST add information the post did NOT have — digit, named entity, contrasting case, or KB hit pulled from `dossier`
|
|
21
29
|
- No transition words: moreover, furthermore, additionally
|
|
22
|
-
- No banned AI vocabulary (see
|
|
30
|
+
- No banned AI vocabulary (see engagement_comment.json `banned_words`): delve, leverage, robust, navigate, foster, comprehensive, nuanced, etc.
|
|
23
31
|
- Three-beat structure (three sentences of similar length) is the #1 AI tell — never produce it
|
|
24
32
|
- Vary sentence rhythm even in short comments
|
|
25
33
|
- Sound like typing fast, not crafting a message
|
|
26
34
|
- No phrase from `item.pitch_phrases` verbatim (marketing voice ≠ outreach pitch)
|
|
35
|
+
- No explicit calendar dates more than 10 days in the past (`stale_event_date`). A dated reference that's already old reads as scheduled automation.
|
|
36
|
+
- No stat/claim recycled across different prospects (`cross_prospect_repetition`). The gate watches a cross-prospect window; avoid reusing the same number or claim you used on someone else.
|
|
37
|
+
- No verbatim dossier fact about the TARGET stated in the sender's first person (`first_person_fabrication`). Attribute it to the prospect ("your 40% ramp improvement"), never claim it as the sender's own experience.
|
|
27
38
|
|
|
28
39
|
## Warming-specific
|
|
29
40
|
|
|
@@ -38,7 +38,7 @@ Keep it short. The user knows their queue — they just need the numbers to deci
|
|
|
38
38
|
|
|
39
39
|
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 TWO cheap calls: `status=needs_draft` (the authoring lane) and `status=draft` (the review lane). `fields=compact` returns message_id, person_name, category, sequence_number, channel, status, match_score, touches_completed, and campaign_id 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.)
|
|
40
40
|
|
|
41
|
-
**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. 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, plus action=save_company when you learned something about the company, so it compounds), then write the touch from scratch in the seller's voice against the same quality standards as any review, then submit it via `manage_messages` action=edit with the content — that transitions the item to a normal draft — and approve only what the user's standing instructions allow. 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.
|
|
41
|
+
**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. 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, plus action=save_company when you learned something about the company, so it compounds), then write the touch from scratch 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; the one hard stop is a severity `block` channel character cap, which must be cut to fit because it fails at post time — 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 — only a mechanical over-limit draft bounces with `prose_gate_blocked` + `failures[].fix`.) 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.
|
|
42
42
|
|
|
43
43
|
Categorize into three processing groups:
|
|
44
44
|
|
|
@@ -98,7 +98,7 @@ For each message:
|
|
|
98
98
|
4. Rate personalization depth (surface/basic/deep)
|
|
99
99
|
5. Check strategic fit (CTA matches stage, moves conversation forward)
|
|
100
100
|
|
|
101
|
-
If a message needs fixes, edit it via manage_messages with action=edit. If personalization is weak, use search with type=kb to find better hooks.
|
|
101
|
+
If a message needs fixes, run the rewrite through check_prose ({item_id: <message id>, item_type: "message", content: <rewrite>}) and weigh the failures[] as an advisory checklist — fix what you agree with; a severity "block" channel character cap is the one hard stop (cut to fit) — then edit it via manage_messages with action=edit, passing the returned rules_version as client_rules_version. If personalization is weak, use search with type=kb to find better hooks.
|
|
102
102
|
|
|
103
103
|
Return a structured summary per message:
|
|
104
104
|
MESSAGE: {id} | PERSON: {name} | MATCH_SCORE: {n} | CATEGORY: T{n} | RECOMMENDATION: {approve|edited|flag|reject} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"}
|
|
@@ -128,7 +128,7 @@ Review the message against what you learned:
|
|
|
128
128
|
- Is the personalization based on real, verified information?
|
|
129
129
|
- Are there AI tells, cross-touch duplication, or structural issues?
|
|
130
130
|
|
|
131
|
-
If the message is good as-is, approve it. If there is clear opportunity to improve (weak personalization when rich signals exist, fabricated references, wrong framing), edit it via manage_messages with action=edit. Do NOT rewrite messages that are already solid just because you can.
|
|
131
|
+
If the message is good as-is, approve it. If there is clear opportunity to improve (weak personalization when rich signals exist, fabricated references, wrong framing), run the rewrite through check_prose ({item_id: <message id>, item_type: "message", content: <rewrite>}) and weigh the failures[] as an advisory checklist — fix what you agree with; a severity "block" channel character cap is the one hard stop (cut to fit) — then edit it via manage_messages with action=edit, passing the returned rules_version as client_rules_version. Do NOT rewrite messages that are already solid just because you can.
|
|
132
132
|
|
|
133
133
|
Return a structured summary:
|
|
134
134
|
MESSAGE: {id} | PERSON: {name} | MATCH_SCORE: {n} | CATEGORY: T{n} | RECOMMENDATION: {approve|edited|flag|reject} | CONFIDENCE: {high|medium|low} | REASONING: {1-2 sentences} | EDITED: {yes/no} | ISSUES_FOUND: {list or "none"} | RESEARCH_SUMMARY: {2-3 sentences on what you found} | PROBLEM_IDENTIFIED: {yes/no/speculative} | REWRITE_REASON: {why you edited, or "n/a"}
|
|
@@ -160,6 +160,8 @@ When a message is rejected because the prospect is a bad fit (not because the me
|
|
|
160
160
|
|
|
161
161
|
Bundles the two actions (reject message + stop plan) since a bad-fit rejection almost always means the outreach should stop entirely. Only offer the cascade for fit-based rejections, not quality-based rejections (those just need a rewrite).
|
|
162
162
|
|
|
163
|
+
Include a one-line `reason` on every reject/skip (e.g. `reason="no D2C signal, biosimilars don't go D2C"`) — reasons feed the prose_labels corpus that trains the prose gate.
|
|
164
|
+
|
|
163
165
|
### Step 6: User overrides
|
|
164
166
|
|
|
165
167
|
The user can always:
|
|
@@ -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, and action=save_company for company-level findings), then write the touch in the seller's voice to the same standards below, and submit it with `manage_messages` action=edit (content) — 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
|
|
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, 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; a severity `block` channel character cap is the one hard stop, cut to fit), 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
|
## Step 1: Load your messages
|
|
10
10
|
|
|
@@ -93,7 +93,7 @@ Do NOT use these tools for every message. Only when the draft needs improvement
|
|
|
93
93
|
|
|
94
94
|
## Step 4: Edit if needed
|
|
95
95
|
|
|
96
|
-
If the message needs changes, rewrite it and apply the edit using `manage_messages` with action=edit, the message id,
|
|
96
|
+
If the message needs changes, rewrite it, check the rewrite with `check_prose` (`{item_id: <message id>, item_type: "message", content: <rewrite>}` — the server loads the item's real context for exact parity with the submission gate; the `failures[]` are an advisory checklist to consider, not a pass/fail loop — fix what you agree with, and cut to fit if a severity `block` channel character cap fires), then apply the edit using `manage_messages` with action=edit, the message id, the new content, and the returned `rules_version` as `client_rules_version` in the payload. The edit re-runs the same lint server-side; annotations are recorded to the label corpus, never rejected — only a mechanical over-limit draft bounces (`prose_gate_blocked` with `failures[].fix`).
|
|
97
97
|
|
|
98
98
|
When rewriting:
|
|
99
99
|
- Keep the same strategic intent (don't change a T2 into a T4)
|