@sellable/mcp 0.1.80 → 0.1.82

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.
Files changed (36) hide show
  1. package/agents/post-find-leads-filter-scout.md +19 -8
  2. package/agents/post-find-leads-message-scout.md +21 -6
  3. package/agents/registry.json +33 -18
  4. package/agents/source-scout-linkedin-engagement.md +10 -2
  5. package/agents/source-scout-prospeo-contact.md +9 -2
  6. package/agents/source-scout-sales-nav.md +10 -2
  7. package/dist/server.js +7 -1
  8. package/dist/tools/bootstrap.js +2 -2
  9. package/dist/tools/campaigns.d.ts +12 -0
  10. package/dist/tools/campaigns.js +40 -5
  11. package/dist/tools/context.d.ts +10 -4
  12. package/dist/tools/context.js +8 -4
  13. package/dist/tools/leads.d.ts +6 -3
  14. package/dist/tools/leads.js +39 -43
  15. package/dist/tools/navigation.d.ts +28 -1
  16. package/dist/tools/navigation.js +220 -10
  17. package/dist/tools/prompts.js +1 -1
  18. package/dist/tools/provider-preflight.js +2 -3
  19. package/dist/tools/readiness.d.ts +31 -6
  20. package/dist/tools/readiness.js +6 -1
  21. package/dist/tools/rubrics.d.ts +2 -0
  22. package/dist/tools/rubrics.js +2 -0
  23. package/package.json +1 -1
  24. package/skills/create-campaign/SKILL.md +24 -12
  25. package/skills/create-campaign-v2/SKILL.md +166 -114
  26. package/skills/create-campaign-v2/SOUL.md +19 -11
  27. package/skills/create-campaign-v2/core/auto-execute.yaml +14 -13
  28. package/skills/create-campaign-v2/core/flow.v2.json +964 -310
  29. package/skills/create-campaign-v2/references/approval-gate-framing.md +64 -32
  30. package/skills/create-campaign-v2/references/filter-leads.md +8 -0
  31. package/skills/create-campaign-v2/references/final-handoff-contract.md +55 -20
  32. package/skills/create-campaign-v2/references/lead-validation-preview.md +30 -17
  33. package/skills/create-campaign-v2/references/step-13-import-leads.md +62 -17
  34. package/skills/create-campaign-v2/references/validation-criteria.md +10 -5
  35. package/skills/create-campaign-v2/references/watch-link-handoff.md +72 -72
  36. package/skills/create-campaign-v2-tail/SKILL.md +123 -92
@@ -1,110 +1,110 @@
1
1
  # Watch Link Handoff
2
2
 
3
- This reference governs how create-campaign-v2 surfaces the watch link after
4
- the commit gate passes and the atomic mint succeeds. Load this file before
5
- showing the watch link, and on every resume where the link needs to be
6
- re-surfaced.
3
+ This reference governs how create-campaign-v2 surfaces the watch link in the
4
+ shell-first flow. Load it before showing the link and on every resume where the
5
+ link needs to be re-surfaced.
6
+
7
+ CampaignOffer state is canonical; disk artifacts are a debug trail. All 14 disk
8
+ artifacts remain as debug outputs, but resume, gating, and handoff read campaign
9
+ state first.
7
10
 
8
11
  ## Plumbing Reuse
9
12
 
10
13
  `create_campaign` already returns a signed `watchUrl` on the response
11
14
  (`CampaignDetail.watchUrl` in `mcp/sellable/src/tools/campaigns.ts`). V2 does
12
15
  NOT mint a new token, does NOT call a different route, and does NOT construct
13
- the URL locally. The skill captures whatever `watchUrl` the existing tool
14
- returns and surfaces it verbatim.
16
+ the URL locally. Capture whatever `watchUrl` the existing tool returns and
17
+ surface it verbatim.
15
18
 
16
- The v1 `create-campaign` SKILL uses the same "watch mode" pattern: after a
17
- step change, the skill names what changed and orients the user to what the
18
- watch link will show next. V2 adopts that pattern verbatim — the only
19
- difference is V2 surfaces the link once, at the atomic mint boundary, and
20
- then re-surfaces it on resume.
19
+ ## Shell-First Link
21
20
 
22
- ## Atomic Mint Sequence
21
+ The active flow creates the campaign shell after the v1 brief has been rendered
22
+ and persisted. The link is surfaced only after `create_campaign` returns both:
23
23
 
24
- The watch link is only surfaced after **both** of these tool calls succeed in
25
- this order:
24
+ - `campaignId`
25
+ - `watchUrl`
26
26
 
27
- 1. `create_campaign({ campaignBrief, currentStep: "auto-execute-leads" })`
28
- response MUST include `{ campaignId, watchUrl }`
29
- 2. `save_rubrics({ campaignOfferId, rubric })`
30
- → must return success
27
+ The shell must already contain the v1 campaign brief, so the user lands on a
28
+ useful brief instead of an empty campaign.
31
29
 
32
- Both calls must succeed before the link is printed. Never print the link
33
- between the two calls. Never print the link if either call fails.
30
+ Example skeleton:
34
31
 
35
- ## Required Output Block
32
+ ```text
33
+ I created the campaign shell with the brief already in it.
34
+ You can watch the lead source, filters, and messages fill in from here.
36
35
 
37
- After both tool calls succeed, print exactly one user-facing block. The block
38
- must contain:
36
+ No leads import and nothing sends yet.
39
37
 
40
- - a plain confirmation that the campaign was created and the approved brief was
41
- saved
42
- - a one-line orientation sentence: `Open this to watch lead sourcing, rubric
43
- scoring, and messaging populate live.`
44
- - the `watchUrl` captured from the `create_campaign` response
38
+ Watch link: {watchUrl}
45
39
 
46
- Example skeleton:
40
+ Cool, let's find leads.
41
+ ```
47
42
 
48
- ```text
49
- Campaign created. I saved the approved brief too.
43
+ Atomic-mint legacy orientation:
50
44
 
45
+ ```text
51
46
  Open this to watch lead sourcing, rubric scoring, and messaging populate live.
52
-
53
- Watch link: {watchUrl}
54
47
  ```
55
48
 
56
- The exact wording may be adapted for tone, but the one-line orientation and
57
- the `watchUrl` are both required. Do not collapse the block into a single
58
- line with the link inline — the user needs to see the orientation clearly
59
- before they click.
49
+ The exact wording may be adapted for tone, but the copy must make three things
50
+ clear:
60
51
 
61
- ## Partial-Mint Recovery
52
+ - the campaign shell exists now
53
+ - the brief is already visible in the campaign
54
+ - import, sequence, and start are still blocked
62
55
 
63
- The following failure modes must NOT print the watch link:
56
+ If shell creation fails or the response is missing `watchUrl`, stop and surface
57
+ the error. Do not continue into campaignless source scouting in the active
58
+ shell-first flow.
59
+ If a legacy atomic-mint response is missing `watchUrl`, it is not a silent skip.
60
+ Stop before `save_rubrics` and recover the missing signed URL first.
64
61
 
65
- - `create_campaign` errors (no campaign exists, no watchUrl was returned) →
66
- surface the underlying error, do not proceed to `save_rubrics`. No
67
- rollback needed.
68
- - `create_campaign` succeeds but the response is missing `watchUrl` → treat
69
- as a recoverable failure. Stop before `save_rubrics`. Do not print a link.
70
- Surface an explicit error that names the contract violation (the backend
71
- tool should always return `watchUrl`; a missing value is a plumbing bug,
72
- not a silent skip).
73
- - `save_rubrics` fails after `create_campaign` succeeds → either rollback the
74
- `CampaignOffer` row, or mark an explicit recoverable failure state so the
75
- user can retry rubric save. Never leave a silent partial campaign with no
76
- rubrics. Do NOT print the watch link during this recovery window.
62
+ ## Step Orientation
77
63
 
78
- The guiding rule: a watch link points at a live, fully-minted campaign. If
79
- the campaign is half-minted, the link is misleading and must not be shown.
64
+ After the shell exists, re-surface the same watch link only when it helps orient
65
+ the user after a meaningful step change:
80
66
 
81
- ## Resume Re-Surface
67
+ - source selected: the campaign should point at the primary provider step
68
+ - rubrics saved: the campaign should show filter/rubric state
69
+ - message set approved: the campaign should show message state
70
+ - 10-lead test batch imported/generated: the campaign should show the test rows
71
+ - settings handoff: the campaign should open to Settings for sender attachment
72
+ - running: the campaign is live
82
73
 
83
- Users can quit the session after `approve` and reopen later. Plan 85-02
84
- handles autonomous tail resume via `CampaignOffer.currentStep`. Part of the
85
- resume contract is re-surfacing the same watch link so the user reorients
86
- without digging through prior transcript.
74
+ Do not spam the link between internal tool calls.
75
+
76
+ ## Resume Re-Surface
87
77
 
88
78
  On resume:
89
79
 
90
80
  1. Load the `CampaignOffer` for the campaign being resumed.
91
- 2. Call the same tool that yielded the link on mint (e.g.
92
- `create_campaign({ campaignId })`, which is idempotent and returns the
93
- existing campaign's `watchUrl` + state see the v1 pattern in
94
- `mcp/sellable/skills/create-campaign/SKILL.md`).
95
- 3. Print the same block again with the one-line orientation plus `watchUrl`.
81
+ 2. Recover the signed link through the existing resume path
82
+ (`create_campaign({ campaignId })` when available).
83
+ 3. Inspect `currentStep` and print a short orientation for where the user will
84
+ land now.
85
+ 4. Print the recovered `watchUrl`.
96
86
 
97
87
  The re-surface must use the signed `watchUrl` from the tool response, not a
98
88
  cached or reconstructed URL.
99
89
 
100
90
  ## Hard Rules
101
91
 
102
- - Surface the link **once** on successful atomic mint, and re-surface on
103
- resume. Do not spam the link between intermediate autonomous-tail steps.
104
- - Never print the link while the mint is mid-flight. Both
105
- `create_campaign` and `save_rubrics` must have succeeded first.
106
- - Never fabricate or reconstruct the URL locally. Capture it from the
107
- `create_campaign` response.
108
- - Missing `watchUrl` in the response is an error, not a silent skip.
109
- - Partial-mint states (campaign row created but rubrics not saved) must
110
- block link surfacing and surface the recovery path instead.
92
+ - Never fabricate or reconstruct the URL locally.
93
+ - Missing `watchUrl` is an error, not a silent skip.
94
+ - The first watch link must be shown only after the v1 brief is on the campaign.
95
+ - A watch link is not approval to import, attach sequence, or start.
96
+ - Import/enrichment remains blocked until rubrics are saved and the approved
97
+ message set is synced into the campaign brief.
98
+
99
+ ## Partial-Mint Recovery
100
+
101
+ If `save_rubrics` fails after `create_campaign` succeeds, prefer a recoverable
102
+ failure state with explicit retry. Rollback is an operator escape hatch, not the
103
+ default. Never leave a silent partial campaign.
104
+ Use rollback only as an explicit operator escape hatch.
105
+ Store a recoverable failure state for retry before any watch link is shown.
106
+
107
+ Do NOT print the watch link during this recovery window. A partial campaign link
108
+ is misleading and must not be shown; block link surfacing and surface the
109
+ recovery path instead.
110
+ During partial mint, block link surfacing and surface the recovery path.
@@ -8,6 +8,11 @@ visibility: internal
8
8
 
9
9
  This is the tail detail extracted from the main create-campaign-v2 SKILL.md to keep the entry prompt slim. The agent loads this on-demand BEFORE entering the auto-execute or validate-sample steps. Follow this verbatim.
10
10
 
11
+ CampaignOffer state is canonical; disk artifacts are a debug trail. All 14 disk
12
+ artifacts remain as debug outputs, but resume, gating, and handoff read campaign
13
+ state first. selectedLeadListId remains the source list; workflowTableId is the
14
+ campaign table.
15
+
11
16
  ## MANDATORY TOOL ORDER (read this BEFORE any tail step)
12
17
 
13
18
  Every tail run MUST call these tools in this exact order. The tail is
@@ -20,16 +25,24 @@ Do NOT manually run rubric-check, enrich, or message-generation
20
25
  tools — the cascade already does them.
21
26
 
22
27
  ```text
23
- Step 13 — import review batch + kick bounded cascade
28
+ Step 13 — import/confirm review batch only
29
+ materialize/reuse approved source with campaignOfferId
24
30
  import_leads(targetLeadCount=importLimit)
25
31
  wait_for_lead_list_ready
26
32
  confirm_lead_list
27
- get_rows_minimal # capture enrichCellIds
33
+ wait_for_campaign_table_ready # campaign table exists
34
+ get_rows_minimal # read imported review batch
35
+ update_campaign(currentStep=filter-choice)
36
+
37
+ Post-import main thread
38
+ launch Lead Fit Builder + Message Draft Builder after workflowTableId exists
39
+ save_rubrics({ campaignOfferId, leadScoringRubrics }) after the campaign table exists
40
+ show readable filters + one concrete sample message
41
+ after approval, update_campaign_brief writes `## Approved Message Template` with `{{...}}` tokens
42
+
43
+ Step 14 — kick bounded cascade + observe sample
28
44
  queue_cells(cellIds=<review-batch Enrich Prospect cells only>) <-- starts bounded chain
29
45
  wait_for_campaign_table_ready # review-batch cascade drains here
30
- update_campaign(currentStep=validate-sample)
31
-
32
- Step 14 — observe sample
33
46
  get_rows_minimal # read passesRubric + message cell status per row
34
47
  compute projectedPass
35
48
  if OK: update_campaign(currentStep=auto-execute-messaging)
@@ -46,7 +59,9 @@ Step 15 — observe messaging
46
59
  (generate_messages is NOT an MCP tool; messages come from the cascade)
47
60
 
48
61
  Step 16 — awaiting-user-greenlight
49
- attach_recommended_sequence({ campaignId }) # tier-aware: premium/SN -> If Open Profile->INMAIL_OPEN, else INVITE->accepted->DM
62
+ get_campaign + list_senders
63
+ if no sender attached: surface campaign settings link and STOP
64
+ if sender attached: attach_recommended_sequence({ campaignId }) # tier-aware: premium/SN -> If Open Profile->INMAIL_OPEN, else INVITE->accepted->DM
50
65
  re-surface watchUrl + review-batch orientation
51
66
  STOP. DO NOT call start_campaign. DO NOT auto-advance currentStep.
52
67
  ```
@@ -118,10 +133,17 @@ After every `update_campaign({ currentStep: ... })` in the tail, narrate
118
133
  what changed and orient the user to what the watch link will show next —
119
134
  reuse the v1 `create-campaign` watch-mode pattern verbatim.
120
135
 
136
+ Resume currentStep names covered by this tail: `"auto-execute-leads"`,
137
+ `"validate-sample"`, `"auto-execute-messaging"`,
138
+ `"awaiting-user-greenlight"`, and `"running"`. New mint-early runs normally
139
+ enter Step 13 from `"confirm-lead-list"`; the `"auto-execute-leads"` string is
140
+ kept for compatibility resumes.
141
+
121
142
  ## Step 13: auto-execute-leads
122
143
 
123
- Entered on `CampaignOffer.currentStep === "auto-execute-leads"` (set by
124
- atomic mint).
144
+ Entered from the lead-source confirmation path, with
145
+ `CampaignOffer.currentStep === "confirm-lead-list"` or an equivalent source
146
+ import milestone.
125
147
 
126
148
  > Reminder: every provider search you run from this point forward — the
127
149
  > review-batch source rerun in Step 13, any expansion search after the
@@ -139,24 +161,16 @@ atomic mint).
139
161
  > - the **campaign workflow table** (`CampaignOffer.workflowTableId`,
140
162
  > also returned as `campaignTableId` from `confirm_lead_list`).
141
163
  >
142
- > `confirm_lead_list` overwrites `CampaignOffer.selectedLeadListId` with
143
- > the campaign-table id. That means after the first confirm, the
144
- > campaign-table id is NOT a valid `sourceLeadListId` for any future
145
- > `import_leads` call.
146
- >
147
- > When you scale up the import, `mode: "add"` requires the ORIGINAL
148
- > lead-list id (the one returned by the first `import_leads`), never the
149
- > campaign-table id. Passing the campaign-table id silently writes
150
- > straight into the campaign workflow table, bypassing the lead list
151
- > entirely; the lead list stays at its original size and the
152
- > campaign-builder `/leads` UI throws "Lead list not found" because it
153
- > expects `selectedLeadListId` to resolve as a lead list.
164
+ > `confirm_lead_list` must preserve the source lead-list id as
165
+ > `CampaignOffer.selectedLeadListId` and store the campaign workflow table as
166
+ > `CampaignOffer.workflowTableId`. The campaign-table id is NOT a valid
167
+ > `sourceLeadListId` for future `import_leads(mode: "add")` calls.
154
168
  >
155
- > Capture the original `leadListId` from the FIRST `import_leads`
156
- > response and reuse it for every scale-up `import_leads(mode: "add")`
157
- > call on the same campaign. If the original id is unknown, query
158
- > for the lead-list table by name or skip the `mode: "add"` path; do
159
- > not pass the campaign-table id as `sourceLeadListId`.
169
+ > Capture the original `leadListId` from the FIRST `import_leads` response and
170
+ > reuse it for every scale-up `import_leads(mode: "add")` call on the same
171
+ > campaign. If the original id is unknown, query for the lead-list table by
172
+ > name or skip the `mode: "add"` path; do not pass the campaign-table id as
173
+ > `sourceLeadListId`.
160
174
 
161
175
  1. Load `core/auto-execute.yaml` through
162
176
  `get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "core/auto-execute.yaml" })`.
@@ -165,12 +179,16 @@ atomic mint).
165
179
  `messaging.critique.enabled`, `handoff.autoStart`,
166
180
  `handoff.orientation`, `retry.sameToolSameError`,
167
181
  `logging.logEveryThresholdTrip`.
168
- 2. Resolve the approved source before importing. If there is no
169
- `lead-source-intake.json`, keep the existing discovered-provider path:
170
- `import_leads({ campaignOfferId, targetLeadCount: importLimit })`.
182
+ 2. Resolve and materialize the approved source before importing. Load
183
+ `references/post-mint-source-materialization.md` when the source is normal
184
+ discovery or when a legacy no-shell approval fixture must be replayed. If
185
+ CampaignOffer state already attached searches/selections to this campaign,
186
+ reuse them. If there is no `lead-source-intake.json`, replay the approved
187
+ provider recipe from `lead-review.md` with `campaignOfferId` before
188
+ `import_leads`.
171
189
  If a manifest exists, branch by `sourceType`:
172
190
  - `supplied-linkedin-profiles`: revalidate file metadata and confirmation
173
- token, confirm `load_csv_linkedin_leads` only after approval to batch the
191
+ token, confirm `load_csv_linkedin_leads` as source-list materialization to batch the
174
192
  supplied CSV into a Sellable lead list, persist the returned `leadListId`,
175
193
  then call `confirm_lead_list({ sourceLeadListId: leadListId,
176
194
  targetLeadCount: importLimit })`. Do not call `import_leads` for this
@@ -188,66 +206,66 @@ targetLeadCount: importLimit })`. Do not call `import_leads` for this
188
206
  the cap, import only the first `importLimit` rows and leave the rest for
189
207
  a later expansion step.
190
208
  Persist or recover materialized IDs on resume: `leadListId`,
191
- `domainFilterId`, `searchId`, `campaignTableId`, imported row IDs, and queued
192
- cell IDs. If the source file changed after preview, or an existing lead list
209
+ `domainFilterId`, `searchId`, `selectedLeadListId`, `workflowTableId`, and
210
+ imported row IDs. If the source file changed after preview, or an existing lead list
193
211
  is missing/wrong-workspace, escalate before materialization instead of
194
212
  silently applying stale preview data. Retrying Step 13 must not duplicate
195
- lead lists, searches, campaign rows, or queued cells.
196
- 3. `wait_for_lead_list_ready` and `confirm_lead_list`.
197
- 4. **Verify the table is set up properly before queuing.** Call
198
- `get_rows_minimal({ tableId })` and confirm:
199
- - `Enrich Prospect` column exists on the table and every imported
200
- row has a corresponding cell with `status === "pending"`.
201
- - `DNC Check`, `ICP Score`, `Passes Rubric`, and `Generate Message`
202
- columns all exist too (the cascade depends on all of them).
203
- - If any are missing, or the cell is in a non-`pending` terminal
204
- state from an earlier run, ESCALATE with an explicit "table not
205
- configured for autonomous tail" diagnosis. Do NOT attempt to
206
- patch the table shape from the tail.
207
- 5. **Kick off the bounded review-batch cascade.** `queue_cells({ tableId,
208
- cellIds: <pending enrichCellIds for the imported review batch only> })`.
209
- You queue ONLY the review-batch `Enrich Prospect` cells; the cascade
210
- auto-dispatches `DNC Check`, `ICP Score`, `Passes Rubric`, and (for rows
211
- where `Passes Rubric === true`) `Generate Message` as each upstream
212
- completes. Do not queue cells for any rows beyond `importLimit` before
213
- user expansion approval.
214
- 6. `wait_for_campaign_table_ready` waits for the review-batch cascade to
215
- drain.
216
- If it times out with most cells still `pending`, ESCALATE: the
217
- engine is blocked.
218
- 7. If the import returns zero usable leads, ESCALATE per
213
+ lead lists, searches, or campaign rows.
214
+ Normal discovery provider order:
215
+ - Signal Discovery: `get_provider_prompt({ provider: "signal-discovery",
216
+ campaignOfferId, confirmed: true })` -> `search_signals({ campaignOfferId,
217
+ ...approved recipe })` -> `select_promising_posts({ campaignOfferId,
218
+ selectionMode: "replace", selections, headlineICPCriteria })` ->
219
+ `import_leads({ campaignOfferId, provider: "signal-discovery",
220
+ targetLeadCount: importLimit })`.
221
+ - Sales Nav: `get_provider_prompt({ provider: "sales-nav", campaignOfferId,
222
+ confirmed: true })` -> rebuild/verify filter IDs -> `search_sales_nav({
223
+ campaignOfferId, filters, confirmed: true })` -> `import_leads({
224
+ campaignOfferId, provider: "sales-nav", searchId, targetLeadCount:
225
+ importLimit })`.
226
+ - Prospeo: `get_provider_prompt({ provider: "prospeo", campaignOfferId,
227
+ confirmed: true })` -> reuse `domainFilterId` when present ->
228
+ `search_prospeo({ campaignOfferId, filters, domainFilterId, confirmed:
229
+ true })` -> `import_leads({ campaignOfferId, provider: "prospeo",
230
+ searchId, targetLeadCount: importLimit })`.
231
+ 3. `wait_for_lead_list_ready` when a provider import job exists, then
232
+ `confirm_lead_list`. Persist both identifiers: `selectedLeadListId` remains
233
+ the source list and `workflowTableId` is the campaign table.
234
+ 4. `wait_for_campaign_table_ready` until the bounded review-batch rows are
235
+ available in the campaign table.
236
+ 5. Call `get_rows_minimal({ tableId: workflowTableId })` and confirm the first
237
+ review batch is present. Do not queue cells in Step 13.
238
+ 6. If the import returns zero usable leads, ESCALATE per
219
239
  `references/escalation-ladder.md` (hard fail).
220
- 8. `update_campaign({ campaignId, currentStep: "validate-sample" })`.
221
- 9. Orient user on the watch link (same pattern as every other step
240
+ 7. `update_campaign({ campaignId, currentStep: "filter-choice" })`.
241
+ 8. Orient user on the watch link (same pattern as every other step
222
242
  change).
223
243
 
224
244
  **Do NOT call `check_rubric`, `wait_for_rubric_results`,
225
- `enrich_with_prospeo`, or `bulk_enrich_with_prospeo` in Step 13.**
245
+ `queue_cells`, `enrich_with_prospeo`, or `bulk_enrich_with_prospeo` in Step 13.**
226
246
  Those are direct-API tools that fetch enrichment/scoring data to the
227
- caller; they do not write to the workflow table cells. The cascade
228
- triggered by `queue_cells` runs the `Enrich Prospect` column's
229
- http_request, which writes the enrichment into the cell, which
230
- triggers the next column, and so on. Running the direct-API tools
231
- is duplicate work with no cell-population side effect.
247
+ caller or start the workflow-table cascade too early. The cascade starts in
248
+ Step 14 only after `save_rubrics` and `update_campaign_brief` have both
249
+ succeeded.
232
250
 
233
251
  ## Step 14: validate-sample (loop)
234
252
 
235
253
  Entered on `CampaignOffer.currentStep === "validate-sample"`. Full
236
254
  decision tree lives in `references/sample-validation-loop.md`.
237
255
 
238
- **Step 14 is pure observation.** The bounded review-batch cascade that Step
239
- 13 kicked off (via `queue_cells` on Enrich Prospect) has already written ICP
240
- scores and Passes Rubric values back into the workflow table. Step 14 reads
241
- those review-batch cell results it does NOT call `check_rubric`,
242
- `wait_for_rubric_results`, `bulk_enrich_with_prospeo`, or any other
243
- direct enrichment/scoring tool. All of that already happened in the
244
- cascade; running them again is wasted work and can overwrite the
245
- real cell results with unrelated data.
256
+ **Step 14 starts the bounded cascade, then observes it.** Step 13 imported the
257
+ review batch only. After `save_rubrics` and the approved message template are
258
+ persisted, Step 14 queues the review-batch Enrich Prospect cells, waits for the
259
+ table cascade to drain, then reads the resulting ICP scores and Passes Rubric
260
+ values. It does NOT call `check_rubric`, `bulk_enrich_with_prospeo`, or any
261
+ other direct enrichment/scoring tool.
246
262
 
247
263
  Shape:
248
264
 
249
265
  ```text
250
- get_rows_minimal({ tableId })
266
+ queue_cells({ tableId: workflowTableId, cellIds: reviewBatchEnrichCellIds })
267
+ wait_for_campaign_table_ready({ tableId: workflowTableId })
268
+ get_rows_minimal({ tableId: workflowTableId })
251
269
  passInSample = count of first sampleSize review-batch rows with passesRubric === true
252
270
  projectedPass = round(passInSample / sampleSize * importLimit)
253
271
 
@@ -376,13 +394,20 @@ Full contract lives in `references/final-handoff-contract.md`.
376
394
 
377
395
  Shape:
378
396
 
379
- 1. `attach_sequence` (bind the committed sequence to the campaign).
380
- 2. Re-surface the `watchUrl` using `references/watch-link-handoff.md`.
381
- 3. Surface the `handoff.orientation` string from `auto-execute.yaml`
397
+ 1. Call `get_campaign({ campaignId })` and inspect sender/sequence state.
398
+ 2. Call `list_senders()` so you can surface connected sender options if the
399
+ campaign has no attached sender.
400
+ 3. If the campaign has no attached sender, surface a direct settings link:
401
+ `/campaign-builder/{campaignId}/settings?mode=claude`. Tell the user to attach
402
+ a sender in Settings, then return here. STOP before sequence attach/start.
403
+ 4. If a sender is attached, call `attach_recommended_sequence({ campaignId })`
404
+ (bind the tier-recommended sequence to the campaign).
405
+ 5. Re-surface the `watchUrl` using `references/watch-link-handoff.md`.
406
+ 6. Surface the `handoff.orientation` string from `auto-execute.yaml`
382
407
  alongside the link.
383
- 4. STOP. Do NOT call `start_campaign`. Do NOT auto-advance
408
+ 7. STOP. Do NOT call `start_campaign`. Do NOT auto-advance
384
409
  `currentStep`. The autonomous tail ends here.
385
- 5. Make the credit boundary explicit in the handoff: only the first review
410
+ 8. Make the credit boundary explicit in the handoff: only the first review
386
411
  batch has been enriched/messaged; expanding to more leads requires a
387
412
  separate user instruction.
388
413
 
@@ -394,10 +419,12 @@ campaign:
394
419
  the running state and shifts to a "campaign is live" confirmation.
395
420
  - **Claude greenlight path** — user replies with an affirmative ("yeah
396
421
  start" / "looks good, start" / "ship it"). The skill then performs,
397
- in order: (a) bulk-approve queued messages via the EXISTING endpoint
422
+ in order: (a) `get_campaign` and verify a sender is attached, (b) if no
423
+ sequence is attached, `attach_recommended_sequence({ campaignId })`, (c)
424
+ bulk-approve queued messages via the EXISTING endpoint
398
425
  `POST /api/v3/workflow-tables/cells/approve-batch` (do NOT invent a
399
- new tool/endpoint), (b) `start_campaign({ campaignId })`, (c)
400
- `update_campaign({ campaignId, currentStep: "running" })`, (d)
426
+ new tool/endpoint), (d) `start_campaign({ campaignId })`, (e)
427
+ `update_campaign({ campaignId, currentStep: "running" })`, (f)
401
428
  re-surface `watchUrl` plus a "campaign is live" confirmation.
402
429
 
403
430
  A Claude greenlight on an already-running campaign is a no-op
@@ -418,6 +445,9 @@ runs.
418
445
  ## Tail Hard Rules
419
446
 
420
447
  - Review-batch sourcing/import happens in Step 13, not during atomic mint.
448
+ - Step 13 materializes/reuses the approved source with `campaignOfferId` before
449
+ import. Do not import a legacy campaignless Signal source until selected posts
450
+ exist on the campaign.
421
451
  - Full-list expansion is not part of the default tail. It requires user
422
452
  approval after the review batch.
423
453
  Import must never exceed `importLimit`.
@@ -434,18 +464,19 @@ runs.
434
464
 
435
465
  <references_index>
436
466
 
437
- | File | Load when |
438
- | ------------------------------------------ | ---------------------------------------------------------------------- |
439
- | `references/approval-gate-framing.md` | Phase 85 commit gate, before showing the approval packet |
440
- | `references/watch-link-handoff.md` | Atomic mint + every tail resume, before surfacing watchUrl |
441
- | `references/sample-validation-loop.md` | Step 14, before enriching + scoring the sample |
442
- | `references/escalation-ladder.md` | Any tail step that needs to decide retry / revise / escalate |
443
- | `references/final-handoff-contract.md` | Step 16, and every Claude greenlight turn |
444
- | `references/parallel-critique-protocol.md` | Step 15 when `messaging.critique.enabled` is true |
445
- | `references/thomas-variant-selection.md` | Step 15 when `messaging.critique.opus.enabled` is true |
446
- | `references/thomas-revision-filters.md` | Any critique rewrite, before persisting |
447
- | `core/auto-execute.yaml` | Start of Step 13, load once; all tail steps read parsed values |
448
- | `core/auto-execute.README.md` | When tuning `auto-execute.yaml` knobs or reviewing threshold-trip logs |
467
+ | File | Load when |
468
+ | ------------------------------------------------ | ------------------------------------------------------------------------- |
469
+ | `references/approval-gate-framing.md` | Phase 85 commit gate, before showing the approval packet |
470
+ | `references/watch-link-handoff.md` | Atomic mint + every tail resume, before surfacing watchUrl |
471
+ | `references/post-mint-source-materialization.md` | Step 13, before importing normal-discovery or legacy campaignless sources |
472
+ | `references/sample-validation-loop.md` | Step 14, before enriching + scoring the sample |
473
+ | `references/escalation-ladder.md` | Any tail step that needs to decide retry / revise / escalate |
474
+ | `references/final-handoff-contract.md` | Step 16, and every Claude greenlight turn |
475
+ | `references/parallel-critique-protocol.md` | Step 15 when `messaging.critique.enabled` is true |
476
+ | `references/thomas-variant-selection.md` | Step 15 when `messaging.critique.opus.enabled` is true |
477
+ | `references/thomas-revision-filters.md` | Any critique rewrite, before persisting |
478
+ | `core/auto-execute.yaml` | Start of Step 13, load once; all tail steps read parsed values |
479
+ | `core/auto-execute.README.md` | When tuning `auto-execute.yaml` knobs or reviewing threshold-trip logs |
449
480
 
450
481
  Load every file in this table with:
451
482