thumbgate 1.28.4 → 1.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.claude/commands/dashboard.md +11 -1
  2. package/.claude/commands/thumbgate-dashboard.md +23 -8
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/llms.txt +18 -10
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +66 -3
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/forge/forge.yaml +3 -3
  9. package/adapters/mcp/server-stdio.js +88 -2
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bin/cli.js +8 -8
  12. package/bin/postinstall.js +4 -13
  13. package/commands/dashboard.md +11 -1
  14. package/commands/thumbgate-dashboard.md +23 -8
  15. package/config/agent-outcome-monitor-thresholds.json +63 -0
  16. package/config/evals/agent-outcomes-baseline.json +17 -0
  17. package/config/evals/agent-outcomes-golden.json +412 -0
  18. package/config/evals/prompt-eval-baseline.json +23 -0
  19. package/config/github-about.json +5 -4
  20. package/config/post-deploy-marketing-pages.json +6 -6
  21. package/config/schemas/task-outcome-receipt.schema.json +296 -0
  22. package/docs/integrations/grafana/README.md +109 -0
  23. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  24. package/openapi/openapi.yaml +475 -5
  25. package/package.json +75 -22
  26. package/public/agent-manager.html +10 -11
  27. package/public/agents-cost-savings.html +2 -2
  28. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  29. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  30. package/public/assets/brand/thumbgate-mark.svg +10 -11
  31. package/public/blog/inside-your-boundary.html +114 -0
  32. package/public/blog/process-over-outcome-gates.html +119 -0
  33. package/public/blog.html +296 -402
  34. package/public/brand/thumbgate-mark.svg +5 -9
  35. package/public/codex-enterprise.html +2 -2
  36. package/public/compare.html +12 -3
  37. package/public/diagnostic.html +79 -29
  38. package/public/guide.html +4 -4
  39. package/public/index.html +1090 -2098
  40. package/public/install.html +3 -3
  41. package/public/js/buyer-intent.js +33 -18
  42. package/public/numbers.html +2 -2
  43. package/public/pricing.html +268 -408
  44. package/public/pro.html +4 -4
  45. package/scripts/agent-outcome-eval.js +130 -0
  46. package/scripts/agent-outcome-monitor.js +261 -0
  47. package/scripts/agent-reasoning-traces.js +8 -9
  48. package/scripts/async-job-runner.js +107 -13
  49. package/scripts/billing.js +456 -126
  50. package/scripts/buyer-paths.js +102 -0
  51. package/scripts/cli-feedback.js +2 -2
  52. package/scripts/commercial-offer.js +18 -10
  53. package/scripts/durability/step.js +121 -12
  54. package/scripts/external-customer-audit.js +881 -0
  55. package/scripts/feedback-loop.js +26 -0
  56. package/scripts/gates-engine.js +554 -19
  57. package/scripts/grafana-revenue-evidence.js +856 -0
  58. package/scripts/human-escalation.js +265 -0
  59. package/scripts/hybrid-feedback-context.js +93 -50
  60. package/scripts/jsonl-window.js +89 -0
  61. package/scripts/judge-reward-function.js +30 -18
  62. package/scripts/lesson-embedding-index.js +3 -7
  63. package/scripts/meta-agent-loop.js +20 -2
  64. package/scripts/observability-env.js +139 -0
  65. package/scripts/observability-setup.js +55 -0
  66. package/scripts/plausible-domain-config.js +4 -0
  67. package/scripts/prompt-eval.js +81 -4
  68. package/scripts/provider-live-evidence.js +1290 -0
  69. package/scripts/provider-payment-reconciler.js +442 -0
  70. package/scripts/provider-revenue-evidence.js +249 -0
  71. package/scripts/rate-limiter.js +1 -5
  72. package/scripts/revenue-action-eligibility.js +414 -0
  73. package/scripts/revenue-evidence-remediation.js +694 -0
  74. package/scripts/revenue-offer-system.js +709 -0
  75. package/scripts/sales-pipeline.js +1117 -0
  76. package/scripts/schedule-manager.js +249 -0
  77. package/scripts/seo-gsd.js +8 -4
  78. package/scripts/stripe-credentials.js +37 -0
  79. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  80. package/scripts/stripe-revenue-catalog.js +164 -0
  81. package/scripts/task-outcomes.js +425 -0
  82. package/scripts/telemetry-analytics.js +23 -3
  83. package/scripts/tool-contract-validator.js +287 -59
  84. package/scripts/tool-registry.js +143 -0
  85. package/scripts/vector-store.js +83 -7
  86. package/scripts/workflow-intake-queue.js +483 -0
  87. package/src/api/server.js +647 -118
@@ -153,11 +153,7 @@ function checkLimit(action, authContext) {
153
153
  const dailyCurrent = usage.counts[action] || 0;
154
154
  const lifetimeCurrent = usage.lifetime[action] || 0;
155
155
 
156
- const isCapture = action === 'capture_feedback';
157
- const monthlyLink = isCapture
158
- ? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
159
- : PRO_MONTHLY_PAYMENT_LINK;
160
- const upgradeMessage = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${monthlyLink}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
156
+ const upgradeMessage = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
161
157
 
162
158
  // Check lifetime limit first (the hard wall)
163
159
  if (lifetimeLimit !== Infinity && lifetimeCurrent >= lifetimeLimit) {
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const {
5
+ evaluateLeadStageEvidence,
6
+ normalizeSalesStage,
7
+ } = require('./sales-pipeline');
8
+
9
+ const FOLLOW_UP_COOLDOWN_MS = 48 * 60 * 60 * 1000;
10
+ const BUYER_REPLY_FRESHNESS_MS = 14 * 24 * 60 * 60 * 1000;
11
+ const EVENT_CLOCK_SKEW_MS = 5 * 60 * 1000;
12
+ const ZERO_SPEND_STATUSES = Object.freeze([
13
+ 'proceed_zero_cost',
14
+ 'hold_unverified_cost',
15
+ 'discarded_paid_requirement',
16
+ ]);
17
+ const DIRECT_ZERO_COST_CHANNELS = new Set([
18
+ 'bluesky',
19
+ 'direct_email',
20
+ 'email',
21
+ 'github',
22
+ 'linkedin',
23
+ 'linkedin_comment',
24
+ 'manual',
25
+ 'owned',
26
+ 'reddit',
27
+ 'reddit_comment',
28
+ 'reddit_dm',
29
+ 'threads',
30
+ ]);
31
+ const PAID_OR_AMBIGUOUS_CHANNEL_PATTERN = /\b(ad|ads|advertising|boost|credit|credits|inmail|lead database|marketplace|paid|pay-to-play|revenue share|sponsor|subscription)\b/i;
32
+
33
+ function normalizeText(value, maxLength = 1000) {
34
+ if (value === undefined || value === null) return null;
35
+ const text = String(value).trim();
36
+ return text ? text.slice(0, maxLength) : null;
37
+ }
38
+
39
+ function parseTimestamp(value) {
40
+ const timestamp = Date.parse(String(value || ''));
41
+ return Number.isFinite(timestamp) ? timestamp : null;
42
+ }
43
+
44
+ function toIsoStringOrNull(value) {
45
+ const date = new Date(value);
46
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
47
+ }
48
+
49
+ function latestEvidenceEvent(lead = {}, kinds = []) {
50
+ const accepted = new Set(kinds);
51
+ const events = Array.isArray(lead.history) ? lead.history : [];
52
+ return events
53
+ .filter((event) => accepted.has(event?.evidence?.kind) && parseTimestamp(event.at) !== null)
54
+ .sort((left, right) => parseTimestamp(right.at) - parseTimestamp(left.at))[0] || null;
55
+ }
56
+
57
+ function resolveZeroSpendStatus(target = {}) {
58
+ if (
59
+ target.requiresBuyerPaymentToAccessLead === true
60
+ || target.requiresPaidAccess === true
61
+ || target.requiresRevenueShare === true
62
+ || target.requiresSellerPayment === true
63
+ || target.requiresSubscription === true
64
+ ) {
65
+ return 'discarded_paid_requirement';
66
+ }
67
+
68
+ const explicit = normalizeText(target.zeroSpendStatus, 80);
69
+ if (explicit === 'discarded_paid_requirement' || explicit === 'hold_unverified_cost') {
70
+ return explicit;
71
+ }
72
+
73
+ const channelTokens = [target.source, target.channel, target.platform, target.distributionMethod]
74
+ .map((value) => normalizeText(value, 120)?.toLowerCase())
75
+ .filter(Boolean);
76
+ const directZeroCostChannel = channelTokens.some((value) => DIRECT_ZERO_COST_CHANNELS.has(value));
77
+ const costVerificationEvidence = normalizeText(target.costVerificationEvidence, 500);
78
+ if (channelTokens.some((value) => PAID_OR_AMBIGUOUS_CHANNEL_PATTERN.test(value))) {
79
+ if (
80
+ explicit === 'proceed_zero_cost'
81
+ && target.costVerifiedZero === true
82
+ && costVerificationEvidence
83
+ ) {
84
+ return 'proceed_zero_cost';
85
+ }
86
+ return 'hold_unverified_cost';
87
+ }
88
+ if (explicit === 'proceed_zero_cost') {
89
+ return directZeroCostChannel || (target.costVerifiedZero === true && costVerificationEvidence)
90
+ ? 'proceed_zero_cost'
91
+ : 'hold_unverified_cost';
92
+ }
93
+ if (directZeroCostChannel) return 'proceed_zero_cost';
94
+ if (target.costVerifiedZero === true && costVerificationEvidence) return 'proceed_zero_cost';
95
+ return 'hold_unverified_cost';
96
+ }
97
+
98
+ function resolveLeadZeroSpendStatus(lead = {}, target = {}) {
99
+ const durableRouteStatus = resolveZeroSpendStatus({
100
+ ...target,
101
+ source: lead.source,
102
+ channel: lead.channel,
103
+ });
104
+ const targetRouteStatus = resolveZeroSpendStatus({
105
+ ...target,
106
+ source: target.source || lead.source,
107
+ channel: target.channel || lead.channel,
108
+ });
109
+ const statuses = [durableRouteStatus, targetRouteStatus];
110
+ if (statuses.includes('discarded_paid_requirement')) return 'discarded_paid_requirement';
111
+ if (statuses.includes('hold_unverified_cost')) return 'hold_unverified_cost';
112
+ return 'proceed_zero_cost';
113
+ }
114
+
115
+ function approvalToken(value) {
116
+ const token = String(value || 'LEAD')
117
+ .toUpperCase()
118
+ .replace(/[^A-Z0-9]+/g, '_')
119
+ .replace(/^_+|_+$/g, '')
120
+ .slice(0, 96);
121
+ return token || 'LEAD';
122
+ }
123
+
124
+ function buildApprovalPhrase(leadId, action) {
125
+ const actions = {
126
+ first_touch: 'FIRST TOUCH',
127
+ follow_up: 'FOLLOW UP',
128
+ qualification_reply: 'QUALIFICATION REPLY',
129
+ };
130
+ const label = actions[action];
131
+ return label ? `APPROVE SEND THUMBGATE ${label} TO ${approvalToken(leadId)}` : null;
132
+ }
133
+
134
+ function decision({
135
+ status,
136
+ queueClass = 'hold',
137
+ readyForOutbound = false,
138
+ requiresApproval = false,
139
+ approvalPhrase = null,
140
+ nextAction,
141
+ reason,
142
+ nextEligibleAt = null,
143
+ evidence,
144
+ zeroCostReplacement = null,
145
+ }) {
146
+ return {
147
+ status,
148
+ queueClass,
149
+ operatorReady: queueClass === 'approval_ready' || queueClass === 'internal_ready',
150
+ readyForOutbound,
151
+ requiresApproval,
152
+ approvalPhrase,
153
+ nextAction,
154
+ reason,
155
+ nextEligibleAt,
156
+ zeroCostReplacement,
157
+ evidence,
158
+ };
159
+ }
160
+
161
+ function evaluateRevenueActionEligibility(lead = {}, target = {}, {
162
+ now = new Date().toISOString(),
163
+ followUpCooldownMs = FOLLOW_UP_COOLDOWN_MS,
164
+ } = {}) {
165
+ const stage = normalizeSalesStage(lead.stage, 'targeted');
166
+ const zeroSpendStatus = resolveLeadZeroSpendStatus(lead, target);
167
+ const stageEvidence = evaluateLeadStageEvidence({ ...lead, stage });
168
+ const latestSend = latestEvidenceEvent(lead, ['platform_send_receipt']);
169
+ const latestBuyerReply = latestEvidenceEvent(lead, ['buyer_reply']);
170
+ const history = Array.isArray(lead.history) ? lead.history : [];
171
+ const outboundReceiptCount = history.filter((event) => event?.evidence?.kind === 'platform_send_receipt').length;
172
+ const nowMs = parseTimestamp(now) ?? Date.now();
173
+ const requestedCooldownMs = Number(followUpCooldownMs);
174
+ const enforcedCooldownMs = Number.isFinite(requestedCooldownMs)
175
+ ? Math.max(FOLLOW_UP_COOLDOWN_MS, requestedCooldownMs)
176
+ : FOLLOW_UP_COOLDOWN_MS;
177
+ const evidence = {
178
+ stage,
179
+ stageVerified: stageEvidence.verified === true,
180
+ stageEvidenceKind: stageEvidence.evidence?.kind || null,
181
+ stageEvidenceAt: stageEvidence.evidenceAt || null,
182
+ latestSendAt: latestSend?.at || null,
183
+ latestBuyerReplyAt: latestBuyerReply?.at || null,
184
+ outboundReceiptCount,
185
+ followUpCooldownMs: enforcedCooldownMs,
186
+ zeroSpendStatus,
187
+ };
188
+
189
+ if (zeroSpendStatus === 'discarded_paid_requirement') {
190
+ return decision({
191
+ status: 'discarded_paid_requirement',
192
+ nextAction: 'Discard this acquisition path and replace it with an owned or direct organic route.',
193
+ reason: 'The route requires seller payment, paid access, a subscription, or revenue share.',
194
+ evidence,
195
+ zeroCostReplacement: 'Use an existing warm conversation, owned first-party intake, or a direct organic channel.',
196
+ });
197
+ }
198
+ if (zeroSpendStatus === 'hold_unverified_cost') {
199
+ return decision({
200
+ status: 'hold_unverified_cost',
201
+ nextAction: 'Verify the complete downstream cost terms before preparing outreach or routing checkout traffic.',
202
+ reason: 'The complete acquisition path is not proven free of present or future seller obligations.',
203
+ evidence,
204
+ zeroCostReplacement: 'Use an existing warm conversation, owned first-party intake, or a direct organic channel.',
205
+ });
206
+ }
207
+
208
+ if (stage !== 'targeted' && !stageEvidence.verified) {
209
+ return decision({
210
+ status: 'hold_unverified_stage_evidence',
211
+ nextAction: `Reconcile a real ${stage} receipt before any new message or offer.`,
212
+ reason: stageEvidence.reason || `The ${stage} label has no stage-appropriate receipt.`,
213
+ evidence,
214
+ });
215
+ }
216
+
217
+ const leadId = lead.leadId || target.pipelineLeadId || 'lead';
218
+ if (stage === 'targeted') {
219
+ if (outboundReceiptCount > 0 || latestBuyerReply) {
220
+ return decision({
221
+ status: 'hold_pipeline_transition_missing',
222
+ nextAction: 'Reconcile the existing send or buyer-reply evidence and advance the pipeline before preparing another first touch.',
223
+ reason: 'The lead is labeled targeted even though its history already contains outbound or buyer-reply evidence.',
224
+ evidence,
225
+ });
226
+ }
227
+ return decision({
228
+ status: 'approval_required_first_touch',
229
+ queueClass: 'approval_ready',
230
+ readyForOutbound: true,
231
+ requiresApproval: true,
232
+ approvalPhrase: buildApprovalPhrase(leadId, 'first_touch'),
233
+ nextAction: 'Request exact action-time approval for the evidence-backed first touch; send nothing before approval.',
234
+ reason: 'The direct organic route is zero-cost and no prior send receipt exists.',
235
+ evidence,
236
+ });
237
+ }
238
+
239
+ const latestSendMs = parseTimestamp(latestSend?.at);
240
+ const latestReplyMs = parseTimestamp(latestBuyerReply?.at);
241
+ const buyerReplyAgeMs = latestReplyMs === null ? null : Math.max(0, nowMs - latestReplyMs);
242
+ const buyerReplyExpiresAtMs = latestReplyMs === null ? null : latestReplyMs + BUYER_REPLY_FRESHNESS_MS;
243
+ const buyerReplyTimestampValid = latestReplyMs === null || latestReplyMs <= nowMs + EVENT_CLOCK_SKEW_MS;
244
+ const buyerReplyFresh = latestReplyMs !== null
245
+ && buyerReplyTimestampValid
246
+ && nowMs <= buyerReplyExpiresAtMs;
247
+ Object.assign(evidence, {
248
+ buyerReplyAgeMs,
249
+ buyerReplyFreshnessMs: BUYER_REPLY_FRESHNESS_MS,
250
+ buyerReplyExpiresAt: buyerReplyExpiresAtMs === null ? null : toIsoStringOrNull(buyerReplyExpiresAtMs),
251
+ buyerReplyTimestampValid,
252
+ buyerReplyFresh,
253
+ });
254
+ if (stage === 'contacted') {
255
+ if (latestReplyMs !== null) {
256
+ if (latestSendMs === null || latestReplyMs >= latestSendMs) {
257
+ return decision({
258
+ status: 'hold_pipeline_transition_missing',
259
+ nextAction: 'Record the verified buyer reply and advance the pipeline before preparing another message.',
260
+ reason: 'Buyer-reply evidence is newer than the recorded send, but the lead is still labeled contacted.',
261
+ evidence,
262
+ });
263
+ }
264
+ return decision({
265
+ status: 'hold_already_followed_up',
266
+ queueClass: 'monitor',
267
+ nextAction: 'Reconcile the stale contacted label and wait for new buyer input; do not send another follow-up.',
268
+ reason: 'The history contains a buyer reply followed by a later outbound receipt while the lead is still labeled contacted.',
269
+ evidence,
270
+ });
271
+ }
272
+ if (outboundReceiptCount >= 2) {
273
+ return decision({
274
+ status: 'hold_already_followed_up',
275
+ queueClass: 'monitor',
276
+ nextAction: 'Wait for buyer input; the one-follow-up allowance is already exhausted.',
277
+ reason: 'Two or more outbound receipts exist without a newer buyer reply.',
278
+ evidence,
279
+ });
280
+ }
281
+ const eligibleAtMs = (latestSendMs ?? parseTimestamp(stageEvidence.evidenceAt) ?? nowMs) + enforcedCooldownMs;
282
+ if (nowMs < eligibleAtMs) {
283
+ return decision({
284
+ status: 'hold_follow_up_cooldown',
285
+ queueClass: 'monitor',
286
+ nextAction: 'Wait for buyer input or the single-follow-up eligibility time; do not send during the cooldown.',
287
+ reason: 'A verified send exists and the default 48-hour single-follow-up cooldown has not elapsed.',
288
+ nextEligibleAt: toIsoStringOrNull(eligibleAtMs),
289
+ evidence,
290
+ });
291
+ }
292
+ return decision({
293
+ status: 'approval_required_follow_up',
294
+ queueClass: 'approval_ready',
295
+ readyForOutbound: true,
296
+ requiresApproval: true,
297
+ approvalPhrase: buildApprovalPhrase(leadId, 'follow_up'),
298
+ nextAction: 'Request exact action-time approval for one value-first follow-up; do not send a second follow-up without new buyer input.',
299
+ reason: 'The verified first send is older than the cooldown and no newer buyer reply is recorded.',
300
+ evidence,
301
+ });
302
+ }
303
+
304
+ if (stage === 'replied') {
305
+ if (latestSendMs !== null && latestReplyMs !== null && latestSendMs >= latestReplyMs) {
306
+ return decision({
307
+ status: 'hold_already_followed_up',
308
+ queueClass: 'monitor',
309
+ nextAction: 'Wait for a new buyer reply; do not send another follow-up.',
310
+ reason: 'A verified outbound receipt is newer than the latest verified buyer reply.',
311
+ evidence,
312
+ });
313
+ }
314
+ if (!buyerReplyTimestampValid) {
315
+ return decision({
316
+ status: 'hold_buyer_reply_time_invalid',
317
+ queueClass: 'monitor',
318
+ nextAction: 'Reconcile the buyer-reply timestamp from the authoritative source before preparing a response.',
319
+ reason: 'The latest buyer-reply receipt is more than five minutes in the future and cannot prove current intent.',
320
+ evidence,
321
+ });
322
+ }
323
+ if (!buyerReplyFresh) {
324
+ return decision({
325
+ status: 'hold_stale_buyer_reply',
326
+ queueClass: 'monitor',
327
+ nextAction: 'Do not treat this as a warm same-day signal. Wait for new buyer evidence or separately review a low-pressure reactivation after checking the current thread state.',
328
+ reason: 'The latest verified buyer reply is older than the 14-day warm-signal window.',
329
+ evidence,
330
+ });
331
+ }
332
+ return decision({
333
+ status: 'approval_required_qualification_reply',
334
+ queueClass: 'approval_ready',
335
+ readyForOutbound: true,
336
+ requiresApproval: true,
337
+ approvalPhrase: buildApprovalPhrase(leadId, 'qualification_reply'),
338
+ nextAction: 'Request exact approval for one qualification reply asking for workflow, consequence, authority, and proof target.',
339
+ reason: 'A verified buyer reply exists and no later outbound receipt is recorded.',
340
+ evidence,
341
+ });
342
+ }
343
+
344
+ if (stage === 'call_booked') {
345
+ return decision({
346
+ status: 'prepare_call_internal',
347
+ queueClass: 'internal_ready',
348
+ nextAction: 'Prepare the diagnostic agenda and evidence questions; do not claim attendance or payment.',
349
+ reason: 'The booking is verified and the next step is internal preparation.',
350
+ evidence,
351
+ });
352
+ }
353
+
354
+ if (stage === 'checkout_started') {
355
+ if (stageEvidence.evidence?.kind !== 'buyer_checkout_confirmation') {
356
+ return decision({
357
+ status: 'hold_checkout_intent_unverified',
358
+ queueClass: 'monitor',
359
+ nextAction: 'Reconcile credible buyer identity or direct buyer checkout confirmation before treating the session as intent.',
360
+ reason: 'A provider checkout object alone may be a probe, bot, owner test, or abandoned session.',
361
+ evidence,
362
+ });
363
+ }
364
+ return decision({
365
+ status: 'monitor_provider_payment',
366
+ queueClass: 'monitor',
367
+ nextAction: 'Monitor provider payment evidence; do not resend the payment link or call checkout revenue.',
368
+ reason: 'Buyer checkout confirmation exists, but provider-confirmed payment does not.',
369
+ evidence,
370
+ });
371
+ }
372
+
373
+ if (stage === 'sprint_intake') {
374
+ return decision({
375
+ status: 'review_intake_internal',
376
+ queueClass: 'internal_ready',
377
+ nextAction: 'Review the intake against the deterministic offer qualification and prepare signed scope only if it fits.',
378
+ reason: 'The intake is verified; scope and payment remain separate future states.',
379
+ evidence,
380
+ });
381
+ }
382
+
383
+ if (stage === 'paid') {
384
+ return decision({
385
+ status: 'delivery_and_expansion_review',
386
+ queueClass: 'internal_ready',
387
+ nextAction: 'Deliver the paid scope, capture outcome evidence, and evaluate recurring or Enterprise fit only after delivery proof.',
388
+ reason: 'Provider-verified payment exists; delivery and expansion are not yet implied.',
389
+ evidence,
390
+ });
391
+ }
392
+
393
+ return decision({
394
+ status: 'terminal_lost',
395
+ queueClass: 'terminal',
396
+ nextAction: 'Take no further revenue action unless a new inbound buyer signal opens a new evidence chain.',
397
+ reason: 'The lead is closed or disqualified.',
398
+ evidence,
399
+ });
400
+ }
401
+
402
+ module.exports = {
403
+ BUYER_REPLY_FRESHNESS_MS,
404
+ DIRECT_ZERO_COST_CHANNELS,
405
+ EVENT_CLOCK_SKEW_MS,
406
+ FOLLOW_UP_COOLDOWN_MS,
407
+ ZERO_SPEND_STATUSES,
408
+ buildApprovalPhrase,
409
+ evaluateRevenueActionEligibility,
410
+ latestEvidenceEvent,
411
+ resolveLeadZeroSpendStatus,
412
+ resolveZeroSpendStatus,
413
+ toIsoStringOrNull,
414
+ };