@sellable/mcp 0.1.509 → 0.1.511

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.
@@ -183,7 +183,9 @@ function sanitizeActionCandidate(candidate) {
183
183
  : undefined,
184
184
  activeJobId: stringValue(candidate.activeJobId) ?? null,
185
185
  activeImportStatus: stringValue(candidate.activeImportStatus) ?? null,
186
- targetRows: typeof candidate.targetRows === "number" ? candidate.targetRows : undefined,
186
+ targetRows: typeof candidate.targetRows === "number"
187
+ ? candidate.targetRows
188
+ : undefined,
187
189
  columnRole: stringValue(candidate.columnRole),
188
190
  rowSelector: sanitizeRowSelector(candidate.rowSelector),
189
191
  actionKey: stringValue(candidate.actionKey),
@@ -209,7 +211,9 @@ function sanitizeRowSelector(value) {
209
211
  return {
210
212
  type: value.type,
211
213
  ...(typeof value.limit === "number" ? { limit: value.limit } : {}),
212
- ...(Array.isArray(value.rowIds) ? { rowIds: stringArray(value.rowIds) } : {}),
214
+ ...(Array.isArray(value.rowIds)
215
+ ? { rowIds: stringArray(value.rowIds) }
216
+ : {}),
213
217
  };
214
218
  }
215
219
  function sanitizeToolInput(value) {
@@ -451,11 +455,15 @@ function sanitizeStructuredSenderPlans(value, selectedBySender) {
451
455
  selectedDays: Array.isArray(plan.horizon.selectedDays)
452
456
  ? plan.horizon.selectedDays
453
457
  : [],
458
+ requestedTarget: numberValue(plan.horizon.requestedTarget),
454
459
  grossTarget: numberValue(plan.horizon.grossTarget),
455
460
  schedulerFillableSlots: typeof plan.horizon.schedulerFillableSlots === "number"
456
461
  ? plan.horizon.schedulerFillableSlots
457
462
  : null,
458
463
  schedulerFillableSlotsKnown: plan.horizon.schedulerFillableSlotsKnown === true,
464
+ schedulerCapacityTarget: typeof plan.horizon.schedulerCapacityTarget === "number"
465
+ ? plan.horizon.schedulerCapacityTarget
466
+ : null,
459
467
  capacityConfidence: stringValue(plan.horizon.capacityConfidence) ?? "unknown",
460
468
  schedulerCapacityMode: stringValue(plan.horizon.schedulerCapacityMode),
461
469
  }
@@ -517,7 +525,9 @@ function emptyUnsupportedResult(result) {
517
525
  eligibleSenderLedger: [],
518
526
  senderRefillPlans: [],
519
527
  globalActionQueue: [],
528
+ requestedTarget: 0,
520
529
  grossTarget: 0,
530
+ effectiveTarget: 0,
521
531
  sent: 0,
522
532
  scheduled: 0,
523
533
  projected: 0,
@@ -564,7 +574,7 @@ function sanitizeBlockers(blockers, selectedKeys) {
564
574
  if (!actionType)
565
575
  return true;
566
576
  const senderId = parts[0];
567
- return selectedKeys.size === 0 || selectedKeys.has(`${senderId}:${actionType}`);
577
+ return (selectedKeys.size === 0 || selectedKeys.has(`${senderId}:${actionType}`));
568
578
  });
569
579
  }
570
580
  function sanitizeRefillTargetPlanResult(result) {
@@ -626,6 +636,13 @@ function sanitizeRefillTargetPlanResult(result) {
626
636
  };
627
637
  });
628
638
  const grossTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.grossTarget), 0);
639
+ const requestedTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.requestedTarget), 0);
640
+ const effectiveTarget = senderPlans.reduce((sum, plan) => {
641
+ const target = typeof plan.schedulerCapacityTarget === "number"
642
+ ? plan.schedulerCapacityTarget
643
+ : numberValue(plan.grossTarget);
644
+ return sum + target;
645
+ }, 0);
629
646
  const sent = senderPlans.reduce((sum, plan) => sum + numberValue(plan.sent), 0);
630
647
  const scheduled = senderPlans.reduce((sum, plan) => sum + numberValue(plan.scheduled), 0);
631
648
  const projected = sent + scheduled;
@@ -662,7 +679,9 @@ function sanitizeRefillTargetPlanResult(result) {
662
679
  eligibleSenderLedger,
663
680
  senderRefillPlans,
664
681
  globalActionQueue,
682
+ requestedTarget,
665
683
  grossTarget,
684
+ effectiveTarget,
666
685
  sent,
667
686
  scheduled,
668
687
  projected,
@@ -1079,7 +1079,6 @@ export declare const allTools: ({
1079
1079
  useMessagingTemplate?: undefined;
1080
1080
  rubric?: undefined;
1081
1081
  flowVersion?: undefined;
1082
- workspaceId?: undefined;
1083
1082
  };
1084
1083
  required: string[];
1085
1084
  additionalProperties: boolean;
@@ -1118,7 +1117,6 @@ export declare const allTools: ({
1118
1117
  useMessagingTemplate?: undefined;
1119
1118
  rubric?: undefined;
1120
1119
  flowVersion?: undefined;
1121
- workspaceId?: undefined;
1122
1120
  };
1123
1121
  required: never[];
1124
1122
  additionalProperties?: undefined;
@@ -1157,7 +1155,6 @@ export declare const allTools: ({
1157
1155
  useMessagingTemplate?: undefined;
1158
1156
  rubric?: undefined;
1159
1157
  flowVersion?: undefined;
1160
- workspaceId?: undefined;
1161
1158
  };
1162
1159
  required: string[];
1163
1160
  additionalProperties?: undefined;
@@ -1239,7 +1236,6 @@ export declare const allTools: ({
1239
1236
  useMessagingTemplate?: undefined;
1240
1237
  rubric?: undefined;
1241
1238
  flowVersion?: undefined;
1242
- workspaceId?: undefined;
1243
1239
  };
1244
1240
  required: string[];
1245
1241
  additionalProperties: boolean;
@@ -1434,7 +1430,6 @@ export declare const allTools: ({
1434
1430
  useMessagingTemplate?: undefined;
1435
1431
  rubric?: undefined;
1436
1432
  flowVersion?: undefined;
1437
- workspaceId?: undefined;
1438
1433
  };
1439
1434
  required: never[];
1440
1435
  additionalProperties?: undefined;
@@ -1640,49 +1635,6 @@ export declare const allTools: ({
1640
1635
  clientProspectId?: undefined;
1641
1636
  senderLinkedinUrl?: undefined;
1642
1637
  messageGenerationMode?: undefined;
1643
- workspaceId?: undefined;
1644
- };
1645
- required: string[];
1646
- additionalProperties?: undefined;
1647
- };
1648
- } | {
1649
- name: string;
1650
- description: string;
1651
- inputSchema: {
1652
- type: string;
1653
- properties: {
1654
- campaignId: {
1655
- type: string;
1656
- description: string;
1657
- };
1658
- workspaceId: {
1659
- type: string;
1660
- description: string;
1661
- };
1662
- limit?: undefined;
1663
- tableId?: undefined;
1664
- leadLimit?: undefined;
1665
- page?: undefined;
1666
- filters?: undefined;
1667
- name?: undefined;
1668
- clientProspectId?: undefined;
1669
- senderLinkedinUrl?: undefined;
1670
- offerPositioning?: undefined;
1671
- campaignBrief?: undefined;
1672
- messageGenerationMode?: undefined;
1673
- currentStep?: undefined;
1674
- watchNarration?: undefined;
1675
- leadSourceType?: undefined;
1676
- leadSourceProvider?: undefined;
1677
- selectedLeadListId?: undefined;
1678
- senderIds?: undefined;
1679
- currentStepTransition?: undefined;
1680
- clearCurrentStepIfMatches?: undefined;
1681
- interactionMode?: undefined;
1682
- enableICPFilters?: undefined;
1683
- useMessagingTemplate?: undefined;
1684
- rubric?: undefined;
1685
- flowVersion?: undefined;
1686
1638
  };
1687
1639
  required: string[];
1688
1640
  additionalProperties?: undefined;
@@ -1724,7 +1676,6 @@ export declare const allTools: ({
1724
1676
  useMessagingTemplate?: undefined;
1725
1677
  rubric?: undefined;
1726
1678
  flowVersion?: undefined;
1727
- workspaceId?: undefined;
1728
1679
  };
1729
1680
  required: string[];
1730
1681
  additionalProperties?: undefined;
@@ -34,7 +34,7 @@ export type SenderDetailResponse = {
34
34
  };
35
35
  export type RefreshPaidInmailCreditsInput = {
36
36
  senderId: string;
37
- workspaceId?: string;
37
+ workspaceId: string;
38
38
  };
39
39
  export type PaidInmailCreditStatus = {
40
40
  available: number;
@@ -47,6 +47,7 @@ export type RefreshPaidInmailCreditsResponse = {
47
47
  senderId: string;
48
48
  refreshed: true;
49
49
  credits: PaidInmailCreditStatus;
50
+ receipt?: unknown;
50
51
  sideEffects: {
51
52
  refreshedLinkedInDerivedCreditFacts: true;
52
53
  updatedSenderCreditCache: true;
@@ -136,20 +136,20 @@ export const senderToolDefinitions = [
136
136
  },
137
137
  {
138
138
  name: "refresh_paid_inmail_credits",
139
- description: "Refresh the cached paid InMail credit facts for one exact sender in the active workspace by calling Sellable's product credit refresh path. This updates only the sender credit cache (balance, sentSince, checkedAt) and returns the fresh status. It does not mutate campaigns, lower thresholds, approve messages, schedule sends, start campaigns, send messages, spend InMail credits, or write scheduler fields. refill_sends --yolo normally handles get_refill_target_plan refresh_paid_inmail_credits candidates internally; use this standalone tool for manual diagnostics or explicit one-off refreshes, then rerun get_refill_target_plan before any refill mutation.",
139
+ description: "Refresh the cached paid InMail credit facts for one exact sender in the explicit request-scoped workspace by calling Sellable's scoped MCP credit refresh path. This updates only the sender credit cache/attempt metadata (balance, sentSince, checkedAt, last attempted/error/lease) and returns the sanitized coordinator receipt. It does not mutate campaigns, lower thresholds, approve messages, schedule sends, start campaigns, send messages, spend InMail credits, or write scheduler fields. refill_sends --yolo normally handles get_refill_target_plan refresh_paid_inmail_credits candidates internally; use this standalone tool for manual diagnostics or explicit one-off refreshes, then rerun get_refill_target_plan before any refill mutation.",
140
140
  inputSchema: {
141
141
  type: "object",
142
142
  properties: {
143
143
  senderId: {
144
144
  type: "string",
145
- description: "Exact Sender.id to refresh. The API verifies the sender belongs to the active workspace.",
145
+ description: "Exact Sender.id to refresh. The API verifies the sender belongs to the explicit workspace.",
146
146
  },
147
147
  workspaceId: {
148
148
  type: "string",
149
149
  description: "Explicit request-scoped workspace id for scheduled/yolo refill automation. Pass this instead of switching the shared active workspace.",
150
150
  },
151
151
  },
152
- required: ["senderId"],
152
+ required: ["senderId", "workspaceId"],
153
153
  additionalProperties: false,
154
154
  },
155
155
  },
@@ -209,15 +209,17 @@ export async function refreshPaidInmailCredits(input) {
209
209
  }
210
210
  const api = getApi();
211
211
  const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
212
+ if (!workspaceId) {
213
+ throw new Error("workspaceId is required.");
214
+ }
212
215
  const requestOptions = workspaceRequestOptions(workspaceId);
213
- const body = workspaceId ? { workspaceId } : {};
214
- const result = requestOptions
215
- ? await api.post(`/api/v3/senders/${encodeURIComponent(senderId)}/refresh-inmail-credits`, body, requestOptions)
216
- : await api.post(`/api/v3/senders/${encodeURIComponent(senderId)}/refresh-inmail-credits`, body);
216
+ const body = { workspaceId };
217
+ const result = await api.post(`/api/v3/mcp/senders/${encodeURIComponent(senderId)}/refresh-inmail-credits`, body, requestOptions);
217
218
  return {
218
219
  senderId,
219
220
  refreshed: true,
220
221
  credits: normalizeCreditStatus(result?.credits),
222
+ receipt: result?.receipt,
221
223
  sideEffects: {
222
224
  refreshedLinkedInDerivedCreditFacts: true,
223
225
  updatedSenderCreditCache: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.509",
3
+ "version": "0.1.511",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - mcp__sellable__get_refill_target_plan
8
8
  - mcp__sellable__get_scheduler_fill_capacity
9
9
  - mcp__sellable__refresh_paid_inmail_credits
10
+ - mcp__sellable__get_subskill_asset
10
11
  - mcp__sellable__get_auth_status
11
12
  - mcp__sellable__start_cli_login
12
13
  - mcp__sellable__wait_for_cli_login
@@ -151,19 +152,23 @@ explicitly stops/statuses the run, or a concrete non-scheduler blocker appears.
151
152
  Do not call the goal complete or blocked only because the current state is
152
153
  `awaiting_scheduler_after_ready_buffer`; treat it as loaded, awaiting scheduler.
153
154
 
154
- Load the internal workflow prompt before taking any operational step:
155
+ Load the internal workflow prompt and deterministic flow asset before taking
156
+ any operational step:
155
157
 
156
158
  ```text
157
159
  get_subskill_prompt({ subskillName: "refill-sends-workflow" })
160
+ get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flow.v1.json" })
158
161
  ```
159
162
 
160
- Then follow that workflow exactly. The default path is read-only research:
161
- compute the refill target plan first, resolve the route, identify the
162
- sender-relevant campaign that most recently had scheduler-owned sends, read
163
- refill state for that target, report the next safe step using campaign names
164
- first, and stop before mutation unless the user has explicitly approved the
165
- exact workspace, campaign/table/source ids, caps/dates, approval mode, expected
166
- side effects, and stop/rollback condition.
163
+ Continue both loads until `hasMore:false`; parse the flow JSON and verify
164
+ `workflow:"refill-sends-workflow"` with a `v1` version. Then follow that
165
+ workflow exactly. The default path is read-only research: compute the refill
166
+ target plan first, resolve the route, identify the sender-relevant campaign
167
+ that most recently had scheduler-owned sends, read refill state for that target,
168
+ report the next safe step using campaign names first, and stop before mutation
169
+ unless the user has explicitly approved the exact workspace,
170
+ campaign/table/source ids, caps/dates, approval mode, expected side effects,
171
+ and stop/rollback condition.
167
172
 
168
173
  Immediately after loading the internal workflow, call
169
174
  `get_refill_target_plan`. The target plan is the canonical first fact receipt:
@@ -223,6 +228,20 @@ threshold, below-threshold paid-InMail facts fall back to an existing connection
223
228
  lane, the same Sales Nav cascade campaign's connection branch, or a manual
224
229
  continuation.
225
230
 
231
+ Freshness gate precedes scheduler wait: if any selected
232
+ `target.senderRefillPlans[].paidInmail.status` is `missing_credit_facts` or
233
+ `stale_credit_facts`, or the target plan contains a
234
+ `refresh_paid_inmail_credits` candidate, do not enter `wait_for_scheduler` even
235
+ when `remainingReadyOrProjectedGap:0`. Refresh the exact selected sender credit
236
+ facts once, rerun `get_refill_target_plan`, and only then decide whether
237
+ scheduler wait is the next safe action. If facts remain missing/stale after the
238
+ single refresh attempt, stop with a paid-InMail freshness blocker instead of
239
+ waiting on scheduler pickup.
240
+ The standalone MCP refresh surface is the scoped route
241
+ `/api/v3/mcp/senders/:senderId/refresh-inmail-credits` with explicit
242
+ `workspaceId`; do not use active-workspace mutation as the automation control
243
+ path.
244
+
226
245
  Compact refill lessons: sender-level target plan is final truth; trust
227
246
  `schedulerGate.sendable` and scheduler gate blockers, not raw
228
247
  `unipileAccountStatus` labels alone; use compact prep status checks for
@@ -239,8 +258,9 @@ will try to place and does not import, approve, schedule, refresh credits, or
239
258
  mutate.
240
259
  If the target plan is complete by projected coverage, report that the selected
241
260
  target is already filled and no-op without asking for approval. If the ready
242
- buffer covers the projected gap but scheduled coverage is still short, keep the
243
- run open in a persistent read-only scheduler wait loop. Poll
261
+ buffer covers the projected gap, paid InMail credit facts are fresh for every
262
+ selected paid-InMail lane, but scheduled coverage is still short, keep the run
263
+ open in a persistent read-only scheduler wait loop. Poll
244
264
  `get_refill_target_plan` every 60-120 seconds, or on the host's next continuation
245
265
  interval, until projected coverage fills the target, a concrete non-scheduler
246
266
  blocker appears, or Christian explicitly asks to stop or only receive a status
@@ -3,6 +3,7 @@ name: refill-sends-workflow
3
3
  description: Internal skill-led refill sends workflow for regular campaigns and evergreen campaigns.
4
4
  visibility: internal
5
5
  allowed-tools:
6
+ - mcp__sellable__get_subskill_asset
6
7
  - mcp__sellable__get_refill_target_plan
7
8
  - mcp__sellable__get_scheduler_fill_capacity
8
9
  - mcp__sellable__refresh_paid_inmail_credits
@@ -79,6 +80,16 @@ open until every selected sender lane is horizon-filled by projected coverage
79
80
  non-scheduler blocker appears. `awaiting_scheduler_after_ready_buffer` is an
80
81
  in-progress wait state, not a reason to mark the goal complete or blocked.
81
82
 
83
+ Runtime flow asset: after loading this prompt, load
84
+ `get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flow.v1.json" })`
85
+ through MCP, continue chunks until `hasMore:false`, parse the JSON, and verify
86
+ `workflow:"refill-sends-workflow"` with a `v1` version before any operational
87
+ step. The flow asset is the deterministic gate order: target-plan read, paid
88
+ InMail credit freshness gate, action selection, bounded table action, scheduler
89
+ wait readback, then completion/blocker. If the asset cannot load or parse, stop
90
+ with `blocked:refill_workflow_asset_unavailable`; do not emulate it from local
91
+ files or memory.
92
+
82
93
  1. Call `get_refill_target_plan` first. This read-only target plan is the
83
94
  canonical opening receipt: eligible senders, selected sender-local days,
84
95
  gross target, inferred per-sender send lane/action selections, actual sent
@@ -136,6 +147,19 @@ in-progress wait state, not a reason to mark the goal complete or blocked.
136
147
  threshold, below-threshold paid-InMail facts fall back to an existing
137
148
  connection lane, the same Sales Nav cascade campaign's connection branch, or
138
149
  a manual continuation.
150
+ Freshness gate precedes scheduler wait: if any selected
151
+ `target.senderRefillPlans[].paidInmail.status` is `missing_credit_facts` or
152
+ `stale_credit_facts`, or the target plan contains a
153
+ `refresh_paid_inmail_credits` candidate, do not enter
154
+ `wait_for_scheduler` even when `remainingReadyOrProjectedGap:0`. Refresh the
155
+ exact selected sender credit facts once, rerun `get_refill_target_plan`, and
156
+ only then decide whether scheduler wait is the next safe action. If facts
157
+ remain missing/stale after the single refresh attempt, stop with a
158
+ paid-InMail freshness blocker instead of waiting on scheduler pickup.
159
+ The standalone MCP refresh surface is the scoped route
160
+ `/api/v3/mcp/senders/:senderId/refresh-inmail-credits` with explicit
161
+ `workspaceId`; do not use active-workspace mutation as the automation
162
+ control path.
139
163
  Compact refill lessons: sender-level target plan is final truth; trust
140
164
  `schedulerGate.sendable` and scheduler gate blockers, not raw
141
165
  `unipileAccountStatus` labels alone; use compact prep status checks for
@@ -154,8 +178,9 @@ in-progress wait state, not a reason to mark the goal complete or blocked.
154
178
  If `status:"complete"`, report the target, selected dates, sent count,
155
179
  scheduled count, projected count, campaign ids, and no-op proof without
156
180
  asking for approval or mutating.
157
- If `remainingReadyOrProjectedGap:0` but `remainingProjectedGap>0`, run only a
158
- persistent read-only scheduler wait/reread loop; do not ask for
181
+ If `remainingReadyOrProjectedGap:0` but `remainingProjectedGap>0`, and paid
182
+ InMail credit freshness is clean for every selected paid-InMail lane, run
183
+ only a persistent read-only scheduler wait/reread loop; do not ask for
159
184
  prep/import/approval. Poll `get_refill_target_plan` every 60-120 seconds, or
160
185
  on the host's next continuation interval, until projected coverage fills,
161
186
  a concrete non-scheduler blocker appears, or Christian explicitly asks to
@@ -0,0 +1,281 @@
1
+ {
2
+ "version": "v1.0",
3
+ "workflow": "refill-sends-workflow",
4
+ "principle": "Use the target planner and scheduler readback as canonical facts. Refresh stale paid-InMail credit facts before scheduler-wait decisions; do not change scheduler allocation logic.",
5
+ "requiredBootstrap": [
6
+ {
7
+ "tool": "get_subskill_prompt",
8
+ "requiredValues": {
9
+ "subskillName": "refill-sends-workflow"
10
+ },
11
+ "mustReadUntil": {
12
+ "hasMore": false
13
+ }
14
+ },
15
+ {
16
+ "tool": "get_subskill_asset",
17
+ "requiredValues": {
18
+ "subskillName": "refill-sends-workflow",
19
+ "assetPath": "core/flow.v1.json"
20
+ },
21
+ "mustReadUntil": {
22
+ "hasMore": false
23
+ },
24
+ "validation": {
25
+ "workflow": "refill-sends-workflow",
26
+ "versionPrefix": "v1",
27
+ "requiredTopLevelKeys": [
28
+ "states",
29
+ "gateOrder",
30
+ "yoloMode"
31
+ ]
32
+ }
33
+ }
34
+ ],
35
+ "gateOrder": [
36
+ "target_plan_read",
37
+ "paid_inmail_credit_freshness_gate",
38
+ "target_action_selection",
39
+ "bounded_table_action",
40
+ "scheduler_wait_readback",
41
+ "completion_or_blocker"
42
+ ],
43
+ "sharedFacts": [
44
+ "workspaceId",
45
+ "senderId",
46
+ "campaignId",
47
+ "tableId",
48
+ "columnId",
49
+ "actionType",
50
+ "targetDate",
51
+ "untilDate",
52
+ "targetShapeRevision",
53
+ "stateRevision",
54
+ "paidInmail.status",
55
+ "paidInmail.available",
56
+ "paidInmail.threshold",
57
+ "paidInmail.maxStalenessSeconds",
58
+ "remainingProjectedGap",
59
+ "remainingReadyOrProjectedGap",
60
+ "readyToSchedule",
61
+ "scheduled",
62
+ "projected"
63
+ ],
64
+ "states": [
65
+ {
66
+ "id": "target_plan_read",
67
+ "description": "Read the canonical target plan before any mutation or wait loop.",
68
+ "allowedTools": [
69
+ "get_refill_target_plan"
70
+ ],
71
+ "requiredOutput": [
72
+ "target.eligibleSenderLedger",
73
+ "target.senderRefillPlans",
74
+ "target.globalActionQueue",
75
+ "targetShapeRevision",
76
+ "stateRevision"
77
+ ],
78
+ "transitions": [
79
+ {
80
+ "when": "status == complete",
81
+ "to": "completion_or_blocker"
82
+ },
83
+ {
84
+ "when": "any selected senderRefillPlans[].paidInmail.status is missing_credit_facts or stale_credit_facts",
85
+ "to": "paid_inmail_credit_freshness_gate"
86
+ },
87
+ {
88
+ "when": "globalActionQueue or actionCandidates contains refresh_paid_inmail_credits",
89
+ "to": "paid_inmail_credit_freshness_gate"
90
+ },
91
+ {
92
+ "otherwise": "target_action_selection"
93
+ }
94
+ ]
95
+ },
96
+ {
97
+ "id": "paid_inmail_credit_freshness_gate",
98
+ "description": "Make paid-InMail credit freshness deterministic before trusting scheduler wait decisions.",
99
+ "allowedTools": [
100
+ "refresh_paid_inmail_credits",
101
+ "get_refill_target_plan"
102
+ ],
103
+ "rules": [
104
+ "This gate precedes scheduler_wait_readback, even when remainingReadyOrProjectedGap is 0.",
105
+ "Refresh only exact sender ids present in the rendered target packet.",
106
+ "Refresh each selected sender at most once per refill_sends command call.",
107
+ "The refresh receipt must include approvalSource, workspaceId, senderId, actionKey, campaignId, tableId, columnId, threshold, maxStalenessSeconds, and targetPlanFingerprint.",
108
+ "After refresh, rerun get_refill_target_plan with the same workspaceId and selectors before choosing a prep, approval, source-copy, or scheduler-wait action.",
109
+ "If facts remain missing or stale after the single refresh attempt, stop with paid_inmail_credit_freshness_blocked rather than entering scheduler wait.",
110
+ "If fresh facts are below threshold, report the exact threshold blocker or same-campaign connection fallback; do not lower thresholds in yolo mode."
111
+ ],
112
+ "disallowedBeforeReread": [
113
+ "wait_for_scheduler",
114
+ "start_campaign_message_preparation",
115
+ "confirm_lead_list",
116
+ "select_campaign_cells",
117
+ "queue_campaign_cells",
118
+ "start_campaign"
119
+ ],
120
+ "transitions": [
121
+ {
122
+ "when": "post_refresh_target_plan has no missing/stale paid-InMail facts for selected lanes",
123
+ "to": "target_action_selection"
124
+ },
125
+ {
126
+ "when": "post_refresh_target_plan still has missing/stale paid-InMail facts",
127
+ "to": "completion_or_blocker"
128
+ }
129
+ ]
130
+ },
131
+ {
132
+ "id": "target_action_selection",
133
+ "description": "Choose only the globally ranked planner primitive after freshness gates pass.",
134
+ "allowedTools": [
135
+ "get_refill_target_plan",
136
+ "get_campaign_refill_state",
137
+ "get_scheduler_fill_capacity"
138
+ ],
139
+ "rules": [
140
+ "Use target.globalActionQueue[0] as the only cross-sender yolo action.",
141
+ "manualAlternates are not yolo actions.",
142
+ "If projected coverage is complete, no-op.",
143
+ "If remainingReadyOrProjectedGap is 0 and remainingProjectedGap is positive, scheduler wait is allowed only after paid_inmail_credit_freshness_gate is clean.",
144
+ "If ready buffer is short, choose bounded existing-row prep, bounded approval, same-source copy, or provider-aligned source-more according to the planner."
145
+ ],
146
+ "transitions": [
147
+ {
148
+ "when": "next action is wait_for_scheduler and paid freshness gate is clean",
149
+ "to": "scheduler_wait_readback"
150
+ },
151
+ {
152
+ "when": "next action is a bounded table/source/prep/approval primitive",
153
+ "to": "bounded_table_action"
154
+ },
155
+ {
156
+ "when": "no safe action remains or non-scheduler blocker exists",
157
+ "to": "completion_or_blocker"
158
+ }
159
+ ]
160
+ },
161
+ {
162
+ "id": "bounded_table_action",
163
+ "description": "Run one safe primitive, then reread the target plan.",
164
+ "allowedTools": [
165
+ "start_campaign_message_preparation",
166
+ "get_campaign_message_preparation_status",
167
+ "import_leads",
168
+ "wait_for_lead_list_ready",
169
+ "confirm_lead_list",
170
+ "select_campaign_cells",
171
+ "queue_campaign_cells",
172
+ "wait_for_campaign_processing",
173
+ "get_refill_target_plan"
174
+ ],
175
+ "rules": [
176
+ "One primitive per loop.",
177
+ "No broad approval.",
178
+ "No scheduler writes.",
179
+ "Reread get_refill_target_plan before any second primitive.",
180
+ "Continue while targetShapeRevision is stable and projected coverage progresses."
181
+ ],
182
+ "transitions": [
183
+ {
184
+ "after": "terminal primitive result and target reread",
185
+ "to": "target_plan_read"
186
+ }
187
+ ]
188
+ },
189
+ {
190
+ "id": "scheduler_wait_readback",
191
+ "description": "Read-only scheduler polling after rows are ready and paid-credit facts are fresh.",
192
+ "allowedTools": [
193
+ "get_refill_target_plan",
194
+ "get_scheduler_fill_capacity"
195
+ ],
196
+ "entryConditions": [
197
+ "remainingReadyOrProjectedGap == 0",
198
+ "remainingProjectedGap > 0",
199
+ "no selected paid-InMail lane has missing_credit_facts or stale_credit_facts",
200
+ "no refresh_paid_inmail_credits action remains in target.globalActionQueue or actionCandidates"
201
+ ],
202
+ "rules": [
203
+ "Poll every 60-120 seconds or on the host continuation interval.",
204
+ "Stop waiting when projected coverage fills the target.",
205
+ "Leave wait if a concrete non-scheduler blocker appears.",
206
+ "Do not mark complete or blocked only because awaiting_scheduler_after_ready_buffer persists."
207
+ ],
208
+ "transitions": [
209
+ {
210
+ "when": "projected coverage fills target",
211
+ "to": "completion_or_blocker"
212
+ },
213
+ {
214
+ "when": "paid facts become missing/stale again",
215
+ "to": "paid_inmail_credit_freshness_gate"
216
+ },
217
+ {
218
+ "when": "ready buffer no longer covers gap",
219
+ "to": "target_action_selection"
220
+ }
221
+ ]
222
+ },
223
+ {
224
+ "id": "completion_or_blocker",
225
+ "description": "Report exact no-op, completion proof, or blocker with ids and revisions.",
226
+ "allowedTools": [
227
+ "get_refill_target_plan",
228
+ "get_campaign_refill_state",
229
+ "get_scheduler_fill_capacity"
230
+ ],
231
+ "terminal": true
232
+ }
233
+ ],
234
+ "yoloMode": {
235
+ "approvalSource": "explicit_yolo_flag",
236
+ "autoExecutableActions": [
237
+ "refresh_paid_inmail_credits",
238
+ "start_campaign_message_preparation",
239
+ "same_source_copy",
240
+ "bounded_generated_message_approval",
241
+ "wait_for_scheduler"
242
+ ],
243
+ "neverAutoExecute": [
244
+ "lower_paid_inmail_threshold",
245
+ "create_campaign",
246
+ "create_connection_campaign",
247
+ "switch_source_family",
248
+ "direct_scheduler_write",
249
+ "start_unselected_campaign",
250
+ "archive_or_delete"
251
+ ],
252
+ "driftStops": [
253
+ "workspaceId",
254
+ "sender set",
255
+ "campaignId",
256
+ "tableId",
257
+ "columnId",
258
+ "actionType",
259
+ "targetDate",
260
+ "untilDate",
261
+ "targetShapeRevision",
262
+ "paid-InMail threshold feasibility",
263
+ "side-effect class"
264
+ ]
265
+ },
266
+ "verification": {
267
+ "requiredProof": [
268
+ "final get_refill_target_plan",
269
+ "projected coverage sent + scheduled",
270
+ "scheduler-owned cells with non-null scheduledFor when claiming scheduled",
271
+ "paid-InMail freshness status clean before scheduler wait",
272
+ "refresh receipt for each refreshed sender"
273
+ ],
274
+ "notCompletionProof": [
275
+ "prepared rows alone",
276
+ "approved rows alone",
277
+ "ready-to-schedule rows alone",
278
+ "awaiting_scheduler_after_ready_buffer"
279
+ ]
280
+ }
281
+ }