@sellable/mcp 0.1.147 → 0.1.148

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/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -3702,6 +3702,7 @@ export declare const allTools: ({
3702
3702
  tableId?: undefined;
3703
3703
  targetCount?: undefined;
3704
3704
  minPassedCount?: undefined;
3705
+ minMessagesCount?: undefined;
3705
3706
  timeoutMs?: undefined;
3706
3707
  intervalMs?: undefined;
3707
3708
  includeRows?: undefined;
@@ -3735,6 +3736,7 @@ export declare const allTools: ({
3735
3736
  tableId?: undefined;
3736
3737
  targetCount?: undefined;
3737
3738
  minPassedCount?: undefined;
3739
+ minMessagesCount?: undefined;
3738
3740
  timeoutMs?: undefined;
3739
3741
  intervalMs?: undefined;
3740
3742
  includeRows?: undefined;
@@ -3793,6 +3795,7 @@ export declare const allTools: ({
3793
3795
  tableId?: undefined;
3794
3796
  targetCount?: undefined;
3795
3797
  minPassedCount?: undefined;
3798
+ minMessagesCount?: undefined;
3796
3799
  timeoutMs?: undefined;
3797
3800
  intervalMs?: undefined;
3798
3801
  includeRows?: undefined;
@@ -3851,6 +3854,7 @@ export declare const allTools: ({
3851
3854
  tableId?: undefined;
3852
3855
  targetCount?: undefined;
3853
3856
  minPassedCount?: undefined;
3857
+ minMessagesCount?: undefined;
3854
3858
  timeoutMs?: undefined;
3855
3859
  intervalMs?: undefined;
3856
3860
  includeRows?: undefined;
@@ -3911,6 +3915,7 @@ export declare const allTools: ({
3911
3915
  tableId?: undefined;
3912
3916
  targetCount?: undefined;
3913
3917
  minPassedCount?: undefined;
3918
+ minMessagesCount?: undefined;
3914
3919
  timeoutMs?: undefined;
3915
3920
  intervalMs?: undefined;
3916
3921
  includeRows?: undefined;
@@ -3941,6 +3946,7 @@ export declare const allTools: ({
3941
3946
  tableId?: undefined;
3942
3947
  targetCount?: undefined;
3943
3948
  minPassedCount?: undefined;
3949
+ minMessagesCount?: undefined;
3944
3950
  timeoutMs?: undefined;
3945
3951
  intervalMs?: undefined;
3946
3952
  includeRows?: undefined;
@@ -3971,6 +3977,7 @@ export declare const allTools: ({
3971
3977
  tableId?: undefined;
3972
3978
  targetCount?: undefined;
3973
3979
  minPassedCount?: undefined;
3980
+ minMessagesCount?: undefined;
3974
3981
  timeoutMs?: undefined;
3975
3982
  intervalMs?: undefined;
3976
3983
  includeRows?: undefined;
@@ -33,6 +33,7 @@ type WaitForRubricResultsInput = {
33
33
  tableId?: string;
34
34
  targetCount?: number;
35
35
  minPassedCount?: number;
36
+ minMessagesCount?: number;
36
37
  timeoutMs?: number;
37
38
  intervalMs?: number;
38
39
  includeRows?: boolean;
@@ -114,6 +115,7 @@ export declare const rubricToolDefinitions: ({
114
115
  tableId?: undefined;
115
116
  targetCount?: undefined;
116
117
  minPassedCount?: undefined;
118
+ minMessagesCount?: undefined;
117
119
  timeoutMs?: undefined;
118
120
  intervalMs?: undefined;
119
121
  includeRows?: undefined;
@@ -147,6 +149,7 @@ export declare const rubricToolDefinitions: ({
147
149
  tableId?: undefined;
148
150
  targetCount?: undefined;
149
151
  minPassedCount?: undefined;
152
+ minMessagesCount?: undefined;
150
153
  timeoutMs?: undefined;
151
154
  intervalMs?: undefined;
152
155
  includeRows?: undefined;
@@ -205,6 +208,7 @@ export declare const rubricToolDefinitions: ({
205
208
  tableId?: undefined;
206
209
  targetCount?: undefined;
207
210
  minPassedCount?: undefined;
211
+ minMessagesCount?: undefined;
208
212
  timeoutMs?: undefined;
209
213
  intervalMs?: undefined;
210
214
  includeRows?: undefined;
@@ -263,6 +267,7 @@ export declare const rubricToolDefinitions: ({
263
267
  tableId?: undefined;
264
268
  targetCount?: undefined;
265
269
  minPassedCount?: undefined;
270
+ minMessagesCount?: undefined;
266
271
  timeoutMs?: undefined;
267
272
  intervalMs?: undefined;
268
273
  includeRows?: undefined;
@@ -323,6 +328,7 @@ export declare const rubricToolDefinitions: ({
323
328
  tableId?: undefined;
324
329
  targetCount?: undefined;
325
330
  minPassedCount?: undefined;
331
+ minMessagesCount?: undefined;
326
332
  timeoutMs?: undefined;
327
333
  intervalMs?: undefined;
328
334
  includeRows?: undefined;
@@ -353,6 +359,7 @@ export declare const rubricToolDefinitions: ({
353
359
  tableId?: undefined;
354
360
  targetCount?: undefined;
355
361
  minPassedCount?: undefined;
362
+ minMessagesCount?: undefined;
356
363
  timeoutMs?: undefined;
357
364
  intervalMs?: undefined;
358
365
  includeRows?: undefined;
@@ -383,6 +390,7 @@ export declare const rubricToolDefinitions: ({
383
390
  tableId?: undefined;
384
391
  targetCount?: undefined;
385
392
  minPassedCount?: undefined;
393
+ minMessagesCount?: undefined;
386
394
  timeoutMs?: undefined;
387
395
  intervalMs?: undefined;
388
396
  includeRows?: undefined;
@@ -412,6 +420,10 @@ export declare const rubricToolDefinitions: ({
412
420
  type: string;
413
421
  description: string;
414
422
  };
423
+ minMessagesCount: {
424
+ type: string;
425
+ description: string;
426
+ };
415
427
  timeoutMs: {
416
428
  type: string;
417
429
  description: string;
@@ -508,6 +520,12 @@ export declare function checkRubric(input: CheckRubricInput): Promise<{
508
520
  export declare function waitForRubricResults(input: WaitForRubricResultsInput): Promise<{
509
521
  stats: WorkflowTableStats;
510
522
  rows?: import("./rows.js").LightweightRow[] | undefined;
523
+ messageGeneration?: {
524
+ completed: number;
525
+ passingGeneratedMessages: number;
526
+ minMessagesCount: number;
527
+ floorMet: boolean;
528
+ } | undefined;
511
529
  ready: boolean;
512
530
  attempts: number;
513
531
  elapsedMs: number;
@@ -520,28 +538,14 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
520
538
  targetCount: number;
521
539
  pending?: undefined;
522
540
  };
523
- reason?: undefined;
524
- partialResult?: undefined;
525
- diagnostic?: undefined;
526
- guidance?: undefined;
527
541
  } | {
528
542
  stats: WorkflowTableStats;
529
543
  rows?: import("./rows.js").LightweightRow[] | undefined;
530
- ready: boolean;
531
- partial: boolean;
532
- reason: string;
533
- attempts: number;
534
- elapsedMs: number;
535
- tableId: string;
536
- passRate: {
537
- completed: number;
538
- passed: number;
539
- pending: number;
540
- percent: number;
541
- targetCount: number;
542
- minPassedCount: number;
543
- };
544
544
  partialResult: {
545
+ messagesCount?: number | undefined;
546
+ passingGeneratedMessages?: number | undefined;
547
+ minMessagesCount?: number | undefined;
548
+ messageFloorMet?: boolean | undefined;
545
549
  completed: number;
546
550
  passed: number;
547
551
  pending: number;
@@ -551,23 +555,32 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
551
555
  enoughToDiagnose: boolean;
552
556
  floorMet: boolean;
553
557
  };
554
- diagnostic?: undefined;
555
- guidance?: undefined;
556
- } | {
558
+ messageGeneration?: {
559
+ completed: number;
560
+ passingGeneratedMessages: number;
561
+ minMessagesCount: number;
562
+ floorMet: boolean;
563
+ } | undefined;
557
564
  ready: boolean;
565
+ partial: boolean;
558
566
  reason: string;
559
567
  attempts: number;
560
568
  elapsedMs: number;
561
569
  tableId: string;
562
570
  passRate: {
563
- minPassedCount?: number | undefined;
564
571
  completed: number;
565
572
  passed: number;
566
573
  pending: number;
567
574
  percent: number;
568
575
  targetCount: number;
576
+ minPassedCount: number;
569
577
  };
578
+ } | {
570
579
  partialResult: {
580
+ messagesCount?: number | undefined;
581
+ passingGeneratedMessages?: number | undefined;
582
+ minMessagesCount?: number | undefined;
583
+ messageFloorMet?: boolean | undefined;
571
584
  enoughToDiagnose: boolean;
572
585
  floorMet: boolean;
573
586
  minPassedCount?: number | undefined;
@@ -581,12 +594,31 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
581
594
  totalRows: number;
582
595
  enrichedCount: number | undefined;
583
596
  needsEnrichCount: number | undefined;
584
- messagesCount: number | undefined;
597
+ messagesCount: number;
585
598
  needsApprovalCount: number | undefined;
586
599
  processingCount: number | undefined;
587
600
  failedCount: number | undefined;
588
601
  };
589
602
  guidance: string;
590
603
  stats: WorkflowTableStats | null;
604
+ messageGeneration?: {
605
+ completed: number;
606
+ passingGeneratedMessages: number;
607
+ minMessagesCount: number;
608
+ floorMet: boolean;
609
+ } | undefined;
610
+ ready: boolean;
611
+ reason: string;
612
+ attempts: number;
613
+ elapsedMs: number;
614
+ tableId: string;
615
+ passRate: {
616
+ minPassedCount?: number | undefined;
617
+ completed: number;
618
+ passed: number;
619
+ pending: number;
620
+ percent: number;
621
+ targetCount: number;
622
+ };
591
623
  }>;
592
624
  export {};
@@ -9,6 +9,11 @@ const DEFAULT_INTERVAL_MS = 2000;
9
9
  function sleep(ms) {
10
10
  return new Promise((resolve) => setTimeout(resolve, ms));
11
11
  }
12
+ function countPassingGeneratedMessages(rowSnapshot) {
13
+ if (!rowSnapshot?.rows?.length)
14
+ return 0;
15
+ return rowSnapshot.rows.filter((row) => row.icpPassed === true && Boolean(row.message?.trim())).length;
16
+ }
12
17
  function normalizeRubricItemDefaults(item) {
13
18
  return {
14
19
  ...item,
@@ -393,6 +398,10 @@ export const rubricToolDefinitions = [
393
398
  type: "number",
394
399
  description: "Optional pass floor for bounded create-campaign samples. When this floor is met, the tool returns ready:true with partial:true instead of waiting for every target row to finish.",
395
400
  },
401
+ minMessagesCount: {
402
+ type: "number",
403
+ description: "Optional generated-message floor. When provided with minPassedCount, the tool waits until both floors are met so create-campaign-v2 can review the first passing generated message without waiting for the full batch.",
404
+ },
396
405
  timeoutMs: {
397
406
  type: "number",
398
407
  description: `Max time to wait in ms (default ${DEFAULT_TIMEOUT_MS}).`,
@@ -688,6 +697,7 @@ export async function waitForRubricResults(input) {
688
697
  const intervalMs = Math.max(500, input.intervalMs ?? DEFAULT_INTERVAL_MS);
689
698
  const targetCount = resolveMaxProspects(input.targetCount);
690
699
  const minPassedCount = resolveMinPassedCount(input.minPassedCount);
700
+ const minMessagesCount = resolveMinPassedCount(input.minMessagesCount);
691
701
  const includeRows = input.includeRows !== false;
692
702
  let tableId = input.tableId;
693
703
  if (!tableId && input.campaignOfferId) {
@@ -709,19 +719,33 @@ export async function waitForRubricResults(input) {
709
719
  const totalRows = stats.totalRows ?? 0;
710
720
  const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
711
721
  const pending = Math.max(effectiveTarget - completed, 0);
722
+ const messagesCount = stats.messagesCount ?? 0;
712
723
  const failedCount = stats.failedCount ?? 0;
713
724
  const processingCount = stats.processingCount ?? 0;
714
725
  const queuedCount = stats.queuedCount ?? 0;
715
726
  const cancellableCount = stats.cancellableCount ?? 0;
716
727
  const minPassFloorMet = minPassedCount !== null && passed >= minPassedCount;
728
+ const rawMessageFloorMet = minMessagesCount === null || messagesCount >= minMessagesCount;
729
+ let rowSnapshotForMessageCheck = null;
730
+ let passingGeneratedMessagesCount = null;
731
+ if (minMessagesCount !== null && minPassFloorMet && rawMessageFloorMet) {
732
+ rowSnapshotForMessageCheck = await getTableRowsMinimal(tableId, {
733
+ limit: effectiveTarget,
734
+ });
735
+ passingGeneratedMessagesCount = countPassingGeneratedMessages(rowSnapshotForMessageCheck);
736
+ }
737
+ const messageFloorMet = minMessagesCount === null ||
738
+ (passingGeneratedMessagesCount ?? 0) >= minMessagesCount;
739
+ const earlyFloorMet = minPassFloorMet && messageFloorMet;
717
740
  const unresolvedRowsResolvedAsFailures = pending > 0 && completed + failedCount >= effectiveTarget;
718
741
  const noActiveProcessing = processingCount === 0 && queuedCount === 0 && cancellableCount === 0;
719
- if (completed >= effectiveTarget) {
742
+ if (completed >= effectiveTarget && messageFloorMet) {
720
743
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
721
744
  const rowSnapshot = includeRows
722
- ? await getTableRowsMinimal(tableId, {
723
- limit: effectiveTarget,
724
- })
745
+ ? (rowSnapshotForMessageCheck ??
746
+ (await getTableRowsMinimal(tableId, {
747
+ limit: effectiveTarget,
748
+ })))
725
749
  : null;
726
750
  return {
727
751
  ready: true,
@@ -735,11 +759,21 @@ export async function waitForRubricResults(input) {
735
759
  targetCount: effectiveTarget,
736
760
  ...(minPassedCount !== null ? { minPassedCount } : {}),
737
761
  },
762
+ ...(minMessagesCount !== null
763
+ ? {
764
+ messageGeneration: {
765
+ completed: messagesCount,
766
+ passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
767
+ minMessagesCount,
768
+ floorMet: true,
769
+ },
770
+ }
771
+ : {}),
738
772
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
739
773
  stats,
740
774
  };
741
775
  }
742
- if (minPassFloorMet) {
776
+ if (earlyFloorMet) {
743
777
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
744
778
  const reason = !noActiveProcessing
745
779
  ? "min_passed_count_met_with_active_processing"
@@ -747,9 +781,10 @@ export async function waitForRubricResults(input) {
747
781
  ? "min_passed_count_met_with_resolved_failures"
748
782
  : "min_passed_count_met_no_active_processing";
749
783
  const rowSnapshot = includeRows
750
- ? await getTableRowsMinimal(tableId, {
751
- limit: effectiveTarget,
752
- })
784
+ ? (rowSnapshotForMessageCheck ??
785
+ (await getTableRowsMinimal(tableId, {
786
+ limit: effectiveTarget,
787
+ })))
753
788
  : null;
754
789
  return {
755
790
  ready: true,
@@ -766,6 +801,16 @@ export async function waitForRubricResults(input) {
766
801
  targetCount: effectiveTarget,
767
802
  minPassedCount,
768
803
  },
804
+ ...(minMessagesCount !== null
805
+ ? {
806
+ messageGeneration: {
807
+ completed: messagesCount,
808
+ passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
809
+ minMessagesCount,
810
+ floorMet: true,
811
+ },
812
+ }
813
+ : {}),
769
814
  partialResult: {
770
815
  completed,
771
816
  passed,
@@ -775,6 +820,14 @@ export async function waitForRubricResults(input) {
775
820
  minPassedCount,
776
821
  enoughToDiagnose: true,
777
822
  floorMet: true,
823
+ ...(minMessagesCount !== null
824
+ ? {
825
+ messagesCount,
826
+ passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
827
+ minMessagesCount,
828
+ messageFloorMet: true,
829
+ }
830
+ : {}),
778
831
  },
779
832
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
780
833
  stats,
@@ -785,9 +838,21 @@ export async function waitForRubricResults(input) {
785
838
  const completed = lastStats?.passRate?.completed ?? 0;
786
839
  const passed = lastStats?.passRate?.passed ?? 0;
787
840
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
841
+ const messagesCount = lastStats?.messagesCount ?? 0;
788
842
  const totalRows = lastStats?.totalRows ?? 0;
789
843
  const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
790
844
  const pending = Math.max(effectiveTarget - completed, 0);
845
+ let timeoutPassingGeneratedMessagesCount = null;
846
+ if (minMessagesCount !== null) {
847
+ try {
848
+ timeoutPassingGeneratedMessagesCount = countPassingGeneratedMessages(await getTableRowsMinimal(tableId, {
849
+ limit: effectiveTarget,
850
+ }));
851
+ }
852
+ catch {
853
+ timeoutPassingGeneratedMessagesCount = null;
854
+ }
855
+ }
791
856
  return {
792
857
  ready: false,
793
858
  reason: "timeout",
@@ -802,6 +867,16 @@ export async function waitForRubricResults(input) {
802
867
  targetCount: effectiveTarget,
803
868
  ...(minPassedCount !== null ? { minPassedCount } : {}),
804
869
  },
870
+ ...(minMessagesCount !== null
871
+ ? {
872
+ messageGeneration: {
873
+ completed: messagesCount,
874
+ passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
875
+ minMessagesCount,
876
+ floorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
877
+ },
878
+ }
879
+ : {}),
805
880
  partialResult: {
806
881
  completed,
807
882
  passed,
@@ -811,17 +886,25 @@ export async function waitForRubricResults(input) {
811
886
  ...(minPassedCount !== null ? { minPassedCount } : {}),
812
887
  enoughToDiagnose: completed > 0,
813
888
  floorMet: minPassedCount !== null && passed >= minPassedCount,
889
+ ...(minMessagesCount !== null
890
+ ? {
891
+ messagesCount,
892
+ passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
893
+ minMessagesCount,
894
+ messageFloorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
895
+ }
896
+ : {}),
814
897
  },
815
898
  diagnostic: {
816
899
  totalRows,
817
900
  enrichedCount: lastStats?.enrichedCount,
818
901
  needsEnrichCount: lastStats?.needsEnrichCount,
819
- messagesCount: lastStats?.messagesCount,
902
+ messagesCount,
820
903
  needsApprovalCount: lastStats?.needsApprovalCount,
821
904
  processingCount: lastStats?.processingCount,
822
905
  failedCount: lastStats?.failedCount,
823
906
  },
824
- guidance: "If this is create-campaign-v2 validate-sample, do not repeat waits indefinitely. Use passRate/stats to diagnose and surface sample_revision_required before Settings when the sample is under the pass floor or messages are incomplete.",
907
+ guidance: "If this is create-campaign-v2 validate-sample, do not repeat waits indefinitely. Use passRate/stats to diagnose and surface sample_revision_required before Settings when the sample is under the pass floor. If minMessagesCount is set, one passing generated message is enough to start review; do not wait for a stronger sample.",
825
908
  stats: lastStats,
826
909
  };
827
910
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.147",
3
+ "version": "0.1.148",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -49,9 +49,10 @@ for debug output.
49
49
  12. Sync the approved template into the campaign brief.
50
50
  13. After message approval, keep the watched app on Filter Leads while the
51
51
  bounded enrichment/filter cascade starts.
52
- 14. Move to Messages only after at least one review row passes, generate and
53
- review bounded messages, then hand off to Settings, sender, sequence, and
54
- explicit launch greenlight.
52
+ 14. Move to Messages only after at least one review row passes and one generated
53
+ message is ready for review. Do not wait for a stronger sample once that
54
+ first passing message exists; review it, then hand off to Settings, sender,
55
+ sequence, and explicit launch greenlight.
55
56
 
56
57
  There is no normal approval-packet, commit-gate, atomic-mint, or local
57
58
  artifact-validation step. Those belong only to legacy validation/rehearsal
@@ -1124,8 +1124,14 @@
1124
1124
  "cellSource": "pending_generate_message_cells_for_passing_rows using approved_campaign_brief_template"
1125
1125
  },
1126
1126
  {
1127
- "tool": "wait_for_campaign_table_ready",
1128
- "purpose": "wait_for_generate_message_cells_to_complete"
1127
+ "tool": "wait_for_rubric_results",
1128
+ "purpose": "wait_for_first_passing_generated_message",
1129
+ "requiredValues": {
1130
+ "includeRows": false,
1131
+ "minPassedCount": 1,
1132
+ "minMessagesCount": 1
1133
+ },
1134
+ "readVia": "stats_only_tool_result"
1129
1135
  },
1130
1136
  {
1131
1137
  "action": "observe_generate_message_results",
@@ -1169,25 +1175,24 @@
1169
1175
  "currentStep": "auto-execute-messaging",
1170
1176
  "watchNarration.stage": "review-ready"
1171
1177
  },
1172
- "watchNarrationRule": "Say what just happened, that review-batch messages are ready in Messages, and that the next action is reviewing or approving generated messages before Settings."
1178
+ "watchNarrationRule": "Say the first passing generated message is ready in Messages; next is review before Settings."
1173
1179
  },
1174
1180
  {
1175
1181
  "action": "ask_generated_message_review_choice",
1176
1182
  "uses": "request_user_input",
1177
1183
  "choices": [
1178
- "Approve generated messages",
1184
+ "Approve generated message and continue to Settings",
1179
1185
  "Revise filters",
1180
1186
  "Revise message template",
1181
1187
  "Pause here"
1182
- ],
1183
- "rule": "Do not advance to Settings until the generated review-batch messages have been reviewed and approved."
1188
+ ]
1184
1189
  }
1185
1190
  ],
1186
1191
  "allowedTools": [
1187
1192
  "get_subskill_asset",
1188
1193
  "get_rows_minimal",
1189
1194
  "queue_cells",
1190
- "wait_for_campaign_table_ready",
1195
+ "wait_for_rubric_results",
1191
1196
  "update_campaign",
1192
1197
  "AskUserQuestion",
1193
1198
  "request_user_input"
@@ -1201,6 +1206,7 @@
1201
1206
  "hardRules": [
1202
1207
  "critique_failure_never_escalates",
1203
1208
  "critique_sample_size_bounded_by_config",
1209
+ "first_passing_generated_message_unblocks_review",
1204
1210
  "critics_fixed_at_targeting_copy_voice",
1205
1211
  "synthesis_enforces_phase_84_token_contract",
1206
1212
  "opus_reserved_for_highest_value_subset",
@@ -60,9 +60,10 @@ auto-revise leads.
60
60
  explicit batch count anyway so future larger expansion batches do not
61
61
  accidentally stop early
62
62
  (see §Known Tool Behaviors #3)
63
- - minPassedCount=1 means one passing filtered row unblocks Step 15
64
- Generate Message observation. Do not wait for all sample rows to finish
65
- before messages start.
63
+ - minPassedCount=1 means one passing filtered row unblocks Step 15 Generate
64
+ Message observation. Step 15 then waits only for one generated message
65
+ (`minMessagesCount=1`) before review. Do not wait for all sample rows to
66
+ finish before messages start.
66
67
 
67
68
  7. call `wait_for_rubric_results` with `includeRows=false`; extract ONLY:
68
69
  - ready: boolean
@@ -101,7 +102,9 @@ auto-revise leads.
101
102
  10. branch:
102
103
  if passInSample >= 1:
103
104
  proceed to Step 15 (auto-execute-messaging) with currently passing rows
104
- so Generate Message can start without waiting for the full sample
105
+ so Generate Message can start without waiting for the full sample. Once
106
+ one passing generated message is ready, stop for user review instead of
107
+ waiting for a stronger sample.
105
108
  else:
106
109
  diagnose (see Brief-vs-List Diagnosis below)
107
110
  revisionRound += 1
@@ -179,7 +182,8 @@ processing makes the experience feel frozen.
179
182
 
180
183
  Workaround: treat timeout stats as a partial sample. If at least one row has
181
184
  passed, move to Step 15 and observe or queue Generate Message for the passing
182
- rows. If zero rows have passed and no active processing is visible, stop at
185
+ rows; Step 15 stops when one generated message is ready. If zero rows have
186
+ passed and no active processing is visible, stop at
183
187
  `Status: sample-needs-revision` before Settings. Show the completed / passed /
184
188
  pending counts and ask whether to revise source, revise filter/rubric, or wait
185
189
  once only if active processing is still visible.
@@ -4,15 +4,16 @@ This reference governs Step 15 (`auto-execute-messaging`) re-cascade
4
4
  behavior when Step 14's validate-sample loop graduates additional rows
5
5
  from pending → passed after the first Generate Message cells have already run.
6
6
 
7
- Load whenever Step 15 is about to transition to
8
- `awaiting-user-greenlight`, and on every resume into Step 15.
7
+ Load whenever Step 15 is about to ask for generated-message review or transition
8
+ to `awaiting-user-greenlight`, and on every resume into Step 15.
9
9
 
10
10
  ## Principle
11
11
 
12
- Generate Message cells are cascade-scoped. If Step 14's rubric flips rows
13
- from pending → passed AFTER Step 15 first observes messages for the review
14
- batch, the new rows can sit pending with no message. Step 15 must explicitly
15
- queue the newly-pending Generate Message cells for the review-batch subset.
12
+ Generate Message cells are cascade-scoped. If Step 14's rubric flips rows from
13
+ pending → passed AFTER Step 15 first observes messages for the review batch, the
14
+ new rows can sit pending with no message. That must not block the first review
15
+ handoff. Step 15 opens review as soon as one passing generated message exists;
16
+ late-passed rows can be re-cascaded after explicit user continuation or resume.
16
17
 
17
18
  Observed on manual Phase-85 signal-discovery run (2026-04-20): 15 new
18
19
  rows graduated pending → passed mid-tail while `messagesCount` stayed
@@ -22,13 +23,17 @@ flat at 257. The new rows never got messages.
22
23
 
23
24
  Re-cascade runs whenever ALL of the following are true:
24
25
 
25
- 1. Step 15 has already observed the initial review-batch message cascade.
26
+ 1. The user has already approved the first generated message or explicitly asked
27
+ to continue processing more review-batch rows.
26
28
  2. A subsequent check of rubric state shows rows that were pending at
27
29
  first message-generation pass are now passed.
28
30
  3. Those newly-passed rows do NOT yet have generated messages
29
31
  (`messagesCount` flat relative to pre-cascade).
30
32
  4. Step 15 has NOT yet transitioned to `awaiting-user-greenlight`.
31
33
 
34
+ Before that first generated-message approval, do not run this loop just to wait
35
+ for a bigger sample. One passing generated message is enough for review.
36
+
32
37
  If condition 4 already fired (i.e. we're in Step 16), the re-cascade
33
38
  runs on resume into Step 15 if validate-sample was re-entered and new
34
39
  rows graduated.
@@ -252,16 +252,17 @@ Template approved, bounded filter test running:
252
252
  "stage": "fit-message",
253
253
  "headline": "Template saved",
254
254
  "visibleState": "The browser stays on Filter Leads while the bounded enrichment and filter test runs.",
255
- "agentIntent": "Codex saved the approved message template, queued the review-batch Enrich Prospect cells, and is waiting for at least one row to pass before moving to Messages.",
256
- "nextAction": "Move to Messages after a passing row"
255
+ "agentIntent": "Codex saved the approved message template, queued the review-batch Enrich Prospect cells, and is waiting for one passing row with one generated message before moving to review.",
256
+ "nextAction": "Review the first passing generated message"
257
257
  }
258
258
  ```
259
259
 
260
260
  Do not move to Messages immediately after `approve-message`. The visible route
261
261
  is already Filter Leads after `save_rubrics`; approving the message only unlocks
262
262
  the bounded cascade from that screen. Move to Messages only once at least one
263
- review-batch row passes and Generate Message cells are ready/running for the
264
- passing rows.
263
+ review-batch row passes and one generated message is ready for review. Do not
264
+ wait for the rest of the review batch or a stronger sample before asking the
265
+ user to approve the generated message.
265
266
 
266
267
  Messages waiting for template:
267
268
 
@@ -58,11 +58,12 @@ Step 14 — kick bounded cascade + observe sample
58
58
  passing filtered row exists.)
59
59
 
60
60
  Step 15 — observe messaging
61
- get_rows_minimal # confirm passing rows have completed Generate Message cells
61
+ wait_for_rubric_results({ minPassedCount: 1, minMessagesCount: 1, includeRows: false })
62
+ get_rows_minimal # confirm the first passing generated message exists
62
63
  (rare) queue_cells on any pending Generate Message cells
63
64
  token-contract spot check via get_rows
64
65
  update_campaign(currentStep=auto-execute-messaging) with review-ready narration
65
- ask the user to approve generated review-batch messages before Settings
66
+ ask the user to approve the generated message before Settings
66
67
  only after approval: update_campaign(currentStep=awaiting-user-greenlight)
67
68
  (generate_messages is NOT an MCP tool; messages come from the cascade)
68
69
 
@@ -291,8 +292,10 @@ Do not route to a visible `validate-sample` step. Full decision tree lives in
291
292
  review batch only. After `save_rubrics` and the approved message template are
292
293
  persisted, Step 14 queues the review-batch Enrich Prospect cells, waits until
293
294
  filter results start landing, then moves to message observation as soon as one
294
- row passes. It does NOT call `check_rubric`, `bulk_enrich_with_prospeo`, or any
295
- other direct enrichment/scoring tool.
295
+ row passes. Step 15 opens review as soon as one passing generated message
296
+ exists. Do not wait for a larger or stronger sample once that first passing
297
+ message is ready. It does NOT call `check_rubric`,
298
+ `bulk_enrich_with_prospeo`, or any other direct enrichment/scoring tool.
296
299
 
297
300
  Shape:
298
301
 
@@ -307,6 +310,7 @@ projectedPass = round(passInSample / sampleSize * importLimit)
307
310
  if wait_for_rubric_results.ready === true and passRate.passed >= 1:
308
311
  advance to Step 15 to observe or queue Generate Message for currently passing rows
309
312
  do not wait for every sample row to finish before message generation starts
313
+ stop for review as soon as one passing generated message is ready
310
314
  else if wait_for_rubric_results.ready === false and reason === "timeout":
311
315
  use the partial passRate/stats as the sample diagnostic
312
316
  if passRate.passed >= 1:
@@ -382,17 +386,20 @@ Template`. If it does not, fail before the cascade runs. Do not repair
382
386
  already (cascade auto-fired it). If it is still `pending`, queue
383
387
  it explicitly: `queue_cells({ tableId, cellIds:
384
388
  <generateMessageCellIds> })`.
385
- 3. `wait_for_campaign_table_ready` (or poll) until every passing
386
- row's Generate Message cell is `completed`.
387
- 4. Read the results back via `get_rows` (full) and sanity-check a
388
- sample against the Phase 84 token contract: no unresolved
389
+ 3. `wait_for_rubric_results({ tableId, targetCount: reviewBatchSize,
390
+ minPassedCount: 1, minMessagesCount: 1, includeRows: false })` until the
391
+ first passing generated message is ready. Do not wait for every passing row's
392
+ Generate Message cell, and do not add "one more wait" for a stronger sample.
393
+ Remaining review-batch rows can continue processing in the background.
394
+ 4. Read the first ready generated message back via `get_rows` (full) and
395
+ sanity-check that sample against the Phase 84 token contract: no unresolved
389
396
  `{{tokens}}`, no invented proof, one sentence per line, etc.
390
397
  5. If the sample fails the token contract, diagnose brief-vs-list
391
398
  (same revision loop as Step 14) and escalate if over
392
399
  `maxRevisionRounds`.
393
- 6. On success, keep `currentStep: "auto-execute-messaging"` and ask the user
394
- to approve the generated review-batch messages. Only that approval may move
395
- the campaign to Settings / `awaiting-user-greenlight`.
400
+ 6. On success, keep `currentStep: "auto-execute-messaging"` and ask the user to
401
+ approve the generated message before continuing to Settings. Only that
402
+ approval may move the campaign to Settings / `awaiting-user-greenlight`.
396
403
 
397
404
  **Do NOT hand-write message bodies via `update_cell`.** `update_cell`
398
405
  is reachable for legitimate operator overrides AFTER the tail hands
@@ -432,15 +439,16 @@ strings, which is why Step 16 requires Step 15 to be complete.
432
439
  4. Check `messaging.tokenContract`. When `strict`, reject any sample
433
440
  message that contains unresolved or unsupported tokens (including
434
441
  any critique rewrite that tried to introduce one).
435
- 5. If the sample passes the token contract (and critique when enabled),
436
- stop at the review-batch handoff. Do NOT scale to the full source list
437
- before explicit user expansion approval.
442
+ 5. If the first passing generated message passes the token contract (and
443
+ critique when enabled), stop at the review-batch handoff. Do NOT wait for the
444
+ remaining review-batch rows and do NOT scale to the full source list before
445
+ explicit user expansion approval.
438
446
  6. If the sample fails the token contract or critique, diagnose +
439
447
  loop the same way Step 14 does (brief-vs-list), subject to the same
440
448
  `maxRevisionRounds` cap.
441
449
  7. On success, keep `currentStep: "auto-execute-messaging"`, show that the
442
- review-batch messages are ready in Messages, and ask for approval before
443
- Settings. Only after the user approves those generated messages should
450
+ first passing generated message is ready in Messages, and ask for approval
451
+ before Settings. Only after the user approves the generated message should
444
452
  `update_campaign({ campaignId, currentStep: "awaiting-user-greenlight" })`
445
453
  run.
446
454
 
@@ -0,0 +1,9 @@
1
+ {
2
+ "parallelMode": "wide",
3
+ "agentCount": 6,
4
+ "maxToolCallsPerAgent": 2,
5
+ "senderMaxAgents": 2,
6
+ "senderMaxToolCallsPerAgent": 3,
7
+ "progressMode": true,
8
+ "debugMode": true
9
+ }