@signaliz/sdk 1.0.17 → 1.0.19

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.
@@ -479,6 +479,38 @@ var Signals = class {
479
479
  constructor(client) {
480
480
  this.client = client;
481
481
  }
482
+ /** Turn the strongest supported company signal into three complete, evidence-backed emails. */
483
+ async signalToCopy(params) {
484
+ const data = await this.client.post("api/v1/signal-to-copy", {
485
+ company_domain: params.companyDomain,
486
+ person_name: params.personName,
487
+ title: params.title,
488
+ campaign_offer: params.campaignOffer,
489
+ research_prompt: params.researchPrompt
490
+ });
491
+ return {
492
+ success: data.success ?? true,
493
+ strongestSignal: data.strongest_signal ?? "",
494
+ whyNow: data.why_now ?? "",
495
+ subjectLine: data.subject_line ?? "",
496
+ openingLine: data.opening_line ?? "",
497
+ confidence: data.confidence ?? 0,
498
+ evidenceUrl: data.evidence_url ?? "",
499
+ researchPrompt: data.research_prompt,
500
+ variations: (data.variations ?? []).map((v) => ({
501
+ style: v.style,
502
+ subjectLine: v.subject_line ?? "",
503
+ openingLine: v.opening_line ?? "",
504
+ cta: v.cta ?? "",
505
+ prediction: v.prediction,
506
+ email: v.email
507
+ }))
508
+ };
509
+ }
510
+ /** @deprecated Use signalToCopy. Retained for compatibility. */
511
+ async personalize(params) {
512
+ return this.signalToCopy(params);
513
+ }
482
514
  /** Enrich a company with actionable signals (V1) */
483
515
  async enrich(params) {
484
516
  const args = {
@@ -487,6 +519,9 @@ var Signals = class {
487
519
  if (params.domain) args.domain = params.domain;
488
520
  if (params.researchPrompt) args.research_prompt = params.researchPrompt;
489
521
  if (params.signalTypes) args.signal_types = params.signalTypes;
522
+ if (params.targetSignalCount) args.target_signal_count = params.targetSignalCount;
523
+ if (params.lookbackDays) args.lookback_days = params.lookbackDays;
524
+ if (params.enableDeepSearch !== void 0) args.enable_deep_search = params.enableDeepSearch;
490
525
  const data = await this.client.mcp("tools/call", {
491
526
  name: "enrich_company_signals",
492
527
  arguments: args
@@ -496,6 +531,16 @@ var Signals = class {
496
531
  signals: (data.signals ?? []).map((s) => ({
497
532
  title: s.title,
498
533
  signalType: s.signal_type,
534
+ sourceType: s.source_type ?? s.signal_classification?.label ?? "unknown",
535
+ sourceProvenance: s.source_provenance ?? s.metadata?.source_provenance ?? null,
536
+ signalFamily: s.signal_family ?? s.signal_type ?? null,
537
+ signalEventType: s.signal_event_type ?? s.signal_classification?.slug ?? null,
538
+ signalEventLabel: s.signal_event_label ?? s.signal_classification?.label ?? null,
539
+ signalClassification: s.signal_classification ? {
540
+ label: s.signal_classification.label,
541
+ slug: s.signal_classification.slug,
542
+ rationale: s.signal_classification.rationale
543
+ } : null,
499
544
  confidenceScore: s.confidence_score ?? 0,
500
545
  detectedAt: s.detected_at ?? "",
501
546
  url: s.url,
@@ -536,8 +581,17 @@ var Signals = class {
536
581
  content: s.content ?? "",
537
582
  date: s.date ?? null,
538
583
  sourceUrl: s.source_url ?? null,
539
- sourceType: s.source_type ?? "unknown",
540
- confidence: s.confidence ?? null
584
+ sourceType: s.source_type ?? s.signal_classification?.label ?? "unknown",
585
+ sourceProvenance: s.source_provenance ?? s.metadata?.source_provenance ?? null,
586
+ signalFamily: s.signal_family ?? s.type ?? null,
587
+ signalEventType: s.signal_event_type ?? s.signal_classification?.slug ?? null,
588
+ signalEventLabel: s.signal_event_label ?? s.signal_classification?.label ?? null,
589
+ confidence: s.confidence ?? null,
590
+ signalClassification: s.signal_classification ? {
591
+ label: s.signal_classification.label,
592
+ slug: s.signal_classification.slug,
593
+ rationale: s.signal_classification.rationale
594
+ } : null
541
595
  })),
542
596
  signalCount: data.signal_count ?? 0,
543
597
  intelligence: data.intelligence ? {
@@ -580,6 +634,58 @@ var Signals = class {
580
634
  }
581
635
  };
582
636
  }
637
+ /** Plan or run experimental Signal Fusion with explicit spend controls. */
638
+ async fusion(params) {
639
+ const args = {};
640
+ if (params.companyName) args.company_name = params.companyName;
641
+ if (params.domain) args.domain = params.domain;
642
+ if (params.companyDomain) args.company_domain = params.companyDomain;
643
+ if (params.linkedinUrl) args.linkedin_url = params.linkedinUrl;
644
+ if (params.companies) args.companies = params.companies;
645
+ if (params.preset) args.preset = params.preset;
646
+ if (params.researchPrompt) args.research_prompt = params.researchPrompt;
647
+ if (params.signalTypes) args.signal_types = params.signalTypes;
648
+ if (params.lookbackDays) args.lookback_days = params.lookbackDays;
649
+ if (params.targetSignalCount) args.target_signal_count = params.targetSignalCount;
650
+ if (params.maxToolCalls) args.max_tool_calls = params.maxToolCalls;
651
+ if (params.maxTokens) args.max_tokens = params.maxTokens;
652
+ if (params.timeoutMs) args.timeout_ms = params.timeoutMs;
653
+ const dryRun = params.dryRun ?? params.dry_run;
654
+ const confirmSpend = params.confirmSpend ?? params.confirm_spend;
655
+ const maxCredits = params.maxCredits ?? params.max_credits;
656
+ const maxCostUsd = params.maxCostUsd ?? params.max_cost_usd;
657
+ if (dryRun !== void 0) args.dry_run = dryRun;
658
+ if (confirmSpend !== void 0) args.confirm_spend = confirmSpend;
659
+ if (maxCredits !== void 0) args.max_credits = maxCredits;
660
+ if (maxCostUsd !== void 0) args.max_cost_usd = maxCostUsd;
661
+ const data = await this.client.post("signal-fusion", args);
662
+ return {
663
+ success: data.success ?? false,
664
+ capability: data.capability ?? "signal_fusion",
665
+ displayName: data.display_name ?? "Signal Fusion",
666
+ company: data.company,
667
+ signals: (data.signals ?? []).map((s) => ({
668
+ id: s.id,
669
+ title: s.title,
670
+ type: s.type,
671
+ content: s.content ?? "",
672
+ date: s.date ?? null,
673
+ sourceUrl: s.source_url ?? null,
674
+ sourceType: s.source_type ?? "unknown",
675
+ confidence: s.confidence ?? null
676
+ })),
677
+ signalCount: data.signal_count ?? data.signals?.length ?? 0,
678
+ results: data.results ?? [],
679
+ model: data.model,
680
+ fusion: data.fusion,
681
+ costUsd: data.cost_usd ?? 0,
682
+ tokensUsed: data.tokens_used ?? 0,
683
+ costSummary: data.cost_summary,
684
+ creditsUsed: data.total_credits_used ?? data.credits_used ?? 0,
685
+ billingMetadata: data.billing_metadata,
686
+ raw: data
687
+ };
688
+ }
583
689
  };
584
690
 
585
691
  // src/resources/systems.ts
@@ -868,7 +974,11 @@ var Campaigns = class {
868
974
  maxSupportedTargetCount: data.max_supported_target_count,
869
975
  brainContext: data.brain_context ?? {},
870
976
  learningHoldout: data.learning_holdout ?? {},
871
- providerRoute: mapProviderRoute(data)
977
+ providerRoute: mapProviderRoute(data),
978
+ effectiveSourceRouting: recordOrNull(data.effective_source_routing ?? data.source_routing),
979
+ sourcePlan: recordOrNull(data.source_plan ?? data.plan?.source_plan),
980
+ sourceRoutes: recordArray(data.source_routes ?? data.source_plan?.routes ?? data.plan?.source_plan?.routes),
981
+ sourceShards: recordArray(data.source_shards ?? data.source_plan?.shards ?? data.plan?.source_plan?.shards)
872
982
  };
873
983
  }
874
984
  async getCampaignBuildStatus(campaignBuildId) {
@@ -1014,6 +1124,7 @@ function mapCustomerRowCounts(value) {
1014
1124
  acceptedRows: numberOrNull(record.accepted_rows) ?? void 0,
1015
1125
  usableRows: numberOrNull(record.usable_rows) ?? void 0,
1016
1126
  copiedRows: numberOrNull(record.copied_rows) ?? void 0,
1127
+ copySkippedRows: numberOrNull(record.copy_skipped_rows) ?? void 0,
1017
1128
  approvalReadyRows: numberOrNull(record.approval_ready_rows) ?? void 0,
1018
1129
  qaReadyRows: numberOrNull(record.qa_ready_rows) ?? void 0,
1019
1130
  deliveredRows: numberOrNull(record.delivered_rows) ?? void 0,
@@ -1060,6 +1171,9 @@ function recordOrNull(value) {
1060
1171
  function stringArray(value) {
1061
1172
  return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
1062
1173
  }
1174
+ function recordArray(value) {
1175
+ return Array.isArray(value) ? value.map(recordOrNull).filter((item) => Boolean(item)) : [];
1176
+ }
1063
1177
  function diagnosticMessages(value) {
1064
1178
  if (!Array.isArray(value)) return [];
1065
1179
  return value.map((item) => {
@@ -1221,7 +1335,8 @@ function buildArgs(request) {
1221
1335
  suppress_prior_campaign_ids: request.cacheReusePolicy.suppressPriorCampaignIds,
1222
1336
  suppress_prior_list_ids: request.cacheReusePolicy.suppressPriorListIds,
1223
1337
  uniqueness: request.cacheReusePolicy.uniqueness,
1224
- require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata
1338
+ require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata,
1339
+ verified_cache_ttl_days: request.cacheReusePolicy.verifiedCacheTtlDays
1225
1340
  };
1226
1341
  }
1227
1342
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
@@ -1246,7 +1361,8 @@ function buildArgs(request) {
1246
1361
  geographies: request.icp.geographies,
1247
1362
  keywords: request.icp.keywords,
1248
1363
  exclusions: request.icp.exclusions,
1249
- require_verified_email: request.icp.requireVerifiedEmail
1364
+ require_verified_email: request.icp.requireVerifiedEmail,
1365
+ persona_expansion_mode: request.icp.personaExpansionMode
1250
1366
  };
1251
1367
  }
1252
1368
  if (request.policy) {
@@ -1286,6 +1402,7 @@ function buildArgs(request) {
1286
1402
  } : void 0,
1287
1403
  sequence_steps: request.copy.sequenceSteps,
1288
1404
  copy_schema: request.copy.copySchema,
1405
+ evidence_mode: request.copy.evidenceMode,
1289
1406
  banned_phrases: request.copy.bannedPhrases,
1290
1407
  approval_required: request.copy.approvalRequired,
1291
1408
  quality_tier: request.copy.qualityTier
@@ -1342,7 +1459,8 @@ var DEFAULT_SIGNALIZ_CAMPAIGN_BUILDER_DEFAULTS = {
1342
1459
  copy: {
1343
1460
  enabled: true,
1344
1461
  tone: "direct",
1345
- maxBodyWords: 125
1462
+ maxBodyWords: 125,
1463
+ evidenceMode: "signal_led"
1346
1464
  },
1347
1465
  delivery: {
1348
1466
  enabled: true,
@@ -1399,7 +1517,8 @@ var CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS = [
1399
1517
  "Block export when required identity, company, or email fields are missing."
1400
1518
  ],
1401
1519
  activationBoundary: "Cache inventory is read-only. Fresh sourcing, cache writes, export, and sender loading require explicit approval.",
1402
- memoryKeywords: ["cache inventory", "large list", "unique emails", "top-up sourcing", "source metadata"]
1520
+ memoryKeywords: ["cache inventory", "large list", "unique emails", "top-up sourcing", "source metadata"],
1521
+ knowledgeSources: ["data-ops/contact-data-lifecycle", "data-ops/credit-optimization-guide", "gtm/data-quality-ops", "gtm/lead-generation-strategies"]
1403
1522
  },
1404
1523
  {
1405
1524
  slug: "net-new-suppressed-list",
@@ -1424,7 +1543,8 @@ var CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS = [
1424
1543
  "Do not describe rows as fully qualified when the approved path only proves net-new email status."
1425
1544
  ],
1426
1545
  activationBoundary: "Suppression review must pass before export, upload, CRM sync, sender load, or sequence launch.",
1427
- memoryKeywords: ["suppression", "net-new", "prior email overlap", "domain review", "provenance-rich QA"]
1546
+ memoryKeywords: ["suppression", "net-new", "prior email overlap", "domain review", "provenance-rich QA"],
1547
+ knowledgeSources: ["gtm/data-quality-ops", "data-ops/contact-data-lifecycle", "gtm/outbound-sequencing"]
1428
1548
  },
1429
1549
  {
1430
1550
  slug: "proof-first-vertical-gate",
@@ -1449,7 +1569,8 @@ var CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS = [
1449
1569
  "Contact fit should meet the configured threshold before email finding or copy generation."
1450
1570
  ],
1451
1571
  activationBoundary: "Proof gates are required before contact spend, copy, export, or external writes.",
1452
- memoryKeywords: ["proof gate", "vertical exclusion", "company before contact", "pass review fail", "fit threshold"]
1572
+ memoryKeywords: ["proof gate", "vertical exclusion", "company before contact", "pass review fail", "fit threshold"],
1573
+ knowledgeSources: ["gtm/icp-design-framework", "gtm/strategy/scoring.yaml", "gtm/advanced/account-prioritization-frameworks", "gtm/advanced/predictive-lead-scoring", "gtm/verticals"]
1453
1574
  },
1454
1575
  {
1455
1576
  slug: "signal-led-copy-approval",
@@ -1474,7 +1595,8 @@ var CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS = [
1474
1595
  "Destination fields must remain blocked until approval metadata is present."
1475
1596
  ],
1476
1597
  activationBoundary: "Copy readiness is not delivery approval. Export, sync, sender load, and launch require a separate human-approved gate.",
1477
- memoryKeywords: ["company signals", "evidence-fed copy", "why now", "copy blocker", "approval gate"]
1598
+ memoryKeywords: ["company signals", "evidence-fed copy", "why now", "copy blocker", "approval gate"],
1599
+ knowledgeSources: ["gtm/email-personalization", "gtm/signal-based-selling", "gtm/signal-combination-matrix", "gtm/advanced/buyer-journey-mapping"]
1478
1600
  },
1479
1601
  {
1480
1602
  slug: "domain-first-recovery",
@@ -1499,7 +1621,8 @@ var CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS = [
1499
1621
  "Target count means final held rows, not raw sourced rows."
1500
1622
  ],
1501
1623
  activationBoundary: "Domain recovery informs sourcing. Contact enrichment, paid qualification, and export still require approved gates.",
1502
- memoryKeywords: ["domain-first", "yield modeling", "attrition", "accepted domains", "gap fill"]
1624
+ memoryKeywords: ["domain-first", "yield modeling", "attrition", "accepted domains", "gap fill"],
1625
+ knowledgeSources: ["gtm/lead-generation-strategies", "data-ops/enrichment-waterfall-strategy", "data-ops/credit-optimization-guide", "gtm/territory-account-planning"]
1503
1626
  },
1504
1627
  {
1505
1628
  slug: "table-workflow-handoff",
@@ -1524,7 +1647,112 @@ var CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS = [
1524
1647
  "Provider route activation must dry-run before any external write."
1525
1648
  ],
1526
1649
  activationBoundary: "MCP, webhook, managed integration, and destination routes remain dry-run until the reviewed route and campaign are approved.",
1527
- memoryKeywords: ["table workflow", "request ready", "provider status", "export blocker", "BYO integration"]
1650
+ memoryKeywords: ["table workflow", "request ready", "provider status", "export blocker", "BYO integration"],
1651
+ knowledgeSources: ["gtm/multi-channel-orchestration", "data-ops/enrichment-waterfall-strategy", "recipes/clay-gtm-webhook-recipe", "systems/blueprints/multi-channel-orchestration"]
1652
+ },
1653
+ {
1654
+ slug: "icp-persona-segmentation",
1655
+ label: "ICP And Persona Segmentation",
1656
+ whenToUse: [
1657
+ "The brief has a broad market, multiple personas, or unclear buying committee.",
1658
+ "The campaign should rank account tiers, persona fit, vertical nuance, and exclusions before sourcing at scale."
1659
+ ],
1660
+ sequence: [
1661
+ "Convert the brief into account tiers, persona roles, buying committee influence, and explicit exclusions.",
1662
+ "Choose vertical guidance only from the customer-safe GTM library and keep internal platform docs out of prompts.",
1663
+ "Score accounts before contacts, then score contacts against persona, seniority, function, geography, and intent fit.",
1664
+ "Return segment counts and review buckets before spend, copy, export, or launch."
1665
+ ],
1666
+ requiredFields: {
1667
+ segment: ["segment_name", "tier", "persona_priority", "buying_committee_role", "fit_reason", "exclusion_reason"],
1668
+ scoring: ["account_fit_score", "persona_fit_score", "segment_confidence", "review_bucket"]
1669
+ },
1670
+ qualityGates: [
1671
+ "Do not treat adjacent personas or verticals as matches unless the brief allows them.",
1672
+ "Keep ICP, persona, and exclusion evidence visible in the proof packet.",
1673
+ "Low-confidence segments route to review before contact discovery or copy."
1674
+ ],
1675
+ activationBoundary: "Segmentation is planning guidance. Paid sourcing, enrichment, export, and launch require the normal approval gates.",
1676
+ memoryKeywords: ["ICP design", "persona segmentation", "buying committee", "account tiers", "fit scoring", "vertical nuance"],
1677
+ knowledgeSources: ["gtm/icp-design-framework", "gtm/personas/sdr.yaml", "gtm/personas/ae.yaml", "gtm/personas/cro.yaml", "gtm/personas/revops.yaml", "gtm/advanced/account-prioritization-frameworks", "gtm/verticals"]
1678
+ },
1679
+ {
1680
+ slug: "buying-signal-prioritization",
1681
+ label: "Buying Signal Prioritization",
1682
+ whenToUse: [
1683
+ "The campaign depends on timing, intent, trigger events, or account prioritization.",
1684
+ "The operator wants why-now proof instead of generic list generation."
1685
+ ],
1686
+ sequence: [
1687
+ "Map the campaign goal to signal families, trigger freshness, and buyer-journey stage.",
1688
+ "Rank signals by relevance, recency, source quality, and persona-specific actionability.",
1689
+ "Attach evidence URLs or evidence summaries without exposing private memory rows.",
1690
+ "Use signal strength to decide source order, copy angle, and review priority."
1691
+ ],
1692
+ requiredFields: {
1693
+ signal: ["signal_type", "signal_date", "signal_source", "why_now", "signal_confidence", "buyer_stage"],
1694
+ prioritization: ["priority_score", "priority_reason", "next_best_action", "review_reason"]
1695
+ },
1696
+ qualityGates: [
1697
+ "Do not use stale, unsupported, or weak signals as personalization proof.",
1698
+ "Signal fit cannot override hard ICP, geography, suppression, or verified-email gates.",
1699
+ "Rows with uncertain why-now evidence must stay review-only."
1700
+ ],
1701
+ activationBoundary: "Signal ranking can prioritize review and copy. It does not approve outreach, sender loading, exports, or sends.",
1702
+ memoryKeywords: ["buying signals", "intent data", "funding trigger", "new executive", "tech adoption", "buyer journey", "why now"],
1703
+ knowledgeSources: ["gtm/signal-based-selling", "gtm/signal-combination-matrix", "gtm/advanced/intent-data-strategies", "gtm/advanced/buyer-journey-mapping", "gtm/signals/funding-round", "gtm/signals/new-executive", "gtm/signals/tech-adoption", "recipes/funding-trigger-outreach", "recipes/new-hire-outreach"]
1704
+ },
1705
+ {
1706
+ slug: "multi-channel-sequence-fit",
1707
+ label: "Multi-Channel Sequence Fit",
1708
+ whenToUse: [
1709
+ "The campaign needs email, LinkedIn, call, webhook, CRM, or sequencer coordination.",
1710
+ "Copy and delivery should adapt by persona, channel, deliverability risk, and proof strength."
1711
+ ],
1712
+ sequence: [
1713
+ "Choose channel mix from persona, deal motion, evidence strength, deliverability constraints, and connected destination readiness.",
1714
+ "Draft sequence logic with email, LinkedIn, call, and task handoffs only where supported by the approved destination.",
1715
+ "Keep copy concise, proof-backed, channel-specific, and reviewable before delivery.",
1716
+ "Run deliverability and destination readiness checks before sender load or launch approval."
1717
+ ],
1718
+ requiredFields: {
1719
+ channel: ["channel", "touch_number", "persona_context", "message_angle", "channel_blocker", "approval_status"],
1720
+ deliverability: ["verified_email", "domain_risk", "send_window", "unsubscribe_risk", "sender_ready"]
1721
+ },
1722
+ qualityGates: [
1723
+ "Email copy must be row-supported and under the configured campaign tone/length limits.",
1724
+ "LinkedIn, phone, CRM, webhook, and sequencer actions remain destination-locked until approved.",
1725
+ "Deliverability risk or missing sender readiness blocks launch even when rows are qualified."
1726
+ ],
1727
+ activationBoundary: "Sequence strategy is draft-only. External writes, sender loading, launch, and sending need separate explicit approval.",
1728
+ memoryKeywords: ["outbound sequencing", "email personalization", "LinkedIn outreach", "cold calling", "multi-channel orchestration", "deliverability"],
1729
+ knowledgeSources: ["gtm/outbound-sequencing", "gtm/email-personalization", "gtm/linkedin-outreach-strategies", "gtm/cold-calling-frameworks", "gtm/multi-channel-orchestration", "gtm/advanced/email-deliverability", "systems/blueprints/multi-channel-orchestration"]
1730
+ },
1731
+ {
1732
+ slug: "revops-feedback-loop",
1733
+ label: "RevOps Feedback Loop",
1734
+ whenToUse: [
1735
+ "The campaign should improve from replies, meetings, delivery results, CRM outcomes, or operator QA.",
1736
+ "The operator needs post-build monitoring, governance, or continuous ops follow-up."
1737
+ ],
1738
+ sequence: [
1739
+ "Define success metrics, guardrails, and feedback sources before launch.",
1740
+ "Collect aggregate outcomes, QA blockers, delivery health, and operator edits after each campaign phase.",
1741
+ "Separate workspace-private evidence from public-safe learnings and Brain defaults.",
1742
+ "Feed approved aggregate learnings back into future sourcing, qualification, copy, and suppression rules."
1743
+ ],
1744
+ requiredFields: {
1745
+ feedback: ["source", "event_type", "metric", "sample_size", "confidence", "learning_candidate"],
1746
+ governance: ["approval_owner", "last_reviewed_at", "risk_flag", "next_review_action"]
1747
+ },
1748
+ qualityGates: [
1749
+ "Do not write memory or Brain learnings from raw replies, private labels, or provider payloads without approved redaction.",
1750
+ "Use holdouts or pre/post comparisons before claiming causal lift.",
1751
+ "Delivery, suppression, and opt-out signals must update future gates before scale-up."
1752
+ ],
1753
+ activationBoundary: "Feedback review is read-only until memory writes, Brain writes, CRM updates, or campaign changes are explicitly approved.",
1754
+ memoryKeywords: ["RevOps", "feedback loop", "campaign learning", "reply outcomes", "sales marketing alignment", "data hygiene", "holdout"],
1755
+ knowledgeSources: ["gtm/revenue-operations", "gtm/advanced/sales-marketing-alignment", "gtm/advanced/predictive-lead-scoring", "gtm/data-quality-ops", "data-ops/contact-data-lifecycle", "recipes/crm-data-hygiene", "gtm/playbooks/revops-automation-playbook"]
1528
1756
  }
1529
1757
  ];
1530
1758
  var CAMPAIGN_BUILDER_STRATEGY_TEMPLATES = [
@@ -1611,7 +1839,15 @@ var CAMPAIGN_BUILDER_STRATEGY_TEMPLATES = [
1611
1839
  agencyContext: {
1612
1840
  partnerEcosystem: ["signaliz", "instantly", "nango"]
1613
1841
  },
1614
- operatingPlaybookSlugs: ["net-new-suppressed-list", "proof-first-vertical-gate", "signal-led-copy-approval"]
1842
+ operatingPlaybookSlugs: [
1843
+ "net-new-suppressed-list",
1844
+ "proof-first-vertical-gate",
1845
+ "signal-led-copy-approval",
1846
+ "icp-persona-segmentation",
1847
+ "buying-signal-prioritization",
1848
+ "multi-channel-sequence-fit",
1849
+ "revops-feedback-loop"
1850
+ ]
1615
1851
  },
1616
1852
  {
1617
1853
  slug: "non-medical-home-care",
@@ -1684,7 +1920,15 @@ var CAMPAIGN_BUILDER_STRATEGY_TEMPLATES = [
1684
1920
  agencyContext: {
1685
1921
  partnerEcosystem: ["clay", "octave", "signaliz", "instantly", "nango"]
1686
1922
  },
1687
- operatingPlaybookSlugs: ["proof-first-vertical-gate", "domain-first-recovery", "table-workflow-handoff"]
1923
+ operatingPlaybookSlugs: [
1924
+ "proof-first-vertical-gate",
1925
+ "domain-first-recovery",
1926
+ "table-workflow-handoff",
1927
+ "icp-persona-segmentation",
1928
+ "buying-signal-prioritization",
1929
+ "multi-channel-sequence-fit",
1930
+ "revops-feedback-loop"
1931
+ ]
1688
1932
  },
1689
1933
  {
1690
1934
  slug: "agency-founder-led",
@@ -1745,7 +1989,15 @@ var CAMPAIGN_BUILDER_STRATEGY_TEMPLATES = [
1745
1989
  agencyContext: {
1746
1990
  partnerEcosystem: ["signaliz", "octave", "instantly", "nango"]
1747
1991
  },
1748
- operatingPlaybookSlugs: ["net-new-suppressed-list", "signal-led-copy-approval", "table-workflow-handoff"]
1992
+ operatingPlaybookSlugs: [
1993
+ "net-new-suppressed-list",
1994
+ "signal-led-copy-approval",
1995
+ "table-workflow-handoff",
1996
+ "icp-persona-segmentation",
1997
+ "buying-signal-prioritization",
1998
+ "multi-channel-sequence-fit",
1999
+ "revops-feedback-loop"
2000
+ ]
1749
2001
  },
1750
2002
  {
1751
2003
  slug: "cloud-infrastructure-displacement",
@@ -1832,7 +2084,15 @@ var CAMPAIGN_BUILDER_STRATEGY_TEMPLATES = [
1832
2084
  agencyContext: {
1833
2085
  partnerEcosystem: ["signaliz", "octave", "instantly", "nango"]
1834
2086
  },
1835
- operatingPlaybookSlugs: ["proof-first-vertical-gate", "domain-first-recovery", "signal-led-copy-approval"]
2087
+ operatingPlaybookSlugs: [
2088
+ "proof-first-vertical-gate",
2089
+ "domain-first-recovery",
2090
+ "signal-led-copy-approval",
2091
+ "icp-persona-segmentation",
2092
+ "buying-signal-prioritization",
2093
+ "multi-channel-sequence-fit",
2094
+ "revops-feedback-loop"
2095
+ ]
1836
2096
  }
1837
2097
  ];
1838
2098
  var CampaignBuilderAgent = class {
@@ -1863,6 +2123,9 @@ var CampaignBuilderAgent = class {
1863
2123
  const plan = await this.createPlan(request, options);
1864
2124
  return createCampaignBuilderReadiness(plan);
1865
2125
  }
2126
+ activeWorkContext(input) {
2127
+ return createCampaignBuilderActiveWorkContext(input);
2128
+ }
1866
2129
  async proof(request, options = {}) {
1867
2130
  const plan = await this.createPlan(request, options);
1868
2131
  const result = await this.dryRunPlan(plan, {
@@ -1904,6 +2167,7 @@ var CampaignBuilderAgent = class {
1904
2167
  },
1905
2168
  brain_defaults: plan.buildRequest.brainDefaults,
1906
2169
  brain_preflight: plan.buildRequest.brainPreflight,
2170
+ learning_holdout: plan.buildRequest.learningHoldout,
1907
2171
  campaign_strategy_context: plan.buildRequest.campaignStrategyContext,
1908
2172
  delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1909
2173
  delivery_risk: plan.buildRequest.deliveryRisk
@@ -1917,7 +2181,8 @@ var CampaignBuilderAgent = class {
1917
2181
  estimated_credits: plan.estimatedCredits
1918
2182
  },
1919
2183
  brain_delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1920
- delivery_risk: plan.buildRequest.deliveryRisk
2184
+ delivery_risk: plan.buildRequest.deliveryRisk,
2185
+ learning_holdout: plan.buildRequest.learningHoldout
1921
2186
  },
1922
2187
  approval_required: plan.approvals.some((approval) => approval.blocking),
1923
2188
  actor_type: "agent",
@@ -2245,9 +2510,10 @@ function createCampaignBuilderAgentPlan(request, context = {}) {
2245
2510
  const estimatedCredits = estimateCredits(resolvedRequest, defaults, targetCount);
2246
2511
  const buildRequest = createBuildRequest(resolvedRequest, defaults, targetCount, campaignName, context.scopeBuildArgs, routes, memory, context.scopeBrainPreflight);
2247
2512
  const brainPreflight = asRecord(buildRequest.brainPreflight);
2513
+ const evidence = buildRequest.evidence ?? null;
2248
2514
  const approvals = deriveApprovals(resolvedRequest, routes, memory, estimatedCredits, defaults.delivery?.approvalRequired !== false);
2249
2515
  return {
2250
- planId: `cbp_${hashString(JSON.stringify({ goal: resolvedRequest.goal, campaignName, targetCount, routes }))}`,
2516
+ planId: `cbp_${hashString(JSON.stringify({ goal: resolvedRequest.goal, campaignName, targetCount, routes, evidence }))}`,
2251
2517
  campaignName,
2252
2518
  goal: resolvedRequest.goal,
2253
2519
  targetCount,
@@ -2262,6 +2528,7 @@ function createCampaignBuilderAgentPlan(request, context = {}) {
2262
2528
  approvals,
2263
2529
  buildRequest,
2264
2530
  brainPreflight,
2531
+ evidence,
2265
2532
  operatingPlaybooks,
2266
2533
  mcpFlow: createMcpFlow(resolvedRequest, buildRequest, routes, memory, approvals),
2267
2534
  estimatedCredits,
@@ -2353,6 +2620,90 @@ function createCampaignBuilderReadiness(plan) {
2353
2620
  nextActions: campaignReadinessNextActions(plan, ready, customRoutes.length)
2354
2621
  };
2355
2622
  }
2623
+ function createCampaignBuilderActiveWorkContext(input) {
2624
+ const plan = input.readiness?.plan ?? input.plan;
2625
+ const remembered = input.remembered ?? {};
2626
+ const lastResult = asRecord(input.lastResult);
2627
+ const readinessBlockers = input.readiness?.summary.blockers ?? [];
2628
+ const explicitBlockers = input.blockers ?? [];
2629
+ const blockers = uniqueStrings([...explicitBlockers, ...readinessBlockers]);
2630
+ const warnings = uniqueStrings([
2631
+ ...input.warnings ?? [],
2632
+ ...input.readiness?.summary.warnings ?? [],
2633
+ ...plan?.warnings ?? []
2634
+ ]);
2635
+ const missingApprovals = plan ? (input.approval ? getMissingCampaignBuilderApprovals(plan, input.approval) : plan.approvals.filter((approval) => approval.blocking)).map((approval) => approval.type) : [];
2636
+ const refs = normalizeCampaignBuilderActiveWorkRefs({
2637
+ plan,
2638
+ remembered,
2639
+ refs: input.refs,
2640
+ lastResult
2641
+ });
2642
+ const state = inferCampaignBuilderActiveWorkState({
2643
+ plan,
2644
+ readiness: input.readiness,
2645
+ remembered,
2646
+ lastResult,
2647
+ blockers,
2648
+ missingApprovals
2649
+ });
2650
+ const nextSafeAction = campaignBuilderActiveWorkNextAction(state, blockers, missingApprovals, refs);
2651
+ const approvalPacket = createCampaignBuilderApprovalPacket(plan, input.approval, missingApprovals);
2652
+ const nextSafeMcpAction = campaignBuilderActiveWorkNextMcpAction({
2653
+ state,
2654
+ plan,
2655
+ refs,
2656
+ approvalPacket
2657
+ });
2658
+ const summary = {
2659
+ plan_id: plan?.planId ?? remembered.planId ?? null,
2660
+ campaign_name: plan?.campaignName ?? null,
2661
+ target_count: plan?.targetCount ?? null,
2662
+ gtm_campaign_id: plan?.buildRequest.gtmCampaignId ?? remembered.gtmCampaignId ?? null,
2663
+ campaign_build_id: stringValue(lastResult.campaign_build_id ?? lastResult.campaignBuildId) ?? remembered.campaignBuildId ?? null,
2664
+ provider_campaign_id: remembered.providerCampaignId ?? null
2665
+ };
2666
+ const approvalBoundary = campaignBuilderActiveWorkApprovalBoundary(missingApprovals, state);
2667
+ const conversationStarters = campaignBuilderActiveWorkConversationStarters({
2668
+ state,
2669
+ blockers,
2670
+ missingApprovals,
2671
+ refs,
2672
+ nextSafeAction
2673
+ });
2674
+ return {
2675
+ packet_version: "campaign-builder-active-work-context.v1",
2676
+ read_only: true,
2677
+ no_spend: true,
2678
+ no_provider_writes: true,
2679
+ private_safe: true,
2680
+ state,
2681
+ summary,
2682
+ refs,
2683
+ blockers,
2684
+ warnings,
2685
+ missing_approval_types: missingApprovals,
2686
+ approval_packet: approvalPacket,
2687
+ approval_boundary: approvalBoundary,
2688
+ next_safe_action: nextSafeAction,
2689
+ next_safe_mcp_action: nextSafeMcpAction,
2690
+ conversation_starters: conversationStarters,
2691
+ openai_agent_handoff: createCampaignBuilderOpenAiAgentHandoff({
2692
+ plan,
2693
+ state,
2694
+ summary,
2695
+ refs,
2696
+ blockers,
2697
+ warnings,
2698
+ missingApprovals,
2699
+ approvalPacket,
2700
+ approvalBoundary,
2701
+ nextSafeAction,
2702
+ nextSafeMcpAction,
2703
+ conversationStarters
2704
+ })
2705
+ };
2706
+ }
2356
2707
  function createCampaignBuilderProofReceipt(plan, result) {
2357
2708
  const strategyMemory = asRecord(plan.strategyMemoryStatus);
2358
2709
  const dryRunResult = asRecord(result);
@@ -2411,6 +2762,7 @@ function createCampaignBuilderProofReceipt(plan, result) {
2411
2762
  estimated_credits: plan.estimatedCredits,
2412
2763
  operating_playbooks: plan.operatingPlaybooks.map((playbook) => playbook.slug)
2413
2764
  },
2765
+ evidence: plan.evidence,
2414
2766
  gates,
2415
2767
  strategy_memory_status: summarizeCampaignBuilderProofStrategyMemory(strategyMemory),
2416
2768
  dry_run_result: summarizeCampaignBuilderProofDryRun(dryRunResult),
@@ -2459,6 +2811,56 @@ function getCampaignBuilderOperatingPlaybooksForRequest(request) {
2459
2811
  ]);
2460
2812
  return slugs.map(getCampaignBuilderOperatingPlaybook).filter((playbook) => Boolean(playbook));
2461
2813
  }
2814
+ function normalizeCampaignBuilderEvidence(evidence) {
2815
+ if (!evidence) return null;
2816
+ const attachmentSummary = (evidence.attachments ?? []).filter((attachment) => stringValue(attachment.name)).slice(0, 5).map((attachment) => ({
2817
+ name: stringValue(attachment.name) ?? "attachment",
2818
+ kind: stringValue(attachment.kind) ?? null,
2819
+ media_type: stringValue(attachment.mediaType) ?? null,
2820
+ redacted: attachment.redacted !== false,
2821
+ columns: uniqueStrings(attachment.columns ?? []).slice(0, 25),
2822
+ row_count: numberOrUndefined2(attachment.rowCount) ?? null,
2823
+ content_summary: stringValue(attachment.contentSummary)?.slice(0, 500) ?? null,
2824
+ evidence_fields: uniqueStrings(attachment.evidenceFields ?? []).slice(0, 25)
2825
+ }));
2826
+ const urlSummary = (evidence.urls ?? []).filter((url) => stringValue(url.url)).slice(0, 8).map((url) => {
2827
+ const rawUrl = stringValue(url.url) ?? "";
2828
+ return {
2829
+ url: rawUrl,
2830
+ host: stringValue(url.host) ?? hostFromUrl(rawUrl) ?? null,
2831
+ type: stringValue(url.type) ?? null,
2832
+ label: stringValue(url.label) ?? null,
2833
+ summary: stringValue(url.summary)?.slice(0, 500) ?? null,
2834
+ redacted: url.redacted === true
2835
+ };
2836
+ });
2837
+ const operatorNotes = uniqueStrings(evidence.notes ?? []).map((note) => note.slice(0, 240)).slice(0, 5);
2838
+ if (!attachmentSummary.length && !urlSummary.length && !operatorNotes.length) return null;
2839
+ return {
2840
+ packet_version: "campaign-builder-evidence.v1",
2841
+ private_safe: true,
2842
+ attachment_summary: attachmentSummary,
2843
+ url_summary: urlSummary,
2844
+ operator_notes: operatorNotes,
2845
+ privacy_boundary: "Use these attachment and URL summaries as planning evidence only. Do not expose raw private attachment text, raw rows, leads, emails, provider payloads, copy bodies, or secrets.",
2846
+ raw_private_fields_withheld: [
2847
+ "raw_private_attachment_text",
2848
+ "raw_attachment_rows",
2849
+ "raw_leads",
2850
+ "email_addresses",
2851
+ "provider_auth_payloads",
2852
+ "copy_bodies",
2853
+ "secrets"
2854
+ ]
2855
+ };
2856
+ }
2857
+ function hostFromUrl(value) {
2858
+ try {
2859
+ return new URL(value).hostname || void 0;
2860
+ } catch {
2861
+ return void 0;
2862
+ }
2863
+ }
2462
2864
  function applyCampaignBuilderStrategyTemplate(request) {
2463
2865
  const template = getCampaignBuilderStrategyTemplate(request.strategyTemplate);
2464
2866
  if (!template) return request;
@@ -3150,7 +3552,7 @@ function normalizeMemory(request) {
3150
3552
  };
3151
3553
  }
3152
3554
  function defaultAgencyMemoryQueries(request, configured) {
3153
- const operatingPlaybookTerms = getCampaignBuilderOperatingPlaybooksForRequest(request).flatMap((playbook) => [playbook.label, ...playbook.memoryKeywords]).join(" ");
3555
+ const operatingPlaybookTerms = getCampaignBuilderOperatingPlaybooksForRequest(request).flatMap((playbook) => [playbook.label, ...playbook.memoryKeywords, ...playbook.knowledgeSources]).join(" ");
3154
3556
  const queries = [{
3155
3557
  query: request.goal,
3156
3558
  scopes: ["workspace", configured.useNetworkPatterns ? "network_patterns" : "operator_notes"],
@@ -3316,6 +3718,21 @@ function routesFromCustomerTools(tools) {
3316
3718
  }))
3317
3719
  );
3318
3720
  }
3721
+ function normalizeCampaignBuilderLearningHoldout(input, gtmCampaignId, campaignName) {
3722
+ const source = asRecord(input);
3723
+ if (source.enabled === false) return { enabled: false };
3724
+ const controlPercentage = numberOrUndefined2(source.control_percentage ?? source.controlPercentage);
3725
+ const minControlSize = numberOrUndefined2(source.min_control_size ?? source.minControlSize);
3726
+ const experimentKey = stringValue(source.experiment_key ?? source.experimentKey) ?? (gtmCampaignId ? `gtm_campaign:${gtmCampaignId}:learning_holdout` : `campaign_builder_agent:${hashString(campaignName)}:learning_holdout`);
3727
+ return {
3728
+ enabled: true,
3729
+ controlPercentage: controlPercentage ?? 0.2,
3730
+ minControlSize: minControlSize ?? 50,
3731
+ experimentKey,
3732
+ sourceCampaignId: stringValue(source.source_campaign_id ?? source.sourceCampaignId) ?? gtmCampaignId,
3733
+ primaryMetric: stringValue(source.primary_metric ?? source.primaryMetric) ?? "positive_reply_rate"
3734
+ };
3735
+ }
3319
3736
  function createBuildRequest(request, defaults, targetCount, campaignName, scopeBuildArgs, routes = [], memory, scopeBrainPreflight) {
3320
3737
  const buildRequest = {
3321
3738
  name: campaignName,
@@ -3346,8 +3763,15 @@ function createBuildRequest(request, defaults, targetCount, campaignName, scopeB
3346
3763
  fillPolicy: "aggressive"
3347
3764
  }
3348
3765
  };
3766
+ const evidence = normalizeCampaignBuilderEvidence(request.evidence);
3767
+ if (evidence) buildRequest.evidence = evidence;
3349
3768
  const scoped = asRecord(scopeBuildArgs);
3350
3769
  buildRequest.gtmCampaignId = request.gtmCampaignId ?? stringValue(scoped.gtm_campaign_id ?? scoped.gtmCampaignId);
3770
+ buildRequest.learningHoldout = normalizeCampaignBuilderLearningHoldout(
3771
+ request.learningHoldout ?? scoped.learning_holdout ?? scoped.learningHoldout,
3772
+ buildRequest.gtmCampaignId,
3773
+ campaignName
3774
+ );
3351
3775
  if (typeof scoped.name === "string") buildRequest.name = scoped.name;
3352
3776
  if (typeof scoped.prompt === "string") buildRequest.prompt = scoped.prompt;
3353
3777
  if (typeof scoped.target_count === "number") buildRequest.targetCount = scoped.target_count;
@@ -3372,6 +3796,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3372
3796
  memory.enabled ? "feedback" : void 0
3373
3797
  ]);
3374
3798
  const providerChain = uniqueStrings(["signaliz", ...request.preferredProviders ?? [], ...routes.map((route) => route.provider)]);
3799
+ const evidence = buildRequest.evidence ?? normalizeCampaignBuilderEvidence(request.evidence);
3375
3800
  const targetIcp = compact({
3376
3801
  personas: buildRequest.icp?.personas,
3377
3802
  industries: buildRequest.icp?.industries,
@@ -3384,6 +3809,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3384
3809
  const learningArgs = compact({
3385
3810
  campaign_brief: request.goal,
3386
3811
  target_icp: targetIcp,
3812
+ evidence_context: evidence,
3387
3813
  layers,
3388
3814
  provider_chain: providerChain,
3389
3815
  lead_source: providerChain[0] ?? "signaliz",
@@ -3395,6 +3821,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3395
3821
  const defaultsArgs = compact({
3396
3822
  campaign_brief: request.goal,
3397
3823
  target_icp: targetIcp,
3824
+ evidence_context: evidence,
3398
3825
  layers,
3399
3826
  include_global: memory.useNetworkPatterns,
3400
3827
  include_memory: memory.enabled,
@@ -3405,6 +3832,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3405
3832
  provider_chain: providerChain,
3406
3833
  lead_source: providerChain[0] ?? "signaliz",
3407
3834
  target_icp: targetIcp,
3835
+ evidence_context: evidence,
3408
3836
  include_global: memory.useNetworkPatterns,
3409
3837
  limit: 25
3410
3838
  });
@@ -3417,11 +3845,14 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3417
3845
  label: playbook.label,
3418
3846
  quality_gates: playbook.qualityGates,
3419
3847
  required_fields: playbook.requiredFields,
3420
- activation_boundary: playbook.activationBoundary
3848
+ activation_boundary: playbook.activationBoundary,
3849
+ knowledge_sources: playbook.knowledgeSources
3421
3850
  })),
3422
3851
  target_icp: targetIcp,
3852
+ evidence_context: evidence,
3423
3853
  provider_chain: providerChain,
3424
3854
  lead_source: providerChain[0] ?? "signaliz",
3855
+ learning_holdout: buildRequest.learningHoldout,
3425
3856
  tool_sequence: [
3426
3857
  { tool: "gtm_brain_learning_cycle_plan", purpose: "Plan the Brain learning loop for this campaign.", arguments: learningArgs },
3427
3858
  { tool: "gtm_brain_seed_defaults", purpose: "Seed campaign defaults from workspace memory and safe aggregate patterns.", arguments: defaultsArgs },
@@ -3691,7 +4122,7 @@ function createMcpFlow(request, buildRequest, routes, memory, approvals) {
3691
4122
  phase: "plan",
3692
4123
  tool: "nango_mcp_tools_list",
3693
4124
  arguments: {
3694
- format: "mcp"
4125
+ format: "openai"
3695
4126
  },
3696
4127
  readOnly: true,
3697
4128
  approvalRequired: false
@@ -4000,6 +4431,7 @@ function buildGtmCampaignBuildPlanArgs(request, buildRequest, routes, memory) {
4000
4431
  layers: layers.length > 0 ? layers : void 0,
4001
4432
  preferred_providers: preferredProviders.length > 0 ? preferredProviders : void 0,
4002
4433
  strategy_model: strategyModel,
4434
+ evidence_context: buildRequest.evidence ?? normalizeCampaignBuilderEvidence(request.evidence),
4003
4435
  include_strategy_patterns: campaignBuilderStrategyPatternsEnabled(agencyContext),
4004
4436
  include_workflow_patterns: campaignBuilderWorkflowPatternsEnabled(agencyContext),
4005
4437
  include_nango_catalog: agencyContext.includeNangoCatalog !== false,
@@ -4008,7 +4440,8 @@ function buildGtmCampaignBuildPlanArgs(request, buildRequest, routes, memory) {
4008
4440
  slug: playbook.slug,
4009
4441
  label: playbook.label,
4010
4442
  quality_gates: playbook.qualityGates,
4011
- activation_boundary: playbook.activationBoundary
4443
+ activation_boundary: playbook.activationBoundary,
4444
+ knowledge_sources: playbook.knowledgeSources
4012
4445
  })),
4013
4446
  include_memory: memory.enabled,
4014
4447
  include_brain: true,
@@ -4089,8 +4522,19 @@ function buildCampaignArgs(request) {
4089
4522
  if (request.dedupKeys) args.dedup_keys = request.dedupKeys;
4090
4523
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
4091
4524
  if (request.brainDefaults) args.brain_defaults = request.brainDefaults;
4525
+ if (request.learningHoldout) {
4526
+ args.learning_holdout = compact({
4527
+ enabled: request.learningHoldout.enabled,
4528
+ control_percentage: request.learningHoldout.controlPercentage,
4529
+ min_control_size: request.learningHoldout.minControlSize,
4530
+ experiment_key: request.learningHoldout.experimentKey,
4531
+ source_campaign_id: request.learningHoldout.sourceCampaignId,
4532
+ primary_metric: request.learningHoldout.primaryMetric
4533
+ });
4534
+ }
4092
4535
  if (request.campaignStrategyContext) args.campaign_strategy_context = request.campaignStrategyContext;
4093
4536
  if (request.deliveryRisk) args.delivery_risk = request.deliveryRisk;
4537
+ if (request.evidence) args.evidence_context = request.evidence;
4094
4538
  if (request.icp) {
4095
4539
  args.icp = compact({
4096
4540
  personas: request.icp.personas,
@@ -4140,7 +4584,8 @@ function buildCampaignArgs(request) {
4140
4584
  max_body_words: request.copy.maxBodyWords,
4141
4585
  sender_context: request.copy.senderContext,
4142
4586
  offer_context: request.copy.offerContext,
4143
- model: request.copy.model
4587
+ model: request.copy.model,
4588
+ evidence_mode: request.copy.evidenceMode
4144
4589
  });
4145
4590
  }
4146
4591
  if (request.delivery) {
@@ -4185,6 +4630,7 @@ function buildExecutionPrepareArgs(request, dryRun, confirmSpend) {
4185
4630
  delivery_risk: buildArgs2.delivery_risk,
4186
4631
  dedup_keys: buildArgs2.dedup_keys,
4187
4632
  policy: buildArgs2.policy,
4633
+ learning_holdout: buildArgs2.learning_holdout,
4188
4634
  signals: buildArgs2.signals,
4189
4635
  qualification: buildArgs2.qualification,
4190
4636
  copy: buildArgs2.copy,
@@ -4219,10 +4665,145 @@ function mapBuildResult(data, dryRun) {
4219
4665
  targetLimitReason: data.target_limit_reason,
4220
4666
  maxSupportedTargetCount: data.max_supported_target_count,
4221
4667
  brainContext: data.brain_context ?? {},
4668
+ learningHoldout: data.learning_holdout ?? {},
4222
4669
  providerRoute: mapProviderRoute2(data)
4223
4670
  };
4224
4671
  }
4672
+ var CAMPAIGN_BUILDER_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "canceled", "cancelled"]);
4673
+ var CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["failed", "canceled", "cancelled"]);
4674
+ function mapAgentCampaignPhaseHealth(value) {
4675
+ if (!Array.isArray(value)) return [];
4676
+ return value.map((phase) => {
4677
+ const record = asRecord(phase);
4678
+ return {
4679
+ phase: stringValue(record.phase) ?? "unknown",
4680
+ status: stringValue(record.status) ?? "unknown",
4681
+ recordsProcessed: numberOrNull2(record.records_processed ?? record.recordsProcessed),
4682
+ recordsSucceeded: numberOrNull2(record.records_succeeded ?? record.recordsSucceeded),
4683
+ recordsFailed: numberOrNull2(record.records_failed ?? record.recordsFailed),
4684
+ heartbeatAgeSeconds: numberOrNull2(record.heartbeat_age_seconds ?? record.heartbeatAgeSeconds ?? record.phase_age_seconds)
4685
+ };
4686
+ });
4687
+ }
4688
+ function deriveCampaignBuildTerminalState(input) {
4689
+ const status = input.status.toLowerCase();
4690
+ const heartbeatAgeSeconds = input.phaseAgeSeconds;
4691
+ const stale = input.staleRunningPhase === true;
4692
+ const safeWatchAction = input.triggerRunId ? `signaliz ops run-status ${input.triggerRunId} --watch` : "signaliz campaign-agent status <campaign_build_id>";
4693
+ if (CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES.has(status)) {
4694
+ return {
4695
+ kind: "blocked",
4696
+ label: "Blocked terminal state",
4697
+ reviewable: true,
4698
+ terminal: true,
4699
+ stale: false,
4700
+ phase: input.phase,
4701
+ phaseStatus: input.phaseStatus,
4702
+ heartbeatAgeSeconds,
4703
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4704
+ terminalReason: input.terminalReason,
4705
+ staleReason: null,
4706
+ safeNextAction: input.nextAction || "Review errors and rerun only after the blocker is resolved."
4707
+ };
4708
+ }
4709
+ if (status === "pending_approval") {
4710
+ return {
4711
+ kind: "pending_approval",
4712
+ label: "Pending delivery approval",
4713
+ reviewable: true,
4714
+ terminal: false,
4715
+ stale: false,
4716
+ phase: input.phase,
4717
+ phaseStatus: input.phaseStatus,
4718
+ heartbeatAgeSeconds,
4719
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4720
+ terminalReason: input.terminalReason,
4721
+ staleReason: null,
4722
+ safeNextAction: input.approvalAction || input.nextAction || "Review rows and approve delivery only after the approval packet passes."
4723
+ };
4724
+ }
4725
+ if (CAMPAIGN_BUILDER_TERMINAL_STATUSES.has(status)) {
4726
+ return {
4727
+ kind: "terminal",
4728
+ label: "Terminal and reviewable",
4729
+ reviewable: true,
4730
+ terminal: true,
4731
+ stale: false,
4732
+ phase: input.phase,
4733
+ phaseStatus: input.phaseStatus,
4734
+ heartbeatAgeSeconds,
4735
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4736
+ terminalReason: input.terminalReason,
4737
+ staleReason: null,
4738
+ safeNextAction: input.nextAction || "Review rows, artifacts, scorecard, and approval locks before delivery."
4739
+ };
4740
+ }
4741
+ if (status === "running" || status === "queued" || status === "processing") {
4742
+ if (stale) {
4743
+ return {
4744
+ kind: "running_stale",
4745
+ label: "Running but stale",
4746
+ reviewable: false,
4747
+ terminal: false,
4748
+ stale: true,
4749
+ phase: input.phase,
4750
+ phaseStatus: input.phaseStatus,
4751
+ heartbeatAgeSeconds,
4752
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4753
+ terminalReason: null,
4754
+ staleReason: input.staleReason || "The active phase heartbeat is stale.",
4755
+ safeNextAction: input.nextAction || safeWatchAction
4756
+ };
4757
+ }
4758
+ return {
4759
+ kind: "running_healthy",
4760
+ label: "Running and healthy",
4761
+ reviewable: false,
4762
+ terminal: false,
4763
+ stale: false,
4764
+ phase: input.phase,
4765
+ phaseStatus: input.phaseStatus,
4766
+ heartbeatAgeSeconds,
4767
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4768
+ terminalReason: null,
4769
+ staleReason: null,
4770
+ safeNextAction: input.nextAction || safeWatchAction
4771
+ };
4772
+ }
4773
+ return {
4774
+ kind: "unknown",
4775
+ label: "Unknown build state",
4776
+ reviewable: false,
4777
+ terminal: false,
4778
+ stale,
4779
+ phase: input.phase,
4780
+ phaseStatus: input.phaseStatus,
4781
+ heartbeatAgeSeconds,
4782
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4783
+ terminalReason: input.terminalReason,
4784
+ staleReason: input.staleReason,
4785
+ safeNextAction: input.nextAction || "Read campaign build status again before making a launch or delivery decision."
4786
+ };
4787
+ }
4225
4788
  function mapAgentCampaignBuildStatus(data) {
4789
+ const customerRowCounts = mapAgentCustomerRowCounts(data.customer_row_counts);
4790
+ const phaseHealth = mapAgentCampaignPhaseHealth(data.phases);
4791
+ const terminalReason = stringValue(data.terminal_reason) ?? customerRowCounts?.terminalReason ?? null;
4792
+ const staleReason = stringValue(data.stale_reason) ?? stringValue(asRecord(data.diagnostics).stale_reason) ?? null;
4793
+ const nextPollAfterSeconds = numberOrNull2(data.next_poll_after_seconds ?? data.nextPollAfterSeconds);
4794
+ const terminalState = deriveCampaignBuildTerminalState({
4795
+ status: stringValue(data.status) ?? "unknown",
4796
+ phase: stringValue(data.current_phase) ?? null,
4797
+ phaseStatus: stringValue(data.current_phase_status) ?? null,
4798
+ staleRunningPhase: data.stale_running_phase === true,
4799
+ phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4800
+ nextPollAfterSeconds,
4801
+ terminalReason,
4802
+ staleReason,
4803
+ nextAction: formatAgentNextAction(data.next_action),
4804
+ approvalAction: formatAgentNextAction(data.approval_action),
4805
+ triggerRunId: stringValue(data.trigger_run_id) ?? null
4806
+ });
4226
4807
  return {
4227
4808
  campaignBuildId: stringValue(data.campaign_build_id) ?? "",
4228
4809
  campaignId: stringValue(data.campaign_id) ?? null,
@@ -4240,12 +4821,18 @@ function mapAgentCampaignBuildStatus(data) {
4240
4821
  errors: diagnosticMessages2(data.errors),
4241
4822
  artifactCount: numberOrUndefined2(data.artifact_count) ?? 0,
4242
4823
  artifactDownloads: Array.isArray(data.artifact_downloads) ? data.artifact_downloads.map(mapAgentCampaignArtifactDownload) : [],
4243
- customerRowCounts: mapAgentCustomerRowCounts(data.customer_row_counts),
4824
+ customerRowCounts,
4244
4825
  phaseAttemptTotals: nullableRecord(data.phase_attempt_totals) ?? void 0,
4245
4826
  providerRoute: mapProviderRoute2(data),
4246
4827
  triggerRunId: stringValue(data.trigger_run_id) ?? null,
4247
4828
  staleRunningPhase: data.stale_running_phase === true,
4248
4829
  phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4830
+ nextPollAfterSeconds,
4831
+ terminalReason,
4832
+ staleReason,
4833
+ safeNextAction: terminalState.safeNextAction,
4834
+ terminalState,
4835
+ phaseHealth,
4249
4836
  diagnostics: nullableRecord(data.diagnostics) ?? {},
4250
4837
  nextAction: formatAgentNextAction(data.next_action),
4251
4838
  approvalAction: formatAgentNextAction(data.approval_action),
@@ -4275,6 +4862,7 @@ function mapAgentCustomerRowCounts(value) {
4275
4862
  acceptedRows: numberOrUndefined2(record.accepted_rows),
4276
4863
  usableRows: numberOrUndefined2(record.usable_rows),
4277
4864
  copiedRows: numberOrUndefined2(record.copied_rows),
4865
+ copySkippedRows: numberOrUndefined2(record.copy_skipped_rows),
4278
4866
  approvalReadyRows: numberOrUndefined2(record.approval_ready_rows),
4279
4867
  qaReadyRows: numberOrUndefined2(record.qa_ready_rows),
4280
4868
  deliveredRows: numberOrUndefined2(record.delivered_rows),
@@ -4333,67 +4921,285 @@ function mapAgentCampaignArtifact(data) {
4333
4921
  createdAt: stringValue(data.created_at) ?? ""
4334
4922
  };
4335
4923
  }
4924
+ var NORTH_STAR_BANNED_COPY_PHRASES = [
4925
+ "i have been following",
4926
+ "i saw that",
4927
+ "would you be opposed",
4928
+ "unlock new opportunities",
4929
+ "significant success",
4930
+ "we understand",
4931
+ "hope this finds you well"
4932
+ ];
4933
+ function campaignReviewGate(id, label, status, detail) {
4934
+ return {
4935
+ id,
4936
+ label,
4937
+ status,
4938
+ passed: status !== "blocked",
4939
+ detail
4940
+ };
4941
+ }
4942
+ function northStarMetric(id, label, status, detail, value, target) {
4943
+ return {
4944
+ id,
4945
+ label,
4946
+ status,
4947
+ detail,
4948
+ ...value !== void 0 ? { value } : {},
4949
+ ...target !== void 0 ? { target } : {}
4950
+ };
4951
+ }
4952
+ function createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState) {
4953
+ if (!terminalState.reviewable) {
4954
+ const pendingMetric = northStarMetric(
4955
+ "terminal_state",
4956
+ "Terminal-state contract",
4957
+ terminalState.kind === "running_stale" ? "blocked" : "pending",
4958
+ terminalState.kind === "running_stale" ? `Build is stale in ${terminalState.phase || "unknown phase"}; do not grade output yet.` : `Build is ${terminalState.label.toLowerCase()}; wait for terminal or approval state before grading.`,
4959
+ terminalState.kind,
4960
+ "terminal or pending_approval"
4961
+ );
4962
+ return {
4963
+ version: "campaign-builder-north-star.v1",
4964
+ status: pendingMetric.status,
4965
+ score: null,
4966
+ moatState: "scaffolding_ready",
4967
+ metrics: [pendingMetric],
4968
+ blockers: pendingMetric.status === "blocked" ? [pendingMetric.detail] : [],
4969
+ warnings: pendingMetric.status === "pending" ? [pendingMetric.detail] : [],
4970
+ nextAction: terminalState.safeNextAction
4971
+ };
4972
+ }
4973
+ const sampledRows = rows.rows.length;
4974
+ const rowData = rows.rows.map((row) => asRecord(row.data));
4975
+ const requestedTarget = status.customerRowCounts?.requestedTarget ?? status.recordsTotal ?? rows.count;
4976
+ const finalRows = status.customerRowCounts?.deliveredRows ?? status.customerRowCounts?.qaReadyRows ?? status.customerRowCounts?.copySkippedRows ?? status.customerRowCounts?.copiedRows ?? status.customerRowCounts?.acceptedRows ?? rows.count;
4977
+ const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4978
+ const verifiedEmailRows = rowData.filter(campaignReviewDataHasVerifiedEmail).length;
4979
+ const uniqueEmails = new Set(rowData.map(campaignReviewDataEmail).filter(Boolean)).size;
4980
+ const uniqueDomains = new Set(rowData.map(campaignReviewDataDomain).filter(Boolean)).size;
4981
+ const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4982
+ const copyQaIssuesByRow = rowData.map(campaignReviewDataCopyQaIssues).filter((issues) => issues.length > 0);
4983
+ const copyQaIssueCounts = countCampaignReviewIssues(copyQaIssuesByRow);
4984
+ const signalBackedRows = rowData.filter(campaignReviewDataHasSignalBasis).length;
4985
+ const noSupportedSignalRows = rowData.filter(campaignReviewDataHasNoSupportedSignal).length;
4986
+ const unsupportedSignalClaims = rowData.filter(campaignReviewDataHasUnsupportedSignalClaim).length;
4987
+ const approvalLockedRows = rowData.filter(campaignReviewDataHasApprovalLock).length;
4988
+ const holdoutRows = rowData.filter(campaignReviewDataHasHoldoutAttribution).length;
4989
+ const artifactCount = artifacts.length || status.artifactCount || 0;
4990
+ const deliveredRows = status.customerRowCounts?.deliveredRows ?? 0;
4991
+ const feedbackReady = campaignReviewStatusHasFeedbackReadiness(status);
4992
+ const feedbackRequiredForApproval = terminalState.kind === "pending_approval";
4993
+ const moat = campaignReviewMoatState(status, holdoutRows);
4994
+ const metrics = [
4995
+ northStarMetric(
4996
+ "terminal_state",
4997
+ "Terminal-state contract",
4998
+ terminalState.kind === "blocked" ? "blocked" : "pass",
4999
+ terminalState.label,
5000
+ terminalState.kind,
5001
+ "terminal or pending_approval"
5002
+ ),
5003
+ northStarMetric(
5004
+ "target_fill",
5005
+ "Target fill",
5006
+ requestedTarget && finalRows < requestedTarget ? "review" : "pass",
5007
+ requestedTarget ? `${finalRows || 0}/${requestedTarget} final or reviewable row(s) are accounted for.` : "No requested target was reported; using sampled rows for review.",
5008
+ finalRows || sampledRows,
5009
+ requestedTarget || sampledRows
5010
+ ),
5011
+ northStarMetric(
5012
+ "unique_emails",
5013
+ "Unique emails",
5014
+ sampledRows === 0 ? "blocked" : uniqueEmails === rowsWithEmail ? "pass" : "blocked",
5015
+ sampledRows === 0 ? "No sampled rows were available for email uniqueness review." : `${uniqueEmails}/${rowsWithEmail} sampled email(s) are unique.`,
5016
+ uniqueEmails,
5017
+ rowsWithEmail
5018
+ ),
5019
+ northStarMetric(
5020
+ "unique_domains",
5021
+ "Unique domains",
5022
+ sampledRows === 0 ? "blocked" : uniqueDomains === sampledRows ? "pass" : "review",
5023
+ sampledRows === 0 ? "No sampled rows were available for domain uniqueness review." : `${uniqueDomains}/${sampledRows} sampled company domain(s) are unique.`,
5024
+ uniqueDomains,
5025
+ sampledRows
5026
+ ),
5027
+ northStarMetric(
5028
+ "verified_email_rows",
5029
+ "Verified email rows",
5030
+ sampledRows === 0 ? "blocked" : verifiedEmailRows === sampledRows ? "pass" : rowsWithEmail === sampledRows ? "review" : "blocked",
5031
+ sampledRows === 0 ? "No sampled rows were available for email verification review." : `${verifiedEmailRows}/${sampledRows} sampled row(s) carry verified-email evidence.`,
5032
+ verifiedEmailRows,
5033
+ sampledRows
5034
+ ),
5035
+ northStarMetric(
5036
+ "copy_completeness",
5037
+ "Copy completeness",
5038
+ sampledRows === 0 ? "blocked" : copyReadyRows === sampledRows ? "pass" : "blocked",
5039
+ sampledRows === 0 ? "No sampled rows were available for copy review." : `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.`,
5040
+ copyReadyRows,
5041
+ sampledRows
5042
+ ),
5043
+ northStarMetric(
5044
+ "copy_quality",
5045
+ "Copy QA",
5046
+ copyQaIssuesByRow.length === 0 ? "pass" : "blocked",
5047
+ copyQaIssuesByRow.length === 0 ? "No sampled copy QA blockers were detected." : `${copyQaIssuesByRow.length}/${sampledRows} sampled row(s) have copy QA issue(s): ${formatCampaignReviewIssueCounts(copyQaIssueCounts)}.`,
5048
+ copyQaIssuesByRow.length,
5049
+ 0
5050
+ ),
5051
+ northStarMetric(
5052
+ "signal_grounding",
5053
+ "Signal grounding",
5054
+ unsupportedSignalClaims > 0 ? "blocked" : signalBackedRows > 0 ? "pass" : "review",
5055
+ unsupportedSignalClaims > 0 ? `${unsupportedSignalClaims} sampled row(s) make unsupported signal claims.` : signalBackedRows > 0 ? `${signalBackedRows}/${sampledRows} sampled row(s) include supported signal or personalization basis.` : noSupportedSignalRows > 0 ? `${noSupportedSignalRows}/${sampledRows} sampled row(s) are explicitly marked no_supported_signal; copy should stay conservative.` : "No sampled rows include supported signal evidence; copy should stay conservative.",
5056
+ signalBackedRows,
5057
+ sampledRows
5058
+ ),
5059
+ northStarMetric(
5060
+ "approval_locks",
5061
+ "Approval locks",
5062
+ approvalLockedRows > 0 || deliveredRows === 0 ? "pass" : "review",
5063
+ approvalLockedRows > 0 ? `${approvalLockedRows}/${sampledRows} sampled row(s) show approval/export lock metadata.` : "Approval-lock metadata was not visible in sampled rows; preserve delivery approval gates.",
5064
+ approvalLockedRows,
5065
+ sampledRows
5066
+ ),
5067
+ northStarMetric(
5068
+ "export_send_safety",
5069
+ "Export/send safety",
5070
+ deliveredRows > 0 && status.status !== "completed" ? "blocked" : "pass",
5071
+ deliveredRows > 0 ? `${deliveredRows} row(s) are delivered; status is ${status.status}.` : "No delivered rows are reported before approval.",
5072
+ deliveredRows,
5073
+ 0
5074
+ ),
5075
+ northStarMetric(
5076
+ "holdout_attribution",
5077
+ "Holdout attribution",
5078
+ holdoutRows > 0 ? "pass" : "review",
5079
+ holdoutRows > 0 ? `${holdoutRows}/${sampledRows} sampled row(s) preserve holdout attribution.` : "No holdout attribution was visible in the sampled rows.",
5080
+ holdoutRows,
5081
+ sampledRows
5082
+ ),
5083
+ northStarMetric(
5084
+ "feedback_readiness",
5085
+ "Feedback readiness",
5086
+ feedbackReady ? "pass" : feedbackRequiredForApproval ? "blocked" : "review",
5087
+ feedbackReady ? "Feedback readiness is visible in status diagnostics." : feedbackRequiredForApproval ? "Feedback webhook or approved pull readiness is required before delivery approval." : "Feedback webhook or approved pull readiness was not visible; require it before real send approval.",
5088
+ feedbackReady,
5089
+ true
5090
+ ),
5091
+ northStarMetric(
5092
+ "causal_moat",
5093
+ "Causal moat",
5094
+ moat.state === "proved" ? "pass" : "review",
5095
+ moat.detail,
5096
+ moat.state,
5097
+ "proved"
5098
+ ),
5099
+ northStarMetric(
5100
+ "artifact_readback",
5101
+ "Artifact readback",
5102
+ artifactCount > 0 ? "pass" : "review",
5103
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available for review.` : "No review artifact is available yet.",
5104
+ artifactCount,
5105
+ 1
5106
+ )
5107
+ ];
5108
+ const gradedMetrics = metrics.filter((metric) => metric.status !== "pending");
5109
+ const score = gradedMetrics.length === 0 ? null : Math.round(gradedMetrics.filter((metric) => metric.status === "pass").length / gradedMetrics.length * 100);
5110
+ const blockers = metrics.filter((metric) => metric.status === "blocked").map((metric) => metric.detail);
5111
+ const warnings = metrics.filter((metric) => metric.status === "review").map((metric) => metric.detail);
5112
+ const scorecardStatus = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "review" : "pass";
5113
+ return {
5114
+ version: "campaign-builder-north-star.v1",
5115
+ status: scorecardStatus,
5116
+ score,
5117
+ moatState: moat.state,
5118
+ metrics,
5119
+ blockers,
5120
+ warnings,
5121
+ nextAction: scorecardStatus === "blocked" ? "Fix blocked North Star gates before approval or delivery." : terminalState.safeNextAction
5122
+ };
5123
+ }
4336
5124
  function createCampaignBuilderBuildReview(status, rows, artifacts) {
5125
+ const terminalState = status.terminalState ?? deriveCampaignBuildTerminalState({
5126
+ status: status.status,
5127
+ phase: status.currentPhase,
5128
+ phaseStatus: status.currentPhaseStatus,
5129
+ staleRunningPhase: status.staleRunningPhase === true,
5130
+ phaseAgeSeconds: status.phaseAgeSeconds ?? null,
5131
+ nextPollAfterSeconds: status.nextPollAfterSeconds ?? null,
5132
+ terminalReason: status.terminalReason ?? status.customerRowCounts?.terminalReason ?? null,
5133
+ staleReason: status.staleReason ?? null,
5134
+ nextAction: status.nextAction,
5135
+ approvalAction: status.approvalAction,
5136
+ triggerRunId: status.triggerRunId ?? null
5137
+ });
4337
5138
  const sampledRows = rows.rows.length;
4338
5139
  const availableRows = rows.count || sampledRows;
4339
5140
  const qualifiedRows = rows.rows.filter((row) => row.qualified !== false).length;
4340
5141
  const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4341
5142
  const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4342
5143
  const artifactCount = artifacts.length || status.artifactCount || 0;
5144
+ const northStarScorecard = createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState);
4343
5145
  const gates = [
4344
- {
4345
- id: "build_completed",
4346
- label: "Build completed",
4347
- passed: status.status === "completed",
4348
- detail: status.status === "completed" ? "The campaign build is completed." : `The campaign build is ${status.status}; review again after completion.`
4349
- },
4350
- {
4351
- id: "rows_available",
4352
- label: "Rows available",
4353
- passed: sampledRows > 0,
4354
- detail: sampledRows > 0 ? `${sampledRows} sampled row(s) are available for review.` : "No rows were returned for review."
4355
- },
4356
- {
4357
- id: "qualified_sample",
4358
- label: "Sample rows qualified",
4359
- passed: sampledRows > 0 && qualifiedRows === sampledRows,
4360
- detail: sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
4361
- },
4362
- {
4363
- id: "email_coverage",
4364
- label: "Email coverage",
4365
- passed: sampledRows > 0 && rowsWithEmail === sampledRows,
4366
- detail: sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
4367
- },
4368
- {
4369
- id: "artifact_available",
4370
- label: "Artifact available",
4371
- passed: artifactCount > 0,
4372
- detail: artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
4373
- },
4374
- {
4375
- id: "no_failed_rows",
4376
- label: "No failed rows",
4377
- passed: status.recordsFailed === 0,
4378
- detail: status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
4379
- }
5146
+ campaignReviewGate(
5147
+ "build_reviewable",
5148
+ "Build reviewable",
5149
+ terminalState.kind === "running_healthy" ? "pending" : terminalState.kind === "running_stale" || terminalState.kind === "blocked" ? "blocked" : "pass",
5150
+ terminalState.label
5151
+ ),
5152
+ campaignReviewGate(
5153
+ "rows_available",
5154
+ "Rows available",
5155
+ sampledRows > 0 ? "pass" : terminalState.reviewable ? "blocked" : "pending",
5156
+ sampledRows > 0 ? `${sampledRows} sampled row(s) are available for review.` : terminalState.reviewable ? "No rows were returned for review." : "Rows are not graded until the build is reviewable."
5157
+ ),
5158
+ campaignReviewGate(
5159
+ "qualified_sample",
5160
+ "Sample rows qualified",
5161
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : qualifiedRows === sampledRows ? "pass" : "blocked",
5162
+ sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
5163
+ ),
5164
+ campaignReviewGate(
5165
+ "email_coverage",
5166
+ "Email coverage",
5167
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : rowsWithEmail === sampledRows ? "pass" : "blocked",
5168
+ sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
5169
+ ),
5170
+ campaignReviewGate(
5171
+ "artifact_available",
5172
+ "Artifact available",
5173
+ artifactCount > 0 ? "pass" : terminalState.reviewable ? "review" : "pending",
5174
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
5175
+ ),
5176
+ campaignReviewGate(
5177
+ "no_failed_rows",
5178
+ "No failed rows",
5179
+ status.recordsFailed === 0 ? "pass" : "blocked",
5180
+ status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
5181
+ )
4380
5182
  ];
4381
- const blockedReasons = gates.filter((gate) => !gate.passed).map((gate) => gate.detail);
5183
+ const blockedReasons = gates.filter((gate) => gate.status === "blocked" || !gate.passed).map((gate) => gate.detail);
4382
5184
  const warnings = [
4383
5185
  rows.hasMore ? "This review is based on a row sample; page through remaining rows before final approval." : void 0,
4384
5186
  sampledRows > 0 && copyReadyRows < sampledRows ? `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.` : void 0,
4385
5187
  ...status.warnings
4386
5188
  ].filter((item) => Boolean(item));
4387
- const readyForDeliveryApproval = blockedReasons.length === 0;
5189
+ const readyForDeliveryApproval = terminalState.reviewable && blockedReasons.length === 0 && northStarScorecard.status !== "blocked";
4388
5190
  return {
4389
5191
  campaignBuildId: status.campaignBuildId || rows.campaignBuildId,
4390
5192
  status,
4391
5193
  rows,
4392
5194
  artifacts,
4393
5195
  gates,
5196
+ northStarScorecard,
4394
5197
  summary: {
4395
5198
  status: status.status,
4396
5199
  phase: status.currentPhase,
5200
+ terminalState: terminalState.kind,
5201
+ scorecardStatus: northStarScorecard.status,
5202
+ scorecardScore: northStarScorecard.score,
4397
5203
  sampledRows,
4398
5204
  availableRows,
4399
5205
  qualifiedRows,
@@ -4447,6 +5253,184 @@ function campaignReviewRowHasCopy(row) {
4447
5253
  stringValue(data.subject) || stringValue(data.subject_line) || stringValue(data.subjectLine) || stringValue(data.opener) || stringValue(data.body) || stringValue(data.cta) || stringValue(copy.subject) || stringValue(copy.body) || stringValue(rawCopy.subject) || stringValue(rawCopy.body)
4448
5254
  );
4449
5255
  }
5256
+ function campaignReviewDataEmail(data) {
5257
+ return stringValue(data.email) || stringValue(data.work_email) || stringValue(data.workEmail) || stringValue(asRecord(data.contact).email);
5258
+ }
5259
+ function campaignReviewDataDomain(data) {
5260
+ const domain = stringValue(data.company_domain) || stringValue(data.companyDomain) || stringValue(asRecord(data.company).domain);
5261
+ return domain?.toLowerCase();
5262
+ }
5263
+ function campaignReviewDataHasVerifiedEmail(data) {
5264
+ const status = String(data.email_status ?? data.emailStatus ?? asRecord(data.contact).email_status ?? "").toLowerCase();
5265
+ return data.email_verified === true || data.emailVerified === true || asRecord(data.contact).email_verified === true || ["valid", "validated", "verified"].includes(status);
5266
+ }
5267
+ function campaignReviewDataCopyParts(data) {
5268
+ const copy = asRecord(data.copy);
5269
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
5270
+ return [
5271
+ stringValue(data.subject),
5272
+ stringValue(data.subject_line),
5273
+ stringValue(data.opener),
5274
+ stringValue(data.body),
5275
+ stringValue(data.cta),
5276
+ stringValue(copy.subject),
5277
+ stringValue(copy.opener),
5278
+ stringValue(copy.body),
5279
+ stringValue(copy.cta),
5280
+ stringValue(rawCopy.subject),
5281
+ stringValue(rawCopy.opener),
5282
+ stringValue(rawCopy.body),
5283
+ stringValue(rawCopy.cta)
5284
+ ].filter((part) => Boolean(part));
5285
+ }
5286
+ function campaignReviewDataHasBadGreeting(data) {
5287
+ return campaignReviewDataCopyParts(data).some(
5288
+ (part) => /\bhi\s*(?:,|\{\{|\[)|\bhello\s*(?:,|\{\{|\[)/i.test(part) || /\b(first_name|firstname|name)\b/i.test(part)
5289
+ );
5290
+ }
5291
+ function campaignReviewDataHasBannedPhrase(data) {
5292
+ const text = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
5293
+ return NORTH_STAR_BANNED_COPY_PHRASES.some((phrase) => text.includes(phrase));
5294
+ }
5295
+ function campaignReviewDataSubject(data) {
5296
+ const copy = asRecord(data.copy);
5297
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
5298
+ return stringValue(data.subject) || stringValue(data.subject_line) || stringValue(data.subjectLine) || stringValue(copy.subject) || stringValue(rawCopy.subject);
5299
+ }
5300
+ function campaignReviewDataCta(data) {
5301
+ const copy = asRecord(data.copy);
5302
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
5303
+ return stringValue(data.cta) || stringValue(copy.cta) || stringValue(rawCopy.cta);
5304
+ }
5305
+ function countCampaignReviewIssues(issueRows) {
5306
+ return issueRows.flat().reduce((acc, issue) => {
5307
+ acc[issue] = (acc[issue] || 0) + 1;
5308
+ return acc;
5309
+ }, {});
5310
+ }
5311
+ function formatCampaignReviewIssueCounts(counts) {
5312
+ const entries = Object.entries(counts);
5313
+ return entries.length > 0 ? entries.map(([issue, count]) => `${issue}=${count}`).join(", ") : "none";
5314
+ }
5315
+ function campaignReviewDataCopyQaIssues(data) {
5316
+ const issues = /* @__PURE__ */ new Set();
5317
+ const copyParts = campaignReviewDataCopyParts(data);
5318
+ if (copyParts.length === 0) return [];
5319
+ if (campaignReviewDataHasBadGreeting(data)) issues.add("bad_greeting");
5320
+ if (campaignReviewDataHasBannedPhrase(data)) issues.add("banned_or_vague_phrase");
5321
+ if (campaignReviewDataHasUnsupportedSignalClaim(data)) issues.add("unsupported_signal_claim");
5322
+ if (campaignReviewDataPersonalizationBasis(data).length === 0) issues.add("missing_personalization_basis");
5323
+ if (!campaignReviewDataCta(data)) issues.add("missing_cta");
5324
+ const subject = campaignReviewDataSubject(data);
5325
+ if (subject && subject.length > 70) issues.add("overlong_subject");
5326
+ return [...issues];
5327
+ }
5328
+ function campaignReviewDataPersonalizationBasis(data) {
5329
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
5330
+ const copy = asRecord(data.copy);
5331
+ const metadata = asRecord(data.metadata);
5332
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
5333
+ return [
5334
+ ...arrayOfStrings(data.personalization_basis) ?? [],
5335
+ ...arrayOfStrings(data.personalizationBasis) ?? [],
5336
+ ...arrayOfStrings(copy.personalization_basis) ?? [],
5337
+ ...arrayOfStrings(rawCopy.personalization_basis) ?? [],
5338
+ ...arrayOfStrings(metadata.personalization_basis) ?? [],
5339
+ ...arrayOfStrings(metadata.personalizationBasis) ?? [],
5340
+ ...arrayOfStrings(signalEvidence.personalization_basis) ?? []
5341
+ ];
5342
+ }
5343
+ function campaignReviewDataHasNoSupportedSignal(data) {
5344
+ const metadata = asRecord(data.metadata);
5345
+ const copy = asRecord(data.copy);
5346
+ const state = [
5347
+ data.copy_signal_state,
5348
+ data.signal_basis,
5349
+ data.signal_type,
5350
+ metadata.copy_signal_state,
5351
+ metadata.signal_basis,
5352
+ metadata.signal_type,
5353
+ copy.copy_signal_state
5354
+ ].map((value) => stringValue(value)?.toLowerCase()).filter(Boolean);
5355
+ return data.no_supported_signal === true || metadata.no_supported_signal === true || state.includes("no_supported_signal");
5356
+ }
5357
+ function campaignReviewDataHasSignalBasis(data) {
5358
+ if (campaignReviewDataHasNoSupportedSignal(data)) return false;
5359
+ const metadata = asRecord(data.metadata);
5360
+ const signal = asRecord(data.signal);
5361
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
5362
+ const basis = campaignReviewDataPersonalizationBasis(data);
5363
+ return Boolean(
5364
+ stringValue(data.signal_type) || stringValue(data.signalType) || stringValue(data.signal_summary) || stringValue(data.signalSummary) || stringValue(data.signal_source_url) || stringValue(data.signalSourceUrl) || stringValue(metadata.signal_type) || stringValue(metadata.signal_summary) || stringValue(metadata.signal_source_url) || stringValue(signal.type) || stringValue(signal.summary) || stringValue(signal.source_url) || stringValue(signalEvidence.signal_type) || stringValue(signalEvidence.summary) || stringValue(signalEvidence.source_url) || basis.some((item) => /\bsignal|hiring|funding|leadership|growth|launch|news|technology\b/i.test(item))
5365
+ );
5366
+ }
5367
+ function campaignReviewDataHasUnsupportedSignalClaim(data) {
5368
+ const copyText = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
5369
+ const claimsSignal = /\b(hiring|funding|raised|launched|expanding|appointed|announced|recent news|new role|job posting)\b/.test(copyText);
5370
+ return claimsSignal && !campaignReviewDataHasSignalBasis(data);
5371
+ }
5372
+ function campaignReviewDataHasApprovalLock(data) {
5373
+ const metadata = asRecord(data.fit_metadata ?? data.metadata);
5374
+ const approval = String(data.approval_status ?? data.delivery_approval_status ?? metadata.approval_status ?? "").toLowerCase();
5375
+ const exportStatus = String(data.export_status ?? data.exportStatus ?? "").toLowerCase();
5376
+ return data.export_ready === false || data.exportReady === false || data.delivery_approved === false || data.deliveryApproved === false || ["pending", "pending_approval", "blocked", "approval_required", "held"].includes(approval) || ["pending", "blocked", "approval_required", "held"].includes(exportStatus);
5377
+ }
5378
+ function campaignReviewDataHasHoldoutAttribution(data) {
5379
+ const fitMetadata = asRecord(data.fit_metadata ?? data.fitMetadata);
5380
+ const metadata = asRecord(data.metadata);
5381
+ const holdout = asRecord(
5382
+ fitMetadata.learning_holdout ?? fitMetadata.learningHoldout ?? metadata.learning_holdout ?? metadata.learningHoldout ?? data.learning_holdout ?? data.learningHoldout
5383
+ );
5384
+ const experimentKey = stringValue(holdout.experiment_key ?? holdout.experimentKey ?? data.experiment_key ?? data.experimentKey);
5385
+ const cohort = stringValue(holdout.cohort_role ?? holdout.cohortRole ?? data.cohort_role ?? data.learning_cohort ?? data.learningCohort);
5386
+ const brainApplied = holdout.brain_applied ?? holdout.brainApplied ?? data.brain_applied ?? data.signaliz_brain_applied;
5387
+ return Boolean(experimentKey && cohort && brainApplied !== void 0);
5388
+ }
5389
+ function campaignReviewStatusHasFeedbackReadiness(status) {
5390
+ const diagnostics = asRecord(status.diagnostics);
5391
+ const feedbackReadiness = asRecord(diagnostics.feedback_readiness ?? diagnostics.feedbackReadiness);
5392
+ const feedbackSubstrate = asRecord(diagnostics.feedback_substrate ?? diagnostics.feedbackSubstrate);
5393
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
5394
+ return feedbackReadiness.ready === true || feedbackReadiness.webhook_configured === true || feedbackSubstrate.stage === "ready" || Number(evidenceCounts.feedback_events ?? 0) > 0;
5395
+ }
5396
+ function campaignReviewMoatState(status, holdoutRows) {
5397
+ const diagnostics = asRecord(status.diagnostics);
5398
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
5399
+ const embeddedHoldout = asRecord(diagnostics.embedded_holdout ?? diagnostics.embeddedHoldout);
5400
+ const holdoutLift = asRecord(diagnostics.holdout_lift ?? diagnostics.holdoutLift ?? diagnostics.holdout_measurement ?? diagnostics.holdoutMeasurement);
5401
+ const controlSample = Math.max(
5402
+ Number(evidenceCounts.holdout_control_sample_size ?? 0),
5403
+ Number(evidenceCounts.embedded_holdout_control_sample ?? 0),
5404
+ Number(embeddedHoldout.control_sample_size ?? 0)
5405
+ );
5406
+ const learnedSample = Math.max(
5407
+ Number(evidenceCounts.holdout_learned_sample_size ?? 0),
5408
+ Number(evidenceCounts.embedded_holdout_learned_sample ?? 0),
5409
+ Number(embeddedHoldout.learned_sample_size ?? 0)
5410
+ );
5411
+ const feedbackEvents = Math.max(
5412
+ Number(evidenceCounts.feedback_events ?? 0),
5413
+ Number(evidenceCounts.campaign_build_feedback_events ?? 0),
5414
+ Number(evidenceCounts.delivery_holdout_sent_outcomes ?? 0)
5415
+ );
5416
+ const proved = diagnostics.closed_loop_proof_ready === true || holdoutLift.honest_measurement === true && holdoutLift.positive_lift === true;
5417
+ if (proved) {
5418
+ return {
5419
+ state: "proved",
5420
+ detail: "Real attributed feedback shows positive lift for explicit control and learned cohorts."
5421
+ };
5422
+ }
5423
+ if (controlSample > 0 && learnedSample > 0 && feedbackEvents > 0) {
5424
+ return {
5425
+ state: "moved_needs_lift_volume",
5426
+ detail: `Control and learned cohorts have attributed feedback (${controlSample} control, ${learnedSample} learned, ${feedbackEvents} feedback event(s)); keep collecting lift volume before claiming proof.`
5427
+ };
5428
+ }
5429
+ return {
5430
+ state: "scaffolding_ready",
5431
+ detail: holdoutRows > 0 ? `${holdoutRows} sampled row(s) carry holdout attribution, but real control/learned feedback lift is not proved yet.` : "Holdout and feedback scaffolding may be ready, but no real control/learned feedback lift is proved yet."
5432
+ };
5433
+ }
4450
5434
  function createCampaignBuilderReadinessLane(route, plan) {
4451
5435
  const builtIn = campaignBuilderBuiltInForRoute(route);
4452
5436
  const customRoute = route.provider !== "signaliz" && route.provider !== "csv" && builtIn === void 0;
@@ -4480,6 +5464,342 @@ function createCampaignBuilderReadinessLane(route, plan) {
4480
5464
  nextAction: customRoute ? `Review ${route.provider} route setup before launch approval.` : void 0
4481
5465
  };
4482
5466
  }
5467
+ function createCampaignBuilderApprovalPacket(plan, approval, missingTypes) {
5468
+ const requestedTypes = plan ? uniqueStrings(plan.approvals.filter((item) => item.blocking).map((item) => item.type)) : [];
5469
+ const approvalMatchesPlan = Boolean(plan && approval?.planId === plan.planId);
5470
+ const approvedTypes = approvalMatchesPlan ? approval?.approvedTypes ?? [] : [];
5471
+ const approvedRoutes = approvalMatchesPlan ? approval?.approvedRouteIds ?? [] : [];
5472
+ const missingIds = new Set(
5473
+ plan && approvalMatchesPlan && approval ? getMissingCampaignBuilderApprovals(plan, approval).map((item) => item.id) : plan?.approvals.filter((item) => item.blocking).map((item) => item.id) ?? []
5474
+ );
5475
+ const routeIdsRequired = plan ? uniqueStrings(plan.approvals.map((item) => item.routeId).filter((id) => Boolean(id))) : [];
5476
+ const spendLimitRequired = plan ? Math.max(0, ...plan.approvals.filter((item) => item.type === "spend" && item.blocking).map((item) => item.estimatedCredits ?? plan.estimatedCredits)) : 0;
5477
+ const normalizedSpendLimit = spendLimitRequired > 0 ? spendLimitRequired : null;
5478
+ const approvalState = !plan ? "unscoped" : missingTypes.length > 0 ? "requires_approval" : "approved";
5479
+ const approvalInput = {
5480
+ approved_by: approval?.approvedBy ?? null,
5481
+ approved_at: approval?.approvedAt ?? null,
5482
+ approved_types: approvedTypes,
5483
+ approved_route_ids: approvedRoutes,
5484
+ spend_limit_credits: approval?.spendLimitCredits ?? null,
5485
+ notes: approval?.notes
5486
+ };
5487
+ return {
5488
+ packet_version: "campaign-builder-approval-packet.v1",
5489
+ read_only: true,
5490
+ no_spend: true,
5491
+ no_provider_writes: true,
5492
+ private_safe: true,
5493
+ approval_state: approvalState,
5494
+ plan_id: plan?.planId ?? null,
5495
+ campaign_name: plan?.campaignName ?? null,
5496
+ requested_approval_types: requestedTypes,
5497
+ missing_approval_types: uniqueStrings(missingTypes),
5498
+ spend_limit_credits_required: normalizedSpendLimit,
5499
+ route_ids_required: routeIdsRequired,
5500
+ required_approvals: (plan?.approvals ?? []).map((item) => compact({
5501
+ id: item.id,
5502
+ type: item.type,
5503
+ label: item.label,
5504
+ reason: item.reason,
5505
+ blocking: item.blocking,
5506
+ route_id: item.routeId,
5507
+ provider: item.provider,
5508
+ estimated_credits: item.estimatedCredits,
5509
+ status: missingIds.has(item.id) ? "missing" : "approved"
5510
+ })),
5511
+ approval_input: approvalInput,
5512
+ cli_handoff: plan ? {
5513
+ command: campaignBuilderApprovalCliHandoff(requestedTypes, routeIdsRequired, normalizedSpendLimit),
5514
+ safety: "Template only: run after explicit human approval identity, approved types, route scope, and spend limit are filled in."
5515
+ } : null,
5516
+ sdk_handoff: plan ? {
5517
+ method: "campaignBuilderAgent.buildApprovedPlan",
5518
+ approval: compact({
5519
+ approvedBy: approval?.approvedBy ?? "<operator-email>",
5520
+ approvedTypes: requestedTypes,
5521
+ approvedRouteIds: routeIdsRequired,
5522
+ spendLimitCredits: normalizedSpendLimit ?? void 0,
5523
+ notes: approval?.notes
5524
+ }),
5525
+ options: {
5526
+ idempotencyKey: "<set-before-execution>"
5527
+ },
5528
+ safety: "Call only after the approval packet is accepted; this helper does not execute writes or spend."
5529
+ } : null,
5530
+ mcp_handoffs: plan ? campaignBuilderApprovalMcpHandoffs(plan, requestedTypes, routeIdsRequired, normalizedSpendLimit) : [],
5531
+ safety_boundary: "This approval packet is a reusable request for human approval; it is not an approval grant and it does not authorize spend, provider writes, sender loading, delivery, or launch by itself."
5532
+ };
5533
+ }
5534
+ function campaignBuilderApprovalCliHandoff(requestedTypes, routeIds, spendLimit) {
5535
+ return [
5536
+ "signaliz campaign-agent build-approved --request-file campaign-request.json --confirm-launch",
5537
+ requestedTypes.length > 0 ? `--approved-types ${shellArg(requestedTypes.join(","))}` : "--approve-all",
5538
+ "--approved-by <operator-email>",
5539
+ spendLimit !== null ? `--spend-limit ${spendLimit}` : void 0,
5540
+ routeIds.length > 0 ? `--approved-route-ids ${shellArg(routeIds.join(","))}` : void 0
5541
+ ].filter(Boolean).join(" ");
5542
+ }
5543
+ function campaignBuilderApprovalMcpHandoffs(plan, requestedTypes, routeIds, spendLimit) {
5544
+ const approvalContext = compact({
5545
+ plan_id: plan.planId,
5546
+ approval_types: requestedTypes,
5547
+ approved_route_ids: routeIds,
5548
+ spend_limit_credits: spendLimit,
5549
+ approved_by: "<operator-email>"
5550
+ });
5551
+ return plan.mcpFlow.filter((step) => ["execution-prepare", "approved-build", "delivery-approval"].includes(step.id)).map((step) => ({
5552
+ id: step.id,
5553
+ tool: step.tool,
5554
+ arguments: step.id === "approved-build" ? compact({ ...step.arguments, approval_context: approvalContext }) : step.arguments,
5555
+ read_only: step.readOnly,
5556
+ approval_required: step.approvalRequired,
5557
+ safety: step.readOnly ? "Read-only readiness handoff; safe to inspect before approval." : "Write-capable handoff; run only after the approval packet is accepted."
5558
+ }));
5559
+ }
5560
+ function campaignBuilderActiveWorkNextMcpAction(input) {
5561
+ const campaignBuildRef = input.refs.find((ref) => ref.kind === "campaign_build" && ref.id);
5562
+ if (campaignBuildRef) {
5563
+ return {
5564
+ id: "check-campaign-build-status",
5565
+ tool: "get_campaign_build_status",
5566
+ arguments: { campaign_build_id: campaignBuildRef.id },
5567
+ read_only: true,
5568
+ approval_required: false,
5569
+ safety: "Read-only status check for the remembered campaign build; does not spend, write, export, send, or approve anything."
5570
+ };
5571
+ }
5572
+ const campaignRef = input.refs.find((ref) => ref.kind === "gtm_campaign" && ref.id);
5573
+ if (campaignRef) {
5574
+ return {
5575
+ id: "check-gtm-campaign-status",
5576
+ tool: "gtm_campaign_execution_status",
5577
+ arguments: { campaign_id: campaignRef.id },
5578
+ read_only: true,
5579
+ approval_required: false,
5580
+ safety: "Read-only campaign execution status check; does not start or mutate the campaign."
5581
+ };
5582
+ }
5583
+ const preferredStepId = input.state === "ready_for_dry_run" ? "dry-run-build" : input.state === "planning" || input.state === "blocked" ? "agency-campaign-build-plan" : null;
5584
+ const preferredStep = preferredStepId ? input.plan?.mcpFlow.find((step) => step.id === preferredStepId && step.readOnly) : void 0;
5585
+ if (preferredStep) return campaignBuilderMcpStepToActiveWorkAction(preferredStep, "Read-only continuation from the saved campaign-agent plan.");
5586
+ const readOnlyApprovalHandoff = input.approvalPacket.mcp_handoffs.find((handoff) => handoff.read_only && !handoff.approval_required);
5587
+ if (readOnlyApprovalHandoff) return readOnlyApprovalHandoff;
5588
+ const firstReadOnlyPlanStep = input.plan?.mcpFlow.find((step) => step.readOnly && !step.approvalRequired);
5589
+ return firstReadOnlyPlanStep ? campaignBuilderMcpStepToActiveWorkAction(firstReadOnlyPlanStep, "Read-only saved plan check; safe before approvals or writes.") : null;
5590
+ }
5591
+ function campaignBuilderMcpStepToActiveWorkAction(step, safety) {
5592
+ return {
5593
+ id: step.id,
5594
+ tool: step.tool,
5595
+ arguments: step.arguments,
5596
+ read_only: step.readOnly,
5597
+ approval_required: step.approvalRequired,
5598
+ safety
5599
+ };
5600
+ }
5601
+ function normalizeCampaignBuilderActiveWorkRefs(input) {
5602
+ const refs = [];
5603
+ const pushRef = (ref) => {
5604
+ if (!ref?.id) return;
5605
+ refs.push(ref);
5606
+ };
5607
+ pushRef(input.plan?.planId ? { kind: "plan", id: input.plan.planId, label: input.plan.campaignName, source: "plan" } : void 0);
5608
+ pushRef(input.remembered.planId ? { kind: "plan", id: input.remembered.planId, source: "remembered" } : void 0);
5609
+ pushRef(
5610
+ input.plan?.buildRequest.gtmCampaignId ?? input.remembered.gtmCampaignId ? {
5611
+ kind: "gtm_campaign",
5612
+ id: input.plan?.buildRequest.gtmCampaignId ?? input.remembered.gtmCampaignId,
5613
+ label: input.plan?.campaignName,
5614
+ source: input.plan?.buildRequest.gtmCampaignId ? "plan" : "remembered"
5615
+ } : void 0
5616
+ );
5617
+ const campaignBuildId = stringValue(input.lastResult.campaign_build_id ?? input.lastResult.campaignBuildId) ?? input.remembered.campaignBuildId;
5618
+ pushRef(campaignBuildId ? { kind: "campaign_build", id: campaignBuildId, status: activeWorkStatus(input.lastResult) ?? input.remembered.status, source: "result" } : void 0);
5619
+ pushRef(
5620
+ input.remembered.providerCampaignId ? {
5621
+ kind: "provider_campaign",
5622
+ id: input.remembered.providerCampaignId,
5623
+ label: input.remembered.provider,
5624
+ status: input.remembered.status,
5625
+ source: "remembered"
5626
+ } : void 0
5627
+ );
5628
+ for (const ref of input.remembered.refs ?? []) pushRef(ref);
5629
+ for (const ref of input.refs ?? []) pushRef(ref);
5630
+ const seen = /* @__PURE__ */ new Set();
5631
+ return refs.filter((ref) => {
5632
+ const key = `${ref.kind}:${ref.id}`;
5633
+ if (seen.has(key)) return false;
5634
+ seen.add(key);
5635
+ return true;
5636
+ });
5637
+ }
5638
+ function inferCampaignBuilderActiveWorkState(input) {
5639
+ if (input.blockers.length > 0) return "blocked";
5640
+ const status = activeWorkStatus(input.lastResult) ?? input.remembered.status;
5641
+ const normalized = status?.toLowerCase();
5642
+ if (normalized && ["completed", "complete", "succeeded", "success", "done"].includes(normalized)) return "completed";
5643
+ if (normalized && ["queued", "running", "in_progress", "processing", "pending"].includes(normalized)) return "queued_or_running";
5644
+ const dryRunComplete = input.lastResult.dry_run === true || input.lastResult.dryRun === true || normalized === "dry_run";
5645
+ if (dryRunComplete) return input.missingApprovals.length > 0 ? "needs_approval" : "dry_run_complete";
5646
+ if (input.missingApprovals.length > 0) return "needs_approval";
5647
+ if (input.readiness?.summary.ready === true) return "ready_for_dry_run";
5648
+ if (input.plan) return "planning";
5649
+ return "unknown";
5650
+ }
5651
+ function campaignBuilderActiveWorkApprovalBoundary(missingApprovals, state) {
5652
+ if (missingApprovals.length > 0) {
5653
+ return `Blocked before spend, provider writes, delivery, or launch until approval covers: ${uniqueStrings(missingApprovals).join(", ")}.`;
5654
+ }
5655
+ if (state === "dry_run_complete" || state === "ready_for_dry_run") {
5656
+ return "Read-only planning and dry-runs are allowed; spend, provider writes, sender loading, delivery, and launch still require explicit approval.";
5657
+ }
5658
+ return "This packet is read-only and does not authorize spend, provider writes, sender loading, delivery, or launch.";
5659
+ }
5660
+ function campaignBuilderActiveWorkNextAction(state, blockers, missingApprovals, refs) {
5661
+ if (blockers.length > 0) return `Resolve blocker: ${blockers[0]}`;
5662
+ if (missingApprovals.length > 0) return `Ask the operator to approve ${uniqueStrings(missingApprovals).join(", ")} before any write or spend action.`;
5663
+ if (state === "ready_for_dry_run") return "Run or inspect the read-only campaign dry-run, then return the receipt for approval review.";
5664
+ if (state === "dry_run_complete") return "Review the dry-run receipt and collect explicit launch or delivery approval before any write action.";
5665
+ if (state === "queued_or_running") return "Poll the remembered campaign build or provider job status; do not start a duplicate job.";
5666
+ if (state === "completed") return "Review final artifacts and delivery gates before export, sync, sender load, or launch.";
5667
+ if (refs.length > 0) return "Use the remembered refs to fetch current read-only status before asking the operator for IDs.";
5668
+ return "Create or refresh a campaign-builder plan before taking any spendful or write action.";
5669
+ }
5670
+ function campaignBuilderActiveWorkConversationStarters(input) {
5671
+ const starters = [];
5672
+ if (input.blockers.length > 0) {
5673
+ starters.push(
5674
+ `I found a blocker: ${input.blockers[0]}. Want me to stay in read-only diagnostics and prepare the fix path?`
5675
+ );
5676
+ }
5677
+ if (input.missingApprovals.length > 0) {
5678
+ starters.push(
5679
+ `You still owe approval for ${uniqueStrings(input.missingApprovals).join(", ")}. Want the exact approval packet before anything writes or spends?`
5680
+ );
5681
+ }
5682
+ if (input.state === "queued_or_running") {
5683
+ const buildRef = input.refs.find((ref) => ref.kind === "campaign_build");
5684
+ starters.push(buildRef ? `I can check campaign build ${buildRef.id} now and avoid starting a duplicate job.` : "I can check the running job status now and avoid starting duplicate work.");
5685
+ }
5686
+ if (input.state === "dry_run_complete") {
5687
+ starters.push("The dry run is complete. Want me to summarize blockers, artifacts, and the approval handoff?");
5688
+ }
5689
+ if (input.state === "ready_for_dry_run") {
5690
+ starters.push("Everything is ready for a safe dry run. Want me to run the no-spend preview first?");
5691
+ }
5692
+ if (input.state === "completed") {
5693
+ starters.push("The build looks complete. Want me to review rows, artifacts, and delivery gates before export or send approval?");
5694
+ }
5695
+ starters.push(`Next safe action: ${input.nextSafeAction}`);
5696
+ return uniqueStrings(starters).slice(0, 4);
5697
+ }
5698
+ function createCampaignBuilderOpenAiAgentHandoff(input) {
5699
+ const readOnlyPlanActions = input.plan?.mcpFlow.filter((step) => step.readOnly && !step.approvalRequired).map((step) => campaignBuilderMcpStepToActiveWorkAction(step, "Read-only Signaliz MCP continuation for the saved campaign-agent plan.")) ?? [];
5700
+ const routeSetupActions = readOnlyPlanActions.filter(
5701
+ (action) => action.tool === "gtm_provider_recipe_prepare" || action.tool === "gtm_provider_route_activate"
5702
+ );
5703
+ const safeReadActions = uniqueCampaignBuilderActiveWorkActions([
5704
+ input.nextSafeMcpAction,
5705
+ ...routeSetupActions,
5706
+ ...readOnlyPlanActions
5707
+ ]);
5708
+ const approvalGatedActions = uniqueCampaignBuilderActiveWorkActions([
5709
+ ...input.approvalPacket.mcp_handoffs.filter((action) => action.approval_required || !action.read_only),
5710
+ ...input.plan?.mcpFlow.filter((step) => step.approvalRequired || !step.readOnly).map((step) => campaignBuilderMcpStepToActiveWorkAction(step, "Approval-gated Signaliz MCP continuation for the saved campaign-agent plan.")) ?? []
5711
+ ]);
5712
+ const approvalTypes = uniqueStrings([
5713
+ ...input.missingApprovals,
5714
+ ...input.approvalPacket.requested_approval_types
5715
+ ]);
5716
+ return {
5717
+ packet_version: "campaign-builder-openai-agent-handoff.v1",
5718
+ surface: "openai_agents_sdk",
5719
+ agent: {
5720
+ name: "Signaliz GTM Campaign Agent",
5721
+ handoff_description: "Owns safe, conversational GTM campaign planning and continuation across Signaliz MCP, SDK, attachments, URLs, customer tools, approvals, and sender/export gates.",
5722
+ instructions: [
5723
+ "Be warm, direct, and expert-level GTM. Translate the current state into plain operator language before proposing work.",
5724
+ "Start from the packet state, refs, evidence_context, approval_boundary, and next_safe_action. Do not ask for IDs already present in refs or summary.",
5725
+ "Use attachment_summary, url_summary, and operator_notes as first-class planning evidence, while honoring the evidence privacy_boundary and withheld raw fields.",
5726
+ "Prefer Signaliz-native, cached, remembered, and read-only MCP actions before provider calls. Never start duplicate jobs when a campaign_build or provider job is queued or running.",
5727
+ "Call only tools listed in safe_read_tools without approval. Treat approval_gated_tools as human-review surfaces, not permission to spend, write, export, load senders, deliver, or launch.",
5728
+ "When approval is missing, show the approval_packet and the exact missing approval scope before any side-effecting step."
5729
+ ],
5730
+ output_contract: [
5731
+ "current_state: one concise sentence describing where the campaign work stands.",
5732
+ "evidence_used: attachment, URL, memory, route, or integration evidence used without exposing private raw rows or secrets.",
5733
+ "next_safe_mcp_action: the read-only Signaliz MCP action to run next, or null when none is safe.",
5734
+ "approval_boundary: the human approval scope required before spend, provider writes, delivery, export, sender load, or launch.",
5735
+ "operator_reply: conversational GTM guidance with blockers, risks, and the safest next move."
5736
+ ]
5737
+ },
5738
+ context: {
5739
+ state: input.state,
5740
+ summary: input.summary,
5741
+ refs: input.refs,
5742
+ blockers: input.blockers,
5743
+ warnings: input.warnings,
5744
+ evidence_context: input.plan?.evidence ?? null,
5745
+ approval_packet: input.approvalPacket,
5746
+ approval_boundary: input.approvalBoundary,
5747
+ next_safe_action: input.nextSafeAction,
5748
+ conversation_starters: input.conversationStarters
5749
+ },
5750
+ tools: {
5751
+ mcp_server_label: "signaliz",
5752
+ remote_mcp_compatible: true,
5753
+ next_safe_mcp_action: input.nextSafeMcpAction,
5754
+ safe_read_tools: safeReadActions.map(
5755
+ (action) => campaignBuilderOpenAiAgentTool(action, "Safe read-only Signaliz MCP action available before approval.")
5756
+ ),
5757
+ approval_gated_tools: approvalGatedActions.map(
5758
+ (action) => campaignBuilderOpenAiAgentTool(action, "Requires explicit human review before execution.")
5759
+ )
5760
+ },
5761
+ guardrails: {
5762
+ input: [
5763
+ "Reject or pause requests to expose raw private attachment text, raw rows, leads, emails, provider payloads, copy bodies, API keys, or secrets.",
5764
+ "Pause when the user asks to spend credits, write to a provider, export, load a sender, deliver, or launch without explicit approval scope."
5765
+ ],
5766
+ tool: [
5767
+ "Read-only tools may run when they match safe_read_tools and their arguments match this packet.",
5768
+ "Any tool with approval_required=true or read_only=false requires human review and the approval_packet scope first."
5769
+ ],
5770
+ human_review_required_for: approvalTypes,
5771
+ privacy: input.plan?.evidence?.privacy_boundary ?? "Use summarized context only. Do not expose raw private attachment text, raw rows, leads, emails, provider payloads, copy bodies, secrets, or credentials."
5772
+ }
5773
+ };
5774
+ }
5775
+ function campaignBuilderOpenAiAgentTool(action, purpose) {
5776
+ return {
5777
+ id: action.id,
5778
+ type: "mcp",
5779
+ server_label: "signaliz",
5780
+ name: action.tool,
5781
+ arguments: action.arguments,
5782
+ read_only: action.read_only,
5783
+ approval_required: action.approval_required,
5784
+ purpose,
5785
+ safety: action.safety
5786
+ };
5787
+ }
5788
+ function uniqueCampaignBuilderActiveWorkActions(actions) {
5789
+ const seen = /* @__PURE__ */ new Set();
5790
+ const unique = [];
5791
+ for (const action of actions) {
5792
+ if (!action) continue;
5793
+ const key = `${action.id}:${action.tool}`;
5794
+ if (seen.has(key)) continue;
5795
+ seen.add(key);
5796
+ unique.push(action);
5797
+ }
5798
+ return unique;
5799
+ }
5800
+ function activeWorkStatus(record) {
5801
+ return stringValue(record.status ?? record.state ?? record.phase);
5802
+ }
4483
5803
  function campaignBuilderBuiltInForRoute(route) {
4484
5804
  return Object.entries(CAMPAIGN_BUILDER_BUILT_IN_ROUTE_IDS).find(([, routeId]) => route.id === routeId)?.[0];
4485
5805
  }
@@ -7276,6 +8596,36 @@ function recordsFromParams(params) {
7276
8596
  if (params.input) return { input: params.input };
7277
8597
  return { input: {} };
7278
8598
  }
8599
+ function toFusionConfig(params) {
8600
+ return {
8601
+ system_prompt: params.systemPrompt || params.system_prompt,
8602
+ user_template: params.userTemplate || params.user_template || params.prompt,
8603
+ preset: params.preset,
8604
+ temperature: params.temperature,
8605
+ max_tool_calls: params.maxToolCalls || params.max_tool_calls,
8606
+ max_tokens: params.maxTokens || params.max_tokens,
8607
+ timeout_ms: params.timeoutMs || params.timeout_ms,
8608
+ output_fields: params.outputFields || params.output_fields || [{ name: "response", type: "text", description: "Structured Fusion enrichment result" }]
8609
+ };
8610
+ }
8611
+ function toFusionSpendControls(params) {
8612
+ const controls = {};
8613
+ const dryRun = params.dryRun ?? params.dry_run;
8614
+ const confirmSpend = params.confirmSpend ?? params.confirm_spend;
8615
+ const maxCredits = params.maxCredits ?? params.max_credits;
8616
+ const maxCostUsd = params.maxCostUsd ?? params.max_cost_usd;
8617
+ if (dryRun !== void 0) controls.dry_run = dryRun;
8618
+ if (confirmSpend !== void 0) controls.confirm_spend = confirmSpend;
8619
+ if (maxCredits !== void 0) controls.max_credits = maxCredits;
8620
+ if (maxCostUsd !== void 0) controls.max_cost_usd = maxCostUsd;
8621
+ return controls;
8622
+ }
8623
+ function fusionRecordsFromParams(params) {
8624
+ const records = params.records || params.inputs;
8625
+ if (records?.length) return { records };
8626
+ if (params.input) return { input: params.input };
8627
+ return { input: {} };
8628
+ }
7279
8629
  var Ai = class {
7280
8630
  constructor(client) {
7281
8631
  this.client = client;
@@ -7309,6 +8659,33 @@ var Ai = class {
7309
8659
  raw: data
7310
8660
  };
7311
8661
  }
8662
+ /**
8663
+ * Plan or run experimental AI Enrichment Fusion with explicit spend controls.
8664
+ */
8665
+ async fusion(params) {
8666
+ if (!params.prompt && !params.userTemplate && !params.user_template) {
8667
+ throw new Error("prompt or userTemplate is required.");
8668
+ }
8669
+ const data = await this.client.post("ai-enrichment-fusion", {
8670
+ ...fusionRecordsFromParams(params),
8671
+ config: toFusionConfig(params),
8672
+ ...toFusionSpendControls(params)
8673
+ });
8674
+ return {
8675
+ success: data.success ?? false,
8676
+ capability: data.capability ?? "ai_enrichment_fusion",
8677
+ displayName: data.display_name ?? "AI Enrichment Fusion",
8678
+ results: data.results ?? [],
8679
+ creditsUsed: data.total_credits_used ?? data.credits_used ?? 0,
8680
+ model: data.model,
8681
+ fusion: data.fusion,
8682
+ tokensUsed: data.tokens_used ?? 0,
8683
+ costUsd: data.cost_usd ?? 0,
8684
+ costSummary: data.cost_summary,
8685
+ billingMetadata: data.billing_metadata,
8686
+ raw: data
8687
+ };
8688
+ }
7312
8689
  };
7313
8690
 
7314
8691
  // src/resources/gtm-kernel.ts
@@ -7662,6 +9039,16 @@ var GtmKernel = class {
7662
9039
  min_privacy_k: input.minPrivacyK
7663
9040
  });
7664
9041
  }
9042
+ /** Queue a dry-run-first bridge from imported historical feedback to attribution-only Campaign Build surrogates. */
9043
+ async runHistoricalCampaignBuildBridge(input = {}) {
9044
+ return this.callMcp("gtm_historical_campaign_build_bridge_run", {
9045
+ dry_run: input.dryRun,
9046
+ write_approved: input.writeApproved,
9047
+ max_events: input.maxEvents,
9048
+ max_campaigns: input.maxCampaigns,
9049
+ min_feedback_events: input.minFeedbackEvents
9050
+ });
9051
+ }
7665
9052
  /** Prepare a provider-agnostic feedback webhook URL for Smartlead, HeyReach, Airbyte, or custom sender events. */
7666
9053
  async prepareFeedbackWebhook(input) {
7667
9054
  return this.callMcp("gtm_feedback_webhook_prepare", {
@@ -7838,6 +9225,24 @@ var GtmKernel = class {
7838
9225
  limit: input.limit
7839
9226
  });
7840
9227
  }
9228
+ /** Predict booked-meeting likelihood for uploaded, inline, or MCP-sourced lead rows without persisting raw rows. */
9229
+ async predictMeetingLikelihood(input) {
9230
+ return this.callMcp("gtm_predict_meeting_likelihood", {
9231
+ lead_rows: input.leadRows,
9232
+ input_source: input.inputSource,
9233
+ campaign_id: input.campaignId,
9234
+ campaign_build_id: input.campaignBuildId,
9235
+ days: input.days,
9236
+ feedback_limit: input.feedbackLimit,
9237
+ include_features: input.includeFeatures,
9238
+ include_global: input.includeGlobal,
9239
+ min_feedback_events: input.minFeedbackEvents,
9240
+ min_historical_feature_sample_size: input.minHistoricalFeatureSampleSize,
9241
+ min_workspace_count: input.minWorkspaceCount,
9242
+ min_privacy_k: input.minPrivacyK,
9243
+ limit: input.limit
9244
+ });
9245
+ }
7841
9246
  /** Rank provider-chain, ICP, source, copy, and sequence failure patterns without writing raw lead data. */
7842
9247
  async failurePatterns(options = {}) {
7843
9248
  return this.callMcp("gtm_brain_failure_patterns", {
@@ -8453,6 +9858,70 @@ function compact2(value) {
8453
9858
  return out;
8454
9859
  }
8455
9860
 
9861
+ // src/resources/public-ground-truth.ts
9862
+ var PublicGroundTruth = class {
9863
+ constructor(client) {
9864
+ this.client = client;
9865
+ }
9866
+ /**
9867
+ * Search the public-ground-truth cache by company/name/domain/native ID,
9868
+ * or by plain-language industry + location.
9869
+ * Results use the same snake_case wire shape returned by the hosted MCP API.
9870
+ */
9871
+ async search(params) {
9872
+ return this.client.mcp("tools/call", {
9873
+ name: "search_public_ground_truth",
9874
+ arguments: {
9875
+ query: params.query ?? params.search,
9876
+ industry: params.industry,
9877
+ location: params.location,
9878
+ source_id: params.sourceId ?? params.source_id,
9879
+ entity_type: params.entityType ?? params.entity_type,
9880
+ native_id: params.nativeId ?? params.native_id,
9881
+ entity_name: params.entityName ?? params.entity_name,
9882
+ domain: params.domain,
9883
+ website_url: params.websiteUrl ?? params.website_url,
9884
+ phone: params.phone,
9885
+ email: params.email,
9886
+ city: params.city,
9887
+ state: params.state,
9888
+ postal_code: params.postalCode ?? params.postal_code,
9889
+ country: params.country,
9890
+ source_url: params.sourceUrl ?? params.source_url,
9891
+ join_keys: params.joinKeys ?? params.join_keys,
9892
+ row_data: params.rowData ?? params.row_data,
9893
+ naics: params.naics,
9894
+ industry_code: params.industryCode ?? params.industry_code,
9895
+ industry_type: params.industryType ?? params.industry_type,
9896
+ year: params.year,
9897
+ quarter: params.quarter,
9898
+ area_fips: params.areaFips ?? params.area_fips,
9899
+ geo_id: params.geoId ?? params.geo_id,
9900
+ own_code: params.ownCode ?? params.own_code,
9901
+ source_updated_after: params.sourceUpdatedAfter ?? params.source_updated_after,
9902
+ source_updated_before: params.sourceUpdatedBefore ?? params.source_updated_before,
9903
+ observed_after: params.observedAfter ?? params.observed_after,
9904
+ observed_before: params.observedBefore ?? params.observed_before,
9905
+ limit: params.limit,
9906
+ offset: params.offset
9907
+ }
9908
+ });
9909
+ }
9910
+ /**
9911
+ * List available public-ground-truth cache sources and their join-key metadata.
9912
+ */
9913
+ async listSources(params = {}) {
9914
+ return this.client.mcp("tools/call", {
9915
+ name: "list_public_ground_truth_sources",
9916
+ arguments: {
9917
+ query: params.query ?? params.search,
9918
+ status: params.status,
9919
+ limit: params.limit
9920
+ }
9921
+ });
9922
+ }
9923
+ };
9924
+
8456
9925
  // src/index.ts
8457
9926
  var MCP_TOOL_CACHE_TTL_MS = 3e4;
8458
9927
  function inferMcpToolCategory(toolName) {
@@ -8495,6 +9964,7 @@ function inferMcpToolCategory(toolName) {
8495
9964
  }
8496
9965
  if (toolName.includes("icp")) return "icp";
8497
9966
  if (toolName.startsWith("ai_clean_")) return "data_cleaning";
9967
+ if (toolName.includes("public_ground_truth")) return "public_data";
8498
9968
  if (toolName.includes("system") || toolName.includes("workflow")) return "automation";
8499
9969
  if (toolName.includes("agent") || toolName.includes("platform_health") || toolName === "discover_capabilities") return "observability";
8500
9970
  return void 0;
@@ -8546,6 +10016,7 @@ var Signaliz = class {
8546
10016
  this.ops = new Ops(this.client);
8547
10017
  this.ai = new Ai(this.client);
8548
10018
  this.gtm = new GtmKernel(this.client);
10019
+ this.publicGroundTruth = new PublicGroundTruth(this.client);
8549
10020
  }
8550
10021
  /** Get current workspace info including credits */
8551
10022
  async getWorkspace() {