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,483 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('node:crypto');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const { resolveHostedBillingConfig } = require('./hosted-config');
10
+
11
+ const DEFAULT_TIMEOUT_MS = 15000;
12
+ const DEFAULT_LIMIT = 50;
13
+ const MAX_LIMIT = 100;
14
+ const OPERATOR_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
15
+ const ALLOWED_STATUSES = new Set([
16
+ 'all',
17
+ 'new',
18
+ 'qualified',
19
+ 'named_pilot',
20
+ 'proof_backed_run',
21
+ 'paid_team',
22
+ ]);
23
+ const ALLOWED_PRIORITY_BANDS = new Set(['review_now', 'review_next', 'hold_or_nurture']);
24
+ const ALLOWED_FIT_BANDS = new Set(['strong_evidence_for_review', 'partial_evidence', 'incomplete_evidence']);
25
+ const ALLOWED_ROUTES = new Set(['diagnostic', 'close', 'nurture', 'disqualify']);
26
+ const ALLOWED_OPERATOR_STEPS = new Set([
27
+ 'request_action_time_approval',
28
+ 'request_action_time_approval_for_discovery',
29
+ 'review_and_qualify',
30
+ 'prepare_scope_or_hold',
31
+ ]);
32
+ const ALLOWED_CLOSE_STATUSES = new Set(['approval_ready_not_authorized', 'hold_not_approval_ready']);
33
+ const ALLOWED_DISCOVERY_STATUSES = new Set(['approval_ready_not_authorized', 'hold_not_approval_ready']);
34
+ const ALLOWED_UNKNOWNS = new Set([
35
+ 'contactEmail',
36
+ 'workflow',
37
+ 'owner',
38
+ 'repeatedFailure',
39
+ 'runtime',
40
+ 'urgencyAndTrigger',
41
+ 'measurableCurrentImpact',
42
+ 'decisionAuthority',
43
+ 'budgetMechanism',
44
+ 'priceUnderstanding',
45
+ 'proofRequiredToDecide',
46
+ ]);
47
+ const ALLOWED_CLOSE_BLOCKERS = new Set([
48
+ 'qualification_review_unverified',
49
+ 'lifecycle_not_evidence_qualified',
50
+ 'intake_not_current',
51
+ 'contact_path_missing',
52
+ 'contact_path_invalid',
53
+ 'material_unknowns_remaining',
54
+ 'price_understanding_unconfirmed',
55
+ 'offer_unavailable',
56
+ 'route_not_closeable',
57
+ 'checkout_url_unavailable',
58
+ ]);
59
+ const ALLOWED_DISCOVERY_BLOCKERS = new Set([
60
+ 'lifecycle_not_new',
61
+ 'intake_not_current',
62
+ 'contact_path_missing',
63
+ 'contact_path_invalid',
64
+ 'no_material_questions',
65
+ 'disqualified',
66
+ 'zero_spend_unverified',
67
+ ]);
68
+ const ALLOWED_OFFER_IDS = new Set([
69
+ 'workflow_hardening_diagnostic',
70
+ 'workflow_hardening_sprint',
71
+ 'workflow_reliability_operations',
72
+ 'enterprise_governance_pilot',
73
+ 'enterprise_reliability_operations',
74
+ ]);
75
+
76
+ function queueError(code, message, status = null) {
77
+ const error = new Error(message);
78
+ error.code = code;
79
+ if (status !== null) error.status = status;
80
+ return error;
81
+ }
82
+
83
+ function parsePositiveInteger(value, label, maximum = Number.MAX_SAFE_INTEGER) {
84
+ const parsed = Number(value);
85
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maximum) {
86
+ throw queueError('invalid_argument', `${label} must be an integer from 1 to ${maximum}.`);
87
+ }
88
+ return parsed;
89
+ }
90
+
91
+ function parseArgs(argv = []) {
92
+ const options = {
93
+ json: false,
94
+ statuses: ['new', 'qualified'],
95
+ limit: DEFAULT_LIMIT,
96
+ timeoutMs: DEFAULT_TIMEOUT_MS,
97
+ exportPrivatePath: null,
98
+ };
99
+
100
+ for (const arg of argv) {
101
+ if (arg === '--json') {
102
+ options.json = true;
103
+ continue;
104
+ }
105
+ if (arg.startsWith('--status=')) {
106
+ const statuses = [...new Set(arg.slice('--status='.length)
107
+ .split(',')
108
+ .map((value) => value.trim().toLowerCase())
109
+ .filter(Boolean))];
110
+ if (statuses.length === 0 || statuses.some((status) => !ALLOWED_STATUSES.has(status)) ||
111
+ (statuses.includes('all') && statuses.length !== 1)) {
112
+ throw queueError(
113
+ 'invalid_argument',
114
+ `status must be all or a comma-separated subset of ${[...ALLOWED_STATUSES].filter((value) => value !== 'all').join(', ')}.`
115
+ );
116
+ }
117
+ options.statuses = statuses;
118
+ continue;
119
+ }
120
+ if (arg.startsWith('--limit=')) {
121
+ options.limit = parsePositiveInteger(arg.slice('--limit='.length), 'limit', MAX_LIMIT);
122
+ continue;
123
+ }
124
+ if (arg.startsWith('--timeout-ms=')) {
125
+ options.timeoutMs = parsePositiveInteger(arg.slice('--timeout-ms='.length), 'timeout-ms', 120000);
126
+ continue;
127
+ }
128
+ if (arg.startsWith('--export-private=')) {
129
+ const outputPath = arg.slice('--export-private='.length).trim();
130
+ if (!outputPath || !path.isAbsolute(outputPath)) {
131
+ throw queueError('invalid_argument', 'export-private must be an absolute local file path.');
132
+ }
133
+ options.exportPrivatePath = path.resolve(outputPath);
134
+ continue;
135
+ }
136
+ throw queueError('invalid_argument', `Unknown argument: ${arg}`);
137
+ }
138
+
139
+ return options;
140
+ }
141
+
142
+ function normalizeText(value) {
143
+ const normalized = String(value || '').trim();
144
+ return normalized || null;
145
+ }
146
+
147
+ function loadOperatorConfig(configPath = OPERATOR_CONFIG_PATH) {
148
+ try {
149
+ const stat = fs.lstatSync(configPath);
150
+ if (!stat.isFile() || stat.isSymbolicLink()) return { operatorKey: null, baseUrl: null };
151
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
152
+ return { operatorKey: null, baseUrl: null };
153
+ }
154
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
155
+ return {
156
+ operatorKey: normalizeText(parsed.operatorKey),
157
+ baseUrl: normalizeText(parsed.baseUrl),
158
+ };
159
+ } catch {
160
+ return { operatorKey: null, baseUrl: null };
161
+ }
162
+ }
163
+
164
+ function resolveHostedQueueConfig(env = process.env, operatorConfig = loadOperatorConfig()) {
165
+ const runtime = resolveHostedBillingConfig();
166
+ return {
167
+ apiBaseUrl: normalizeText(env.THUMBGATE_BILLING_API_BASE_URL)
168
+ || normalizeText(operatorConfig.baseUrl)
169
+ || runtime.billingApiBaseUrl,
170
+ apiKey: normalizeText(env.THUMBGATE_OPERATOR_KEY)
171
+ || normalizeText(operatorConfig.operatorKey)
172
+ || normalizeText(env.THUMBGATE_API_KEY),
173
+ };
174
+ }
175
+
176
+ function validateHostedConfig(config = {}) {
177
+ const apiBaseUrl = String(config.apiBaseUrl || '').trim();
178
+ const apiKey = String(config.apiKey || '').trim();
179
+ if (!apiBaseUrl || !apiKey) {
180
+ throw queueError(
181
+ 'operator_config_missing',
182
+ 'Hosted intake queue is not configured. Set THUMBGATE_OPERATOR_KEY or configure the local ThumbGate operator file.'
183
+ );
184
+ }
185
+ let parsed;
186
+ try {
187
+ parsed = new URL(apiBaseUrl);
188
+ } catch {
189
+ throw queueError('operator_config_invalid', 'Hosted intake queue base URL is invalid.');
190
+ }
191
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
192
+ throw queueError(
193
+ 'operator_config_invalid',
194
+ 'Hosted intake queue base URL must not contain credentials, query parameters, or fragments.'
195
+ );
196
+ }
197
+ const localHttp = parsed.protocol === 'http:' && ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
198
+ if (parsed.protocol !== 'https:' && !localHttp) {
199
+ throw queueError('operator_config_invalid', 'Hosted intake queue requires HTTPS outside localhost.');
200
+ }
201
+ return { apiBaseUrl: parsed.toString(), apiKey };
202
+ }
203
+
204
+ async function fetchWithTimeout(fetchImpl, url, options, timeoutMs) {
205
+ const controller = new AbortController();
206
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
207
+ try {
208
+ return await fetchImpl(url, { ...options, signal: controller.signal });
209
+ } catch (error) {
210
+ if (error && error.name === 'AbortError') {
211
+ throw queueError('intake_queue_timeout', `Hosted intake queue timed out after ${timeoutMs}ms.`);
212
+ }
213
+ throw queueError('intake_queue_unreachable', 'Hosted intake queue could not be reached.');
214
+ } finally {
215
+ clearTimeout(timer);
216
+ }
217
+ }
218
+
219
+ function assertPrivateResponse(response) {
220
+ const cacheControl = String(response.headers?.get?.('cache-control') || '').toLowerCase();
221
+ const vary = String(response.headers?.get?.('vary') || '').toLowerCase();
222
+ if (!cacheControl.includes('private') || !cacheControl.includes('no-store') ||
223
+ !vary.split(',').map((value) => value.trim()).includes('authorization')) {
224
+ throw queueError(
225
+ 'unsafe_cache_policy',
226
+ 'Hosted intake queue response is missing the required private, no-store, Vary: Authorization cache policy.'
227
+ );
228
+ }
229
+ }
230
+
231
+ function validateQueuePayload(payload) {
232
+ const counters = payload && typeof payload === 'object' ? [
233
+ payload.total,
234
+ payload.eligibleTotal,
235
+ payload.returned,
236
+ payload.approvalReadyTotal,
237
+ payload.discoveryReadyTotal,
238
+ ] : [];
239
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.leads) ||
240
+ counters.some((value) => !Number.isSafeInteger(value) || value < 0) ||
241
+ payload.eligibleTotal > payload.total || payload.returned !== payload.leads.length ||
242
+ payload.returned > payload.eligibleTotal ||
243
+ payload.approvalReadyTotal + payload.discoveryReadyTotal > payload.eligibleTotal) {
244
+ throw queueError('invalid_queue_payload', 'Hosted intake queue returned an invalid payload.');
245
+ }
246
+ return payload;
247
+ }
248
+
249
+ function leadRef(leadId) {
250
+ return crypto.createHash('sha256').update(String(leadId || '')).digest('hex').slice(0, 12);
251
+ }
252
+
253
+ function allowlistedText(value, allowed) {
254
+ const normalized = String(value || '').trim();
255
+ return allowed.has(normalized) ? normalized : null;
256
+ }
257
+
258
+ function allowlistedList(values, allowed) {
259
+ if (!Array.isArray(values)) return [];
260
+ return [...new Set(values.map((value) => allowlistedText(value, allowed)).filter(Boolean))];
261
+ }
262
+
263
+ function safeTimestamp(value) {
264
+ const parsed = new Date(String(value || ''));
265
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
266
+ }
267
+
268
+ function summarizeLead(lead = {}) {
269
+ const card = lead.qualificationCard || {};
270
+ const discoveryPacket = lead.discoveryPacket || {};
271
+ const closePacket = lead.closePacket || {};
272
+ const closeOfferId = allowlistedText(closePacket.offer?.offerId, ALLOWED_OFFER_IDS);
273
+ const recommendedOfferId = allowlistedText(card.recommendedOffer?.offerId, ALLOWED_OFFER_IDS);
274
+ const closePriceCents = Number.isSafeInteger(closePacket.offer?.priceCents) && closePacket.offer.priceCents >= 0
275
+ ? closePacket.offer.priceCents
276
+ : null;
277
+ const recommendedPriceCents = Number.isSafeInteger(card.recommendedOffer?.priceCents) && card.recommendedOffer.priceCents >= 0
278
+ ? card.recommendedOffer.priceCents
279
+ : null;
280
+ return {
281
+ ref: leadRef(lead.leadId),
282
+ submittedAt: safeTimestamp(lead.submittedAt),
283
+ updatedAt: safeTimestamp(lead.updatedAt),
284
+ lifecycleStatus: allowlistedText(lead.status, ALLOWED_STATUSES),
285
+ priorityRank: Number.isInteger(lead.priorityRank) ? lead.priorityRank : null,
286
+ priorityScore: Number.isFinite(card.priorityScore) ? card.priorityScore : null,
287
+ priorityBand: allowlistedText(card.priorityBand, ALLOWED_PRIORITY_BANDS),
288
+ fitBand: allowlistedText(card.fitBand, ALLOWED_FIT_BANDS),
289
+ route: allowlistedText(card.route, ALLOWED_ROUTES),
290
+ unknowns: allowlistedList(card.unknowns, ALLOWED_UNKNOWNS),
291
+ nextOperatorStep: allowlistedText(lead.nextOperatorStep, ALLOWED_OPERATOR_STEPS),
292
+ discoveryStatus: allowlistedText(discoveryPacket.status, ALLOWED_DISCOVERY_STATUSES),
293
+ discoveryBlockers: allowlistedList(discoveryPacket.blockers, ALLOWED_DISCOVERY_BLOCKERS),
294
+ closeStatus: allowlistedText(closePacket.status, ALLOWED_CLOSE_STATUSES),
295
+ closeBlockers: allowlistedList(closePacket.blockers, ALLOWED_CLOSE_BLOCKERS),
296
+ offerId: closeOfferId || recommendedOfferId,
297
+ priceCents: closePriceCents ?? recommendedPriceCents,
298
+ };
299
+ }
300
+
301
+ function summarizeQueue(payload, apiBaseUrl) {
302
+ const byStatus = {};
303
+ for (const status of ALLOWED_STATUSES) {
304
+ if (status === 'all') continue;
305
+ const count = payload.byStatus?.[status];
306
+ if (Number.isSafeInteger(count) && count >= 0) byStatus[status] = count;
307
+ }
308
+ return {
309
+ generatedAt: safeTimestamp(payload.generatedAt),
310
+ source: 'hosted_operator_intake_queue',
311
+ apiOrigin: new URL(apiBaseUrl).origin,
312
+ queueAvailable: true,
313
+ total: payload.total,
314
+ eligibleTotal: payload.eligibleTotal,
315
+ returned: Number.isInteger(payload.returned) ? payload.returned : payload.leads.length,
316
+ approvalReadyTotal: payload.approvalReadyTotal,
317
+ discoveryReadyTotal: payload.discoveryReadyTotal,
318
+ primaryApprovalActionAvailable: Boolean(payload.primaryApprovalAction),
319
+ primaryDiscoveryActionAvailable: Boolean(payload.primaryDiscoveryAction),
320
+ byStatus,
321
+ filters: {
322
+ statuses: allowlistedList(payload.filters?.statuses, ALLOWED_STATUSES),
323
+ limit: Number.isInteger(payload.filters?.limit) ? payload.filters.limit : null,
324
+ },
325
+ latestSubmittedAt: safeTimestamp(payload.latestSubmittedAt),
326
+ oldestSubmittedAt: safeTimestamp(payload.oldestSubmittedAt),
327
+ leads: payload.leads.map(summarizeLead),
328
+ privacy: {
329
+ contactDetailsIncluded: false,
330
+ draftsIncluded: false,
331
+ approvalTokensIncluded: false,
332
+ rawLeadIdsIncluded: false,
333
+ },
334
+ externalActionAuthorized: false,
335
+ revenueRecognized: false,
336
+ };
337
+ }
338
+
339
+ function exportPrivateQueue(payload, outputPath) {
340
+ const parent = path.dirname(outputPath);
341
+ const realParent = fs.realpathSync(parent);
342
+ const safePath = path.join(realParent, path.basename(outputPath));
343
+ const serialized = `${JSON.stringify({
344
+ exportedAt: new Date().toISOString(),
345
+ sensitivity: 'private_buyer_intake_do_not_commit_or_share',
346
+ externalActionAuthorized: false,
347
+ revenueRecognized: false,
348
+ queue: payload,
349
+ }, null, 2)}\n`;
350
+ fs.writeFileSync(safePath, serialized, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
351
+ fs.chmodSync(safePath, 0o600);
352
+ const stat = fs.statSync(safePath);
353
+ return {
354
+ path: safePath,
355
+ bytes: stat.size,
356
+ mode: (stat.mode & 0o777).toString(8).padStart(3, '0'),
357
+ sha256: crypto.createHash('sha256').update(serialized).digest('hex'),
358
+ };
359
+ }
360
+
361
+ async function fetchWorkflowIntakeQueue(options = {}, config = resolveHostedQueueConfig(), fetchImpl = fetch) {
362
+ const effectiveOptions = {
363
+ statuses: Array.isArray(options.statuses) ? options.statuses : ['new', 'qualified'],
364
+ limit: Number.isInteger(options.limit) ? options.limit : DEFAULT_LIMIT,
365
+ timeoutMs: Number.isInteger(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS,
366
+ };
367
+ const hosted = validateHostedConfig(config);
368
+ const url = new URL('/v1/intake/workflow-sprint/queue', hosted.apiBaseUrl);
369
+ url.searchParams.set('status', effectiveOptions.statuses.join(','));
370
+ url.searchParams.set('limit', String(effectiveOptions.limit));
371
+ const response = await fetchWithTimeout(fetchImpl, url, {
372
+ method: 'GET',
373
+ headers: {
374
+ authorization: `Bearer ${hosted.apiKey}`,
375
+ accept: 'application/json',
376
+ },
377
+ }, effectiveOptions.timeoutMs);
378
+
379
+ if (response.status === 404) {
380
+ throw queueError(
381
+ 'release_required',
382
+ 'Production does not expose the authenticated intake queue yet; deploy the reviewed revenue-evidence candidate first.',
383
+ 404
384
+ );
385
+ }
386
+ if (response.status === 401 || response.status === 403) {
387
+ const authProbeUrl = new URL('/v1/billing/summary?window=today', hosted.apiBaseUrl);
388
+ let authProbe = null;
389
+ try {
390
+ authProbe = await fetchWithTimeout(fetchImpl, authProbeUrl, {
391
+ method: 'GET',
392
+ headers: {
393
+ authorization: `Bearer ${hosted.apiKey}`,
394
+ accept: 'application/json',
395
+ },
396
+ }, effectiveOptions.timeoutMs);
397
+ } catch {
398
+ // Keep the original queue authorization result authoritative when the
399
+ // compatibility probe is unavailable.
400
+ }
401
+ if (authProbe?.ok) {
402
+ throw queueError(
403
+ 'release_required',
404
+ 'Production accepts the operator key but does not enable the authenticated intake queue yet; deploy the reviewed revenue-evidence candidate first.',
405
+ response.status
406
+ );
407
+ }
408
+ throw queueError('operator_credentials_rejected', 'Hosted intake queue rejected operator credentials.', response.status);
409
+ }
410
+ if (!response.ok) {
411
+ throw queueError('intake_queue_http_error', `Hosted intake queue returned HTTP ${response.status}.`, response.status);
412
+ }
413
+ assertPrivateResponse(response);
414
+ const payload = validateQueuePayload(await response.json());
415
+ return { payload, summary: summarizeQueue(payload, hosted.apiBaseUrl) };
416
+ }
417
+
418
+ function formatSummary(summary, privateExport = null) {
419
+ const lines = [
420
+ 'ThumbGate hosted intake close queue',
421
+ `Source: ${summary.source}`,
422
+ `Total: ${summary.total} | eligible: ${summary.eligibleTotal} | returned: ${summary.returned}`,
423
+ `Approval-ready: ${summary.approvalReadyTotal} (not authorized to send)`,
424
+ `Discovery-ready: ${summary.discoveryReadyTotal} (not authorized to send)`,
425
+ `Status: ${Object.entries(summary.byStatus).map(([key, value]) => `${key}=${value}`).join(', ') || 'none'}`,
426
+ `Latest: ${summary.latestSubmittedAt || 'none'}`,
427
+ 'Privacy: buyer details, drafts, approval tokens, and raw lead IDs are withheld from terminal output.',
428
+ 'Revenue recognized: no',
429
+ ];
430
+ if (privateExport) {
431
+ lines.push(`Private export: ${privateExport.path}`);
432
+ lines.push(`Private export proof: bytes=${privateExport.bytes} mode=${privateExport.mode} sha256=${privateExport.sha256}`);
433
+ }
434
+ return `${lines.join('\n')}\n`;
435
+ }
436
+
437
+ async function main(argv = process.argv.slice(2)) {
438
+ const options = parseArgs(argv);
439
+ const { payload, summary } = await fetchWorkflowIntakeQueue(options);
440
+ const privateExport = options.exportPrivatePath
441
+ ? exportPrivateQueue(payload, options.exportPrivatePath)
442
+ : null;
443
+ if (options.json) {
444
+ process.stdout.write(`${JSON.stringify({ ...summary, privateExport }, null, 2)}\n`);
445
+ return;
446
+ }
447
+ process.stdout.write(formatSummary(summary, privateExport));
448
+ }
449
+
450
+ module.exports = {
451
+ ALLOWED_STATUSES,
452
+ ALLOWED_CLOSE_BLOCKERS,
453
+ ALLOWED_DISCOVERY_BLOCKERS,
454
+ ALLOWED_OFFER_IDS,
455
+ ALLOWED_UNKNOWNS,
456
+ DEFAULT_LIMIT,
457
+ DEFAULT_TIMEOUT_MS,
458
+ MAX_LIMIT,
459
+ OPERATOR_CONFIG_PATH,
460
+ parseArgs,
461
+ loadOperatorConfig,
462
+ resolveHostedQueueConfig,
463
+ validateHostedConfig,
464
+ assertPrivateResponse,
465
+ validateQueuePayload,
466
+ leadRef,
467
+ allowlistedText,
468
+ allowlistedList,
469
+ safeTimestamp,
470
+ summarizeLead,
471
+ summarizeQueue,
472
+ exportPrivateQueue,
473
+ fetchWorkflowIntakeQueue,
474
+ formatSummary,
475
+ main,
476
+ };
477
+
478
+ if (require.main === module) { // NOSONAR
479
+ main().catch((error) => {
480
+ process.stderr.write(`${error.code || 'intake_queue_error'}: ${error.message}\n`);
481
+ process.exit(1);
482
+ });
483
+ }