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
@@ -0,0 +1,694 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const { ensureParentDir } = require('./fs-utils');
8
+ const {
9
+ SALES_STAGE_EVIDENCE_KINDS,
10
+ evaluateLeadStageEvidence,
11
+ loadSalesLeads,
12
+ normalizeSalesStage,
13
+ } = require('./sales-pipeline');
14
+ const {
15
+ BUYER_REPLY_FRESHNESS_MS,
16
+ EVENT_CLOCK_SKEW_MS,
17
+ evaluateRevenueActionEligibility,
18
+ resolveLeadZeroSpendStatus,
19
+ } = require('./revenue-action-eligibility');
20
+
21
+ const PLACEHOLDER_PREFIX = 'REPLACE_WITH_ACTUAL_';
22
+ const STAGE_SIGNAL_FRESHNESS_MS = BUYER_REPLY_FRESHNESS_MS;
23
+ const STAGE_PRIORITY = Object.freeze({
24
+ checkout_started: 100,
25
+ sprint_intake: 95,
26
+ replied: 85,
27
+ call_booked: 80,
28
+ contacted: 60,
29
+ targeted: 40,
30
+ paid: 30,
31
+ lost: 0,
32
+ });
33
+ const PREFERRED_STAGE_EVIDENCE = Object.freeze({
34
+ contacted: 'platform_send_receipt',
35
+ replied: 'buyer_reply',
36
+ call_booked: 'booking_confirmation',
37
+ checkout_started: 'buyer_checkout_confirmation',
38
+ sprint_intake: 'intake_submission',
39
+ paid: 'provider_payment',
40
+ lost: 'operator_disqualified',
41
+ });
42
+ const SAME_DAY_CLOSE_STAGES = new Set([
43
+ 'replied',
44
+ 'call_booked',
45
+ 'checkout_started',
46
+ 'sprint_intake',
47
+ ]);
48
+
49
+ function normalizeText(value, maxLength = 1000) {
50
+ if (value === undefined || value === null) return null;
51
+ const text = String(value).trim();
52
+ return text ? text.slice(0, maxLength) : null;
53
+ }
54
+
55
+ function normalizeLimit(value, fallback = null) {
56
+ if (value === undefined || value === null || value === '') return fallback;
57
+ const parsed = Number.parseInt(String(value), 10);
58
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
59
+ }
60
+
61
+ function parseTimestamp(value) {
62
+ const parsed = Date.parse(String(value || ''));
63
+ return Number.isFinite(parsed) ? parsed : null;
64
+ }
65
+
66
+ function isConcreteEvidenceValue(value, maxLength) {
67
+ const normalized = normalizeText(value, maxLength);
68
+ return Boolean(normalized && !/^REPLACE_WITH_/i.test(normalized));
69
+ }
70
+
71
+ function resolveCurrentStageEnteredAt(lead = {}) {
72
+ const stage = normalizeSalesStage(lead.stage, 'targeted');
73
+ const history = Array.isArray(lead.history) ? lead.history : [];
74
+ const transition = history
75
+ .filter((event) => (
76
+ event?.toStage === stage
77
+ && event?.fromStage !== stage
78
+ && parseTimestamp(event?.at) !== null
79
+ ))
80
+ .reduce((latest, event) => {
81
+ if (!latest) return event;
82
+ return parseTimestamp(event.at) > parseTimestamp(latest.at) ? event : latest;
83
+ }, null);
84
+ return transition?.at || normalizeText(lead.createdAt, 64) || null;
85
+ }
86
+
87
+ function resolvePreferredStageEvidence(lead = {}) {
88
+ const stage = normalizeSalesStage(lead.stage, 'targeted');
89
+ const preferredKind = PREFERRED_STAGE_EVIDENCE[stage] || null;
90
+ if (!preferredKind) return null;
91
+ const history = Array.isArray(lead.history) ? lead.history : [];
92
+ const supportingEvent = history
93
+ .filter((event) => (
94
+ event?.toStage === stage
95
+ && event?.evidence?.kind === preferredKind
96
+ && isConcreteEvidenceValue(event?.evidence?.source, 160)
97
+ && isConcreteEvidenceValue(event?.evidence?.reference, 1000)
98
+ && parseTimestamp(event?.at) !== null
99
+ ))
100
+ .reduce((latest, event) => {
101
+ if (!latest) return event;
102
+ return parseTimestamp(event.at) > parseTimestamp(latest.at) ? event : latest;
103
+ }, null);
104
+ if (!supportingEvent) return null;
105
+ return {
106
+ kind: preferredKind,
107
+ at: supportingEvent.at,
108
+ source: supportingEvent.evidence.source,
109
+ reference: supportingEvent.evidence.reference,
110
+ };
111
+ }
112
+
113
+ function buildPreferredStageEvidenceTiming(lead = {}, {
114
+ now = new Date().toISOString(),
115
+ } = {}) {
116
+ const nowMs = parseTimestamp(now) ?? Date.now();
117
+ const preferredKind = PREFERRED_STAGE_EVIDENCE[normalizeSalesStage(lead.stage, 'targeted')] || null;
118
+ const preferredEvidence = resolvePreferredStageEvidence(lead);
119
+ const preferredEvidenceMs = parseTimestamp(preferredEvidence?.at);
120
+ const preferredEvidenceExpiresAtMs = preferredEvidenceMs === null
121
+ ? null
122
+ : preferredEvidenceMs + STAGE_SIGNAL_FRESHNESS_MS;
123
+ const preferredEvidenceTimestampValid = preferredEvidenceMs !== null
124
+ && preferredEvidenceMs <= nowMs + EVENT_CLOCK_SKEW_MS;
125
+ const preferredEvidenceFresh = preferredEvidenceMs !== null
126
+ && preferredEvidenceTimestampValid
127
+ && nowMs <= preferredEvidenceExpiresAtMs;
128
+ return {
129
+ preferredStageEvidenceKind: preferredKind,
130
+ preferredStageEvidenceAt: preferredEvidence?.at || null,
131
+ preferredStageEvidenceAgeMs: preferredEvidenceMs === null ? null : Math.max(0, nowMs - preferredEvidenceMs),
132
+ preferredStageEvidenceExpiresAt: preferredEvidenceExpiresAtMs === null
133
+ ? null
134
+ : new Date(preferredEvidenceExpiresAtMs).toISOString(),
135
+ preferredStageEvidenceTimestampValid: preferredEvidenceTimestampValid,
136
+ preferredStageEvidenceFresh: preferredEvidenceFresh,
137
+ };
138
+ }
139
+
140
+ function buildStageSignalTiming(lead = {}, stageEvidence = evaluateLeadStageEvidence(lead), {
141
+ now = new Date().toISOString(),
142
+ } = {}) {
143
+ const nowMs = parseTimestamp(now) ?? Date.now();
144
+ const stageEnteredAt = resolveCurrentStageEnteredAt(lead);
145
+ const stageSignalAt = stageEvidence.verified === true
146
+ ? normalizeText(stageEvidence.evidenceAt, 64)
147
+ : stageEnteredAt;
148
+ const stageSignalMs = parseTimestamp(stageSignalAt);
149
+ const stageSignalAgeMs = stageSignalMs === null ? null : Math.max(0, nowMs - stageSignalMs);
150
+ const stageSignalExpiresAtMs = stageSignalMs === null
151
+ ? null
152
+ : stageSignalMs + STAGE_SIGNAL_FRESHNESS_MS;
153
+ const stageSignalTimestampValid = stageSignalMs !== null
154
+ && stageSignalMs <= nowMs + EVENT_CLOCK_SKEW_MS;
155
+ const stageSignalFresh = stageSignalMs !== null
156
+ && stageSignalTimestampValid
157
+ && nowMs <= stageSignalExpiresAtMs;
158
+ return {
159
+ stageEnteredAt,
160
+ stageSignalAt,
161
+ stageSignalAgeMs,
162
+ stageSignalExpiresAt: stageSignalExpiresAtMs === null
163
+ ? null
164
+ : new Date(stageSignalExpiresAtMs).toISOString(),
165
+ stageSignalFreshnessMs: STAGE_SIGNAL_FRESHNESS_MS,
166
+ stageSignalTimestampValid,
167
+ stageSignalFresh,
168
+ };
169
+ }
170
+
171
+ function shellQuote(value) {
172
+ return `'${String(value ?? '').replaceAll("'", `'\\''`)}'`;
173
+ }
174
+
175
+ function formatArgv(argv = []) {
176
+ return argv.map(shellQuote).join(' ');
177
+ }
178
+
179
+ function loadTargetRecords(targetsPath = null) {
180
+ if (!targetsPath) return [];
181
+ const resolved = path.resolve(targetsPath);
182
+ const text = fs.readFileSync(resolved, 'utf8').trim();
183
+ if (!text) return [];
184
+
185
+ if (text.startsWith('[') || text.startsWith('{')) {
186
+ try {
187
+ const parsed = JSON.parse(text);
188
+ if (Array.isArray(parsed)) return parsed;
189
+ if (Array.isArray(parsed.targets)) return parsed.targets;
190
+ if (Array.isArray(parsed.rows)) return parsed.rows;
191
+ } catch {
192
+ // Fall through to JSONL so one malformed aggregate does not hide valid rows.
193
+ }
194
+ }
195
+
196
+ return text.split(/\n+/).map((line, index) => {
197
+ try {
198
+ return JSON.parse(line);
199
+ } catch (error) {
200
+ throw new Error(`Invalid target JSONL at line ${index + 1}: ${error.message}`);
201
+ }
202
+ });
203
+ }
204
+
205
+ function buildTargetMap(targets = []) {
206
+ const map = new Map();
207
+ for (const target of targets) {
208
+ const leadId = normalizeText(target?.pipelineLeadId || target?.leadId, 160);
209
+ if (leadId) map.set(leadId, target);
210
+ }
211
+ return map;
212
+ }
213
+
214
+ function evidenceRequirement(stage, preferredKind) {
215
+ const requirements = {
216
+ platform_send_receipt: 'A platform- or mail-origin delivery/send receipt with a stable message URL or message ID.',
217
+ buyer_reply: 'A buyer-origin reply with a stable message URL or message ID; an operator note is not a reply.',
218
+ booking_confirmation: 'A calendar or scheduling-provider confirmation tied to this buyer and call.',
219
+ buyer_checkout_confirmation: 'A direct buyer confirmation tied to the checkout attempt; a provider session object alone is not buyer intent.',
220
+ provider_checkout_session: 'A live provider session reference; this verifies the object only and does not prove buyer intent or payment.',
221
+ intake_submission: 'A persisted first-party intake submission tied to this buyer and workflow.',
222
+ workflow_materials_received: 'A durable reference proving the buyer supplied the agreed workflow materials.',
223
+ provider_payment: 'A live provider API payment reconciled through sales:reconcile-payment with provider identity and digest proof.',
224
+ buyer_declined: 'A buyer-origin decline receipt.',
225
+ operator_disqualified: 'A durable operator review record with the concrete disqualification reason.',
226
+ stale_closed: 'A durable operator closure record showing the staleness rule and evidence window.',
227
+ provider_refund: 'A live provider API refund reconciled with provider identity and digest proof.',
228
+ };
229
+ return requirements[preferredKind]
230
+ || `Stage ${stage} requires one of: ${(SALES_STAGE_EVIDENCE_KINDS[stage] || []).join(', ') || 'none'}.`;
231
+ }
232
+
233
+ function buildReceiptCommandTemplate(lead = {}, preferredKind) {
234
+ const stage = normalizeSalesStage(lead.stage, 'targeted');
235
+ if (stage === 'targeted') return null;
236
+ if (stage === 'paid' || preferredKind === 'provider_payment') {
237
+ const argv = [
238
+ 'npm', 'run', 'sales:reconcile-payment', '--',
239
+ '--provider', `${PLACEHOLDER_PREFIX}PAYMENT_PROVIDER`,
240
+ '--payment', `${PLACEHOLDER_PREFIX}PROVIDER_PAYMENT_ID`,
241
+ '--lead', lead.leadId,
242
+ ];
243
+ return {
244
+ executable: false,
245
+ reason: 'Payment state is provider-controlled; replace the payment ID only after a live provider readback.',
246
+ argv,
247
+ display: formatArgv(argv),
248
+ };
249
+ }
250
+
251
+ const argv = [
252
+ 'node', 'scripts/sales-pipeline.js', 'advance',
253
+ '--lead', lead.leadId,
254
+ '--stage', stage,
255
+ '--evidence-kind', preferredKind,
256
+ '--evidence-source', `${PLACEHOLDER_PREFIX}SOURCE`,
257
+ '--evidence-ref', `${PLACEHOLDER_PREFIX}RECEIPT`,
258
+ '--timestamp', `${PLACEHOLDER_PREFIX}TIMESTAMP`,
259
+ ];
260
+ return {
261
+ executable: false,
262
+ reason: 'The source, receipt, and timestamp placeholders make this template fail closed until real read-only evidence is inspected.',
263
+ argv,
264
+ display: formatArgv(argv),
265
+ };
266
+ }
267
+
268
+ function buildStageEvidenceRepair(lead = {}, stageEvidence = evaluateLeadStageEvidence(lead)) {
269
+ if (stageEvidence.verified === true) return null;
270
+ const stage = normalizeSalesStage(lead.stage, 'targeted');
271
+ const allowedEvidenceKinds = SALES_STAGE_EVIDENCE_KINDS[stage] || [];
272
+ const preferredKind = PREFERRED_STAGE_EVIDENCE[stage] || allowedEvidenceKinds[0] || null;
273
+ if (!preferredKind) return null;
274
+ return {
275
+ kind: 'stage_evidence_repair',
276
+ stage,
277
+ preferredEvidenceKind: preferredKind,
278
+ allowedEvidenceKinds,
279
+ requirement: evidenceRequirement(stage, preferredKind),
280
+ safeAction: `Inspect the authoritative ${preferredKind} source read-only. Record it only if the receipt exists and belongs to this lead; otherwise keep the lead held.`,
281
+ commandTemplate: buildReceiptCommandTemplate(lead, preferredKind),
282
+ };
283
+ }
284
+
285
+ function buildBlockers({ zeroSpendStatus, stageEvidence, actionEligibility, stageSignalTiming, stage }) {
286
+ const blockers = [];
287
+ if (zeroSpendStatus === 'discarded_paid_requirement') {
288
+ blockers.push({
289
+ code: 'paid_requirement',
290
+ reason: 'This route requires seller payment, paid access, subscription, or revenue share.',
291
+ });
292
+ } else if (zeroSpendStatus === 'hold_unverified_cost') {
293
+ blockers.push({
294
+ code: 'cost_unverified',
295
+ reason: 'The complete funnel is not proven free of present or future seller obligations.',
296
+ });
297
+ }
298
+ if (stageEvidence.verified !== true) {
299
+ blockers.push({
300
+ code: 'stage_evidence_missing',
301
+ reason: stageEvidence.reason || 'Current stage lacks stage-appropriate evidence.',
302
+ });
303
+ }
304
+ if (actionEligibility.status === 'hold_pipeline_transition_missing') {
305
+ blockers.push({
306
+ code: 'pipeline_transition_missing',
307
+ reason: actionEligibility.reason,
308
+ });
309
+ }
310
+ if (actionEligibility.status === 'hold_follow_up_cooldown') {
311
+ blockers.push({
312
+ code: 'follow_up_cooldown',
313
+ reason: actionEligibility.reason,
314
+ });
315
+ }
316
+ if (actionEligibility.status === 'hold_already_followed_up') {
317
+ blockers.push({
318
+ code: 'follow_up_exhausted',
319
+ reason: actionEligibility.reason,
320
+ });
321
+ }
322
+ if (actionEligibility.status === 'hold_checkout_intent_unverified') {
323
+ blockers.push({
324
+ code: 'checkout_intent_unverified',
325
+ reason: actionEligibility.reason,
326
+ });
327
+ }
328
+ if (actionEligibility.status === 'hold_stale_buyer_reply') {
329
+ blockers.push({
330
+ code: 'buyer_reply_stale',
331
+ reason: actionEligibility.reason,
332
+ });
333
+ }
334
+ if (actionEligibility.status === 'hold_buyer_reply_time_invalid') {
335
+ blockers.push({
336
+ code: 'buyer_reply_time_invalid',
337
+ reason: actionEligibility.reason,
338
+ });
339
+ }
340
+ if (
341
+ SAME_DAY_CLOSE_STAGES.has(stage)
342
+ && stageSignalTiming.stageSignalTimestampValid === false
343
+ && actionEligibility.status !== 'hold_buyer_reply_time_invalid'
344
+ ) {
345
+ blockers.push({
346
+ code: stageSignalTiming.stageSignalAt ? 'stage_signal_time_invalid' : 'stage_signal_time_missing',
347
+ reason: stageSignalTiming.stageSignalAt
348
+ ? 'The current-stage signal is more than five minutes in the future and cannot receive same-day priority.'
349
+ : 'The current-stage signal has no valid timestamp and cannot receive same-day priority.',
350
+ });
351
+ } else if (
352
+ SAME_DAY_CLOSE_STAGES.has(stage)
353
+ && stageSignalTiming.stageSignalFresh === false
354
+ && actionEligibility.status !== 'hold_stale_buyer_reply'
355
+ ) {
356
+ blockers.push({
357
+ code: stageEvidence.verified === true ? 'stage_signal_stale' : 'stage_label_stale',
358
+ reason: stageEvidence.verified === true
359
+ ? 'The latest stage-appropriate signal is older than 14 days and is not a same-day revenue signal.'
360
+ : 'The unverified high-intent stage label was entered more than 14 days ago and is not a same-day revenue signal.',
361
+ });
362
+ }
363
+ return blockers;
364
+ }
365
+
366
+ function resolveRemediationQueueClass({ zeroSpendStatus, stageEvidence, actionEligibility }) {
367
+ if (zeroSpendStatus === 'discarded_paid_requirement') return 'discarded';
368
+ if (zeroSpendStatus === 'hold_unverified_cost' || stageEvidence.verified !== true) return 'remediation';
369
+ if (actionEligibility.queueClass === 'approval_ready') return 'approval_ready';
370
+ if (actionEligibility.queueClass === 'internal_ready') return 'internal_ready';
371
+ if (actionEligibility.queueClass === 'terminal') return 'terminal';
372
+ return 'monitor';
373
+ }
374
+
375
+ function scoreRemediationRow(row = {}) {
376
+ if (row.queueClass === 'discarded') return -1000;
377
+ let score = STAGE_PRIORITY[row.stage] || 0;
378
+ if (row.queueClass === 'remediation') score += 10;
379
+ // A verified buyer conversation must outrank repair work. Evidence repair is
380
+ // important, but it is not intent and must never displace a real warm reply.
381
+ if (row.queueClass === 'approval_ready') score += 30;
382
+ if (row.queueClass === 'internal_ready') score += 15;
383
+ if (row.zeroSpendStatus === 'hold_unverified_cost') score -= 20;
384
+ if (row.actionStatus === 'hold_already_followed_up') score -= 30;
385
+ if (row.actionStatus === 'hold_follow_up_cooldown') score -= 10;
386
+ if (row.actionStatus === 'hold_stale_buyer_reply') score -= 25;
387
+ if (row.actionStatus === 'hold_buyer_reply_time_invalid') score -= 40;
388
+ if (SAME_DAY_CLOSE_STAGES.has(row.stage) && row.stageSignalFresh === false) score -= 35;
389
+ if (row.stage === 'paid') score -= 20;
390
+ return score;
391
+ }
392
+
393
+ function buildSafeNextAction({ zeroSpendStatus, stageEvidenceRepair, actionEligibility }) {
394
+ if (zeroSpendStatus === 'discarded_paid_requirement') {
395
+ return actionEligibility.zeroCostReplacement
396
+ || 'Discard the paid route and use an existing warm conversation, owned intake, or direct organic channel.';
397
+ }
398
+ if (zeroSpendStatus === 'hold_unverified_cost') {
399
+ return 'Inspect the complete downstream terms read-only. If zero cost cannot be proven, discard the route and use a direct organic replacement.';
400
+ }
401
+ if (stageEvidenceRepair) return stageEvidenceRepair.safeAction;
402
+ return actionEligibility.nextAction;
403
+ }
404
+
405
+ function buildRemediationRow(lead = {}, target = {}, { now } = {}) {
406
+ const evaluatedAt = now || new Date().toISOString();
407
+ const stage = normalizeSalesStage(lead.stage, 'targeted');
408
+ const zeroSpendStatus = resolveLeadZeroSpendStatus(lead, target);
409
+ const stageEvidence = evaluateLeadStageEvidence(lead);
410
+ const actionEligibility = evaluateRevenueActionEligibility(lead, target, { now: evaluatedAt });
411
+ const stageSignalTiming = buildStageSignalTiming(lead, stageEvidence, { now: evaluatedAt });
412
+ const preferredStageEvidenceTiming = buildPreferredStageEvidenceTiming(lead, { now: evaluatedAt });
413
+ const stageEvidenceRepair = buildStageEvidenceRepair(lead, stageEvidence);
414
+ const blockers = buildBlockers({
415
+ zeroSpendStatus,
416
+ stageEvidence,
417
+ actionEligibility,
418
+ stageSignalTiming,
419
+ stage,
420
+ });
421
+ const queueClass = resolveRemediationQueueClass({ zeroSpendStatus, stageEvidence, actionEligibility });
422
+ const sameDayPriorityEligible = SAME_DAY_CLOSE_STAGES.has(stage)
423
+ && zeroSpendStatus === 'proceed_zero_cost'
424
+ && actionEligibility.status !== 'hold_stale_buyer_reply'
425
+ && actionEligibility.status !== 'hold_buyer_reply_time_invalid';
426
+ const row = {
427
+ leadId: lead.leadId,
428
+ stage,
429
+ source: normalizeText(target.source || lead.source, 80) || 'manual',
430
+ channel: normalizeText(target.channel || lead.channel, 80) || 'manual',
431
+ offer: normalizeText(lead.offer, 120) || 'workflow_hardening_sprint',
432
+ queueClass,
433
+ actionStatus: actionEligibility.status,
434
+ zeroSpendStatus,
435
+ stageEvidenceVerified: stageEvidence.verified === true,
436
+ stageEvidenceKind: stageEvidence.evidence?.kind || null,
437
+ ...stageSignalTiming,
438
+ ...preferredStageEvidenceTiming,
439
+ sameDayEvidencePriority: sameDayPriorityEligible
440
+ && preferredStageEvidenceTiming.preferredStageEvidenceFresh === true,
441
+ sameDayReviewPriority: sameDayPriorityEligible
442
+ && stageSignalTiming.stageSignalFresh === true
443
+ && preferredStageEvidenceTiming.preferredStageEvidenceFresh !== true
444
+ && !['discarded', 'terminal'].includes(queueClass),
445
+ readyForOutbound: actionEligibility.readyForOutbound === true,
446
+ requiresApproval: actionEligibility.requiresApproval === true,
447
+ approvalPhrase: actionEligibility.approvalPhrase || null,
448
+ nextEligibleAt: actionEligibility.nextEligibleAt || null,
449
+ blockers,
450
+ stageEvidenceRepair,
451
+ zeroCostReplacement: actionEligibility.zeroCostReplacement || null,
452
+ safeNextAction: buildSafeNextAction({ zeroSpendStatus, stageEvidenceRepair, actionEligibility }),
453
+ reason: actionEligibility.reason,
454
+ };
455
+ row.priorityScore = scoreRemediationRow(row);
456
+ return row;
457
+ }
458
+
459
+ function summarizeRows(rows = []) {
460
+ const byQueueClass = {};
461
+ const byActionStatus = {};
462
+ const byStage = {};
463
+ for (const row of rows) {
464
+ byQueueClass[row.queueClass] = (byQueueClass[row.queueClass] || 0) + 1;
465
+ byActionStatus[row.actionStatus] = (byActionStatus[row.actionStatus] || 0) + 1;
466
+ byStage[row.stage] = (byStage[row.stage] || 0) + 1;
467
+ }
468
+ return {
469
+ total: rows.length,
470
+ byQueueClass,
471
+ byActionStatus,
472
+ byStage,
473
+ sameDayEvidencePriority: rows.filter((row) => row.sameDayEvidencePriority).length,
474
+ sameDayReviewPriority: rows.filter((row) => row.sameDayReviewPriority).length,
475
+ approvalReady: rows.filter((row) => row.queueClass === 'approval_ready').length,
476
+ internalReady: rows.filter((row) => row.queueClass === 'internal_ready').length,
477
+ remediationRequired: rows.filter((row) => row.queueClass === 'remediation').length,
478
+ discardedPaidRequirement: rows.filter((row) => row.queueClass === 'discarded').length,
479
+ };
480
+ }
481
+
482
+ function buildPrimaryAction(rows = []) {
483
+ const row = rows.find((candidate) => !['discarded', 'terminal'].includes(candidate.queueClass));
484
+ if (!row) return null;
485
+ return {
486
+ leadId: row.leadId,
487
+ stage: row.stage,
488
+ queueClass: row.queueClass,
489
+ actionStatus: row.actionStatus,
490
+ priorityScore: row.priorityScore,
491
+ sameDayEvidencePriority: row.sameDayEvidencePriority,
492
+ sameDayReviewPriority: row.sameDayReviewPriority,
493
+ safeAction: row.safeNextAction,
494
+ proofRequired: row.stageEvidenceRepair?.requirement || row.reason,
495
+ approvalPhrase: row.approvalPhrase,
496
+ externalSideEffectAuthorized: false,
497
+ };
498
+ }
499
+
500
+ function buildRevenueEvidenceRemediationQueue({ leads = [], targets = [], now = new Date().toISOString() } = {}) {
501
+ const targetMap = buildTargetMap(targets);
502
+ const rows = leads
503
+ .map((lead) => buildRemediationRow(lead, targetMap.get(lead.leadId) || {}, { now }))
504
+ .sort((left, right) => (
505
+ right.priorityScore - left.priorityScore
506
+ || String(left.leadId).localeCompare(String(right.leadId))
507
+ ));
508
+ return {
509
+ generatedAt: now,
510
+ claimBoundary: 'This queue proves local action/remediation eligibility only. A fresh-label review row is not verified buyer evidence. The queue does not prove a send, reply, checkout, payment, revenue, or customer outcome.',
511
+ zeroSpendRule: 'No paid access, seller fee, subscription, credit purchase, card-required trial, or revenue share may be used to reach a buyer.',
512
+ summary: summarizeRows(rows),
513
+ primaryAction: buildPrimaryAction(rows),
514
+ rows,
515
+ };
516
+ }
517
+
518
+ function renderRowMarkdown(row, index) {
519
+ const blockerLines = row.blockers.length
520
+ ? row.blockers.map((blocker) => ` - ${blocker.code}: ${blocker.reason}`)
521
+ : [' - none'];
522
+ const repair = row.stageEvidenceRepair;
523
+ return [
524
+ `### ${index + 1}. ${row.leadId}`,
525
+ `- Stage: ${row.stage}`,
526
+ `- Queue: ${row.queueClass}`,
527
+ `- Priority score: ${row.priorityScore}`,
528
+ `- Action status: ${row.actionStatus}`,
529
+ `- Zero-spend status: ${row.zeroSpendStatus}`,
530
+ `- Current-stage evidence verified: ${row.stageEvidenceVerified}`,
531
+ `- Preferred same-day evidence kind: ${row.preferredStageEvidenceKind || 'none'}`,
532
+ `- Preferred same-day evidence at: ${row.preferredStageEvidenceAt || 'none'}`,
533
+ `- Preferred same-day evidence fresh: ${row.preferredStageEvidenceFresh}`,
534
+ `- Verified same-day evidence priority: ${row.sameDayEvidencePriority}`,
535
+ `- Fresh-label read-only review priority: ${row.sameDayReviewPriority}`,
536
+ `- Ready for outbound: ${row.readyForOutbound}`,
537
+ `- Approval phrase: ${row.approvalPhrase || 'none'}`,
538
+ '- Blockers:',
539
+ ...blockerLines,
540
+ `- Safe next action: ${row.safeNextAction}`,
541
+ ...(repair ? [
542
+ `- Preferred evidence: ${repair.preferredEvidenceKind}`,
543
+ `- Proof required: ${repair.requirement}`,
544
+ '- Non-executable repair template:',
545
+ '```text',
546
+ repair.commandTemplate?.display || 'none',
547
+ '```',
548
+ ] : []),
549
+ '',
550
+ ];
551
+ }
552
+
553
+ function renderRevenueEvidenceRemediationMarkdown(queue = {}, { limit = null } = {}) {
554
+ const rows = Array.isArray(queue.rows) ? queue.rows : [];
555
+ const displayedRows = normalizeLimit(limit, null) ? rows.slice(0, normalizeLimit(limit, null)) : rows;
556
+ const summary = queue.summary || summarizeRows(rows);
557
+ const primary = queue.primaryAction;
558
+ return [
559
+ '# Revenue Evidence Remediation Queue',
560
+ '',
561
+ `Generated: ${queue.generatedAt || new Date().toISOString()}`,
562
+ '',
563
+ queue.claimBoundary,
564
+ '',
565
+ `Zero-spend rule: ${queue.zeroSpendRule}`,
566
+ '',
567
+ '## Summary',
568
+ '',
569
+ `- Total leads: ${summary.total || 0}`,
570
+ `- Verified same-day evidence-priority rows: ${summary.sameDayEvidencePriority || 0}`,
571
+ `- Fresh-label read-only review rows: ${summary.sameDayReviewPriority || 0}`,
572
+ `- Remediation required: ${summary.remediationRequired || 0}`,
573
+ `- Approval ready: ${summary.approvalReady || 0}`,
574
+ `- Internal ready: ${summary.internalReady || 0}`,
575
+ `- Discarded paid requirement: ${summary.discardedPaidRequirement || 0}`,
576
+ '',
577
+ '## Primary Safe Action',
578
+ '',
579
+ ...(primary ? [
580
+ `- Lead: ${primary.leadId}`,
581
+ `- Stage: ${primary.stage}`,
582
+ `- Queue: ${primary.queueClass}`,
583
+ `- Verified same-day evidence priority: ${primary.sameDayEvidencePriority}`,
584
+ `- Fresh-label read-only review priority: ${primary.sameDayReviewPriority}`,
585
+ `- Action: ${primary.safeAction}`,
586
+ `- Proof required: ${primary.proofRequired}`,
587
+ `- Approval phrase: ${primary.approvalPhrase || 'none'}`,
588
+ `- External side effect authorized: ${primary.externalSideEffectAuthorized}`,
589
+ ] : ['- No non-terminal action is available.']),
590
+ '',
591
+ '## Ranked Rows',
592
+ '',
593
+ ...(displayedRows.length ? displayedRows.flatMap(renderRowMarkdown) : ['- No leads loaded.', '']),
594
+ ].join('\n').trimEnd() + '\n';
595
+ }
596
+
597
+ function parseArgs(argv = []) {
598
+ const options = {};
599
+ for (let index = 0; index < argv.length; index += 1) {
600
+ const arg = argv[index];
601
+ if (!arg.startsWith('--')) continue;
602
+ const eqIndex = arg.indexOf('=', 2);
603
+ const rawKey = eqIndex === -1 ? arg.slice(2) : arg.slice(2, eqIndex);
604
+ const key = rawKey.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
605
+ if (eqIndex !== -1) {
606
+ options[key] = arg.slice(eqIndex + 1);
607
+ continue;
608
+ }
609
+ const next = argv[index + 1];
610
+ if (next && !next.startsWith('--')) {
611
+ options[key] = next;
612
+ index += 1;
613
+ } else {
614
+ options[key] = true;
615
+ }
616
+ }
617
+ return options;
618
+ }
619
+
620
+ function writeArtifact(outPath, content) {
621
+ if (!outPath) return null;
622
+ const resolved = path.resolve(outPath);
623
+ ensureParentDir(resolved);
624
+ fs.writeFileSync(resolved, content, 'utf8');
625
+ return resolved;
626
+ }
627
+
628
+ function runCli(argv = process.argv.slice(2)) {
629
+ const options = parseArgs(argv);
630
+ const leads = loadSalesLeads({ statePath: options.state, feedbackDir: options.feedbackDir });
631
+ const targets = loadTargetRecords(options.targets || null);
632
+ const queue = buildRevenueEvidenceRemediationQueue({
633
+ leads,
634
+ targets,
635
+ now: options.now || new Date().toISOString(),
636
+ });
637
+ const markdown = renderRevenueEvidenceRemediationMarkdown(queue, { limit: options.limit });
638
+ const markdownPath = writeArtifact(options.out, markdown);
639
+ const jsonPath = writeArtifact(options.jsonOut, `${JSON.stringify(queue, null, 2)}\n`);
640
+ return {
641
+ queue,
642
+ markdown,
643
+ output: options.json === true ? `${JSON.stringify(queue, null, 2)}\n` : markdown,
644
+ markdownPath,
645
+ jsonPath,
646
+ };
647
+ }
648
+
649
+ function isCliInvocation(argv = process.argv) {
650
+ const invokedPath = argv[1];
651
+ if (!invokedPath) return false;
652
+ try {
653
+ return fs.realpathSync(path.resolve(invokedPath)) === fs.realpathSync(__filename);
654
+ } catch {
655
+ return path.resolve(invokedPath) === path.resolve(__filename);
656
+ }
657
+ }
658
+
659
+ if (isCliInvocation()) {
660
+ try {
661
+ const result = runCli();
662
+ process.stdout.write(result.output);
663
+ } catch (error) {
664
+ console.error(error?.message || error);
665
+ process.exitCode = 1;
666
+ }
667
+ }
668
+
669
+ module.exports = {
670
+ PLACEHOLDER_PREFIX,
671
+ PREFERRED_STAGE_EVIDENCE,
672
+ SAME_DAY_CLOSE_STAGES,
673
+ STAGE_SIGNAL_FRESHNESS_MS,
674
+ STAGE_PRIORITY,
675
+ buildPrimaryAction,
676
+ buildReceiptCommandTemplate,
677
+ buildRemediationRow,
678
+ buildRevenueEvidenceRemediationQueue,
679
+ buildPreferredStageEvidenceTiming,
680
+ buildStageEvidenceRepair,
681
+ buildStageSignalTiming,
682
+ formatArgv,
683
+ isCliInvocation,
684
+ loadTargetRecords,
685
+ parseArgs,
686
+ resolvePreferredStageEvidence,
687
+ resolveCurrentStageEnteredAt,
688
+ renderRevenueEvidenceRemediationMarkdown,
689
+ runCli,
690
+ scoreRemediationRow,
691
+ shellQuote,
692
+ summarizeRows,
693
+ writeArtifact,
694
+ };