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,102 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_APP_ORIGIN = 'https://thumbgate.ai';
4
+
5
+ function normalizeAppOrigin(value = DEFAULT_APP_ORIGIN) {
6
+ const parsed = new URL(String(value || DEFAULT_APP_ORIGIN));
7
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
8
+ throw new Error('Buyer-path app origin must use http or https.');
9
+ }
10
+ parsed.pathname = '/';
11
+ parsed.search = '';
12
+ parsed.hash = '';
13
+ return parsed.origin;
14
+ }
15
+
16
+ function buildBuyerUrl(pathname, params = {}, appOrigin = DEFAULT_APP_ORIGIN) {
17
+ const normalizedOrigin = normalizeAppOrigin(appOrigin);
18
+ const url = new URL(pathname, `${normalizedOrigin}/`);
19
+ if (url.origin !== normalizedOrigin) {
20
+ throw new Error('Buyer URL must remain on the configured first-party origin.');
21
+ }
22
+ for (const [key, value] of Object.entries(params || {})) {
23
+ if (value === undefined || value === null || String(value).trim() === '') continue;
24
+ url.searchParams.set(key, String(value).trim());
25
+ }
26
+ return url.toString();
27
+ }
28
+
29
+ function buildProBuyerUrl({
30
+ billingCycle = 'monthly',
31
+ appOrigin = DEFAULT_APP_ORIGIN,
32
+ source = null,
33
+ medium = null,
34
+ campaign = null,
35
+ content = null,
36
+ } = {}) {
37
+ return buildBuyerUrl('/go/pro', {
38
+ plan_id: 'pro',
39
+ billing_cycle: billingCycle === 'annual' ? 'annual' : 'monthly',
40
+ utm_source: source,
41
+ utm_medium: medium,
42
+ utm_campaign: campaign,
43
+ utm_content: content,
44
+ }, appOrigin);
45
+ }
46
+
47
+ function buildDiagnosticBuyerUrl({
48
+ appOrigin = DEFAULT_APP_ORIGIN,
49
+ source = null,
50
+ medium = null,
51
+ campaign = null,
52
+ content = null,
53
+ } = {}) {
54
+ return buildBuyerUrl('/diagnostic', {
55
+ plan_id: 'sprint_diagnostic',
56
+ utm_source: source,
57
+ utm_medium: medium,
58
+ utm_campaign: campaign,
59
+ utm_content: content,
60
+ }, appOrigin);
61
+ }
62
+
63
+ function buildSprintBuyerUrl({
64
+ appOrigin = DEFAULT_APP_ORIGIN,
65
+ source = null,
66
+ medium = null,
67
+ campaign = null,
68
+ content = null,
69
+ } = {}) {
70
+ return buildBuyerUrl('/go/sprint', {
71
+ plan_id: 'workflow_sprint',
72
+ utm_source: source,
73
+ utm_medium: medium,
74
+ utm_campaign: campaign,
75
+ utm_content: content,
76
+ }, appOrigin);
77
+ }
78
+
79
+ function buildEnterpriseBuyerUrl({ appOrigin = DEFAULT_APP_ORIGIN } = {}) {
80
+ const url = new URL('/', `${normalizeAppOrigin(appOrigin)}/`);
81
+ url.hash = 'workflow-sprint-intake';
82
+ return url.toString();
83
+ }
84
+
85
+ function isFirstPartyBuyerUrl(value, appOrigin = DEFAULT_APP_ORIGIN) {
86
+ try {
87
+ return new URL(value).origin === normalizeAppOrigin(appOrigin);
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ module.exports = {
94
+ DEFAULT_APP_ORIGIN,
95
+ buildBuyerUrl,
96
+ buildDiagnosticBuyerUrl,
97
+ buildEnterpriseBuyerUrl,
98
+ buildProBuyerUrl,
99
+ buildSprintBuyerUrl,
100
+ isFirstPartyBuyerUrl,
101
+ normalizeAppOrigin,
102
+ };
@@ -98,13 +98,13 @@ function formatCliOutput(result) {
98
98
  const isDown = ['down', 'negative', 'thumbs_down'].includes(feedbackSignal);
99
99
 
100
100
  // Header
101
- if (feedbackResult?.duplicate) {
101
+ if (feedbackResult?.duplicate && feedbackResult.accepted === true) {
102
102
  lines.push(`${Y}${BD}Feedback already captured${RST}`);
103
103
  const feedbackId = feedbackResult.feedbackEvent?.id;
104
104
  const memoryId = feedbackResult.memoryRecord?.id;
105
105
  if (feedbackId) lines.push(`${D} Feedback ID: ${feedbackId}${RST}`);
106
106
  if (memoryId) lines.push(`${D} Memory ID : ${memoryId}${RST}`);
107
- } else if (feedbackResult && feedbackResult.accepted !== false) {
107
+ } else if (feedbackResult?.accepted === true) {
108
108
  lines.push(`${isDown ? R : G}${BD}${isDown ? '👎 Thumbs down recorded' : '👍 Thumbs up recorded'}${RST}`);
109
109
  const feedbackId = feedbackResult.feedbackEvent?.id || feedbackResult.id;
110
110
  const memoryId = feedbackResult.memoryRecord?.id || feedbackResult.memoryId;
@@ -1,7 +1,15 @@
1
1
  'use strict';
2
2
 
3
- const PRO_MONTHLY_PAYMENT_LINK = 'https://thumbgate.ai/go/pro?utm_source=offer';
4
- const PRO_ANNUAL_PAYMENT_LINK = 'https://buy.stripe.com/3cI8wPfCYaPs2dzdKz3sI07';
3
+ const { buildProBuyerUrl } = require('./buyer-paths');
4
+
5
+ const PRO_MONTHLY_PAYMENT_LINK = buildProBuyerUrl({
6
+ billingCycle: 'monthly',
7
+ source: 'offer',
8
+ });
9
+ const PRO_ANNUAL_PAYMENT_LINK = buildProBuyerUrl({
10
+ billingCycle: 'annual',
11
+ source: 'offer',
12
+ });
5
13
 
6
14
  const PRO_MONTHLY_PRICE_ID = 'price_1THQY7GGBpd520QYHoS7RG0J';
7
15
  const PRO_ANNUAL_PRICE_ID = 'price_1THQZ7GGBpd520QYxzDRnxhB';
@@ -14,12 +22,11 @@ const TEAM_ANNUAL_PRICE_DOLLARS = 588;
14
22
  const TEAM_MIN_SEATS = 3;
15
23
 
16
24
  const PRO_PRICE_LABEL = '$19/mo or $149/yr (individual)';
17
- // Enterprise is the contact-sales tier (absorbs the former Team workflow + the
18
- // regulated-industry lane). Pricing is scoped after intake — no self-serve seat
19
- // price is surfaced. The dormant TEAM_* Stripe constants below remain only as
20
- // inert billing plumbing pending a dedicated cleanup; they are no longer a
21
- // customer-facing tier.
22
- const ENTERPRISE_PRICE_LABEL = 'Custom pricing, scoped after intake — Enterprise agent governance';
25
+ // The Enterprise Workflow Gate is the one promoted public service offer. The
26
+ // dormant TEAM_* Stripe constants below remain only as inert billing plumbing
27
+ // pending a dedicated cleanup; they are no longer a customer-facing tier.
28
+ const ENTERPRISE_PRICE_LABEL = 'Enterprise Workflow Gate — $499 one-time for one supported workflow';
29
+ const ENTERPRISE_ENTRY_URL = 'https://thumbgate.ai/pricing';
23
30
 
24
31
  function normalizePlanId(value) {
25
32
  const text = String(value || '').trim().toLowerCase();
@@ -74,7 +81,7 @@ function buildCaptureReceipt({ signal, feedbackId, memoryId, actionType } = {})
74
81
  ` Solo Pro : ${PRO_PRICE_LABEL} for recall, dashboard, adapters, and exports`,
75
82
  ` Upgrade : ${trackedProUrl('cli_capture_receipt', actionType || normalizedSignal.toLowerCase())}`,
76
83
  ` Enterprise : ${ENTERPRISE_PRICE_LABEL}; start with one repeated workflow failure`,
77
- ' https://thumbgate.ai/#workflow-sprint-intake',
84
+ ` ${ENTERPRISE_ENTRY_URL}`,
78
85
  '',
79
86
  ];
80
87
  return lines.join('\n');
@@ -107,7 +114,7 @@ function buildStatsReceipt(stats = {}) {
107
114
  lines.push(' Show the buyer : npx thumbgate cost');
108
115
  lines.push(' Pro recall value : keep lessons/rules searchable, exportable, and visible');
109
116
  lines.push(` Solo Pro : ${trackedProUrl('cli_stats_receipt', 'proof_seen')}`);
110
- lines.push(' Enterprise : https://thumbgate.ai/#workflow-sprint-intake');
117
+ lines.push(` Enterprise : ${ENTERPRISE_ENTRY_URL}`);
111
118
  lines.push('');
112
119
  return lines.join('\n');
113
120
  }
@@ -125,6 +132,7 @@ module.exports = {
125
132
  TEAM_MIN_SEATS,
126
133
  PRO_PRICE_LABEL,
127
134
  ENTERPRISE_PRICE_LABEL,
135
+ ENTERPRISE_ENTRY_URL,
128
136
  normalizePlanId,
129
137
  normalizeBillingCycle,
130
138
  normalizeSeatCount,
@@ -107,7 +107,34 @@ function errMessage(err) {
107
107
  return err?.message ?? err;
108
108
  }
109
109
 
110
- function handleStepError({ err, attempt, retries, classify, backoffMs, name, onRetry, onFail, logger }) {
110
+ function retryAfterMs(err) {
111
+ const raw = err?.retryAfterMs
112
+ ?? err?.headers?.['retry-after-ms']
113
+ ?? err?.headers?.['retry-after'];
114
+ if (raw === undefined || raw === null || raw === '') return null;
115
+ const numeric = Number(raw);
116
+ if (Number.isFinite(numeric)) {
117
+ return String(raw).includes('.') || err?.headers?.['retry-after'] !== undefined
118
+ ? Math.max(0, numeric * 1000)
119
+ : Math.max(0, numeric);
120
+ }
121
+ const dateMs = Date.parse(raw);
122
+ return Number.isNaN(dateMs) ? null : Math.max(0, dateMs - Date.now());
123
+ }
124
+
125
+ function handleStepError({
126
+ err,
127
+ attempt,
128
+ retries,
129
+ classify,
130
+ backoffMs,
131
+ name,
132
+ onRetry,
133
+ onFail,
134
+ logger,
135
+ jitterRatio,
136
+ randomFn,
137
+ }) {
111
138
  const verdict = classify(err);
112
139
  const terminal = verdict === 'fail' || attempt >= retries;
113
140
  if (terminal) {
@@ -117,7 +144,11 @@ function handleStepError({ err, attempt, retries, classify, backoffMs, name, onR
117
144
  }
118
145
  return { terminal: true };
119
146
  }
120
- const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)];
147
+ const configuredWait = backoffMs[Math.min(attempt, backoffMs.length - 1)];
148
+ const serverWait = retryAfterMs(err);
149
+ const baseWait = serverWait === null ? configuredWait : Math.max(configuredWait, serverWait);
150
+ const jitter = jitterRatio > 0 ? baseWait * jitterRatio * ((randomFn() * 2) - 1) : 0;
151
+ const waitMs = Math.max(0, Math.round(baseWait + jitter));
121
152
  if (typeof onRetry === 'function') onRetry({ name, attempt, err, waitMs, verdict });
122
153
  if (typeof logger === 'function') {
123
154
  logger(`[step:${name}] RETRY attempt=${attempt} waitMs=${waitMs} err=${errMessage(err)}`);
@@ -126,10 +157,9 @@ function handleStepError({ err, attempt, retries, classify, backoffMs, name, onR
126
157
  }
127
158
 
128
159
  async function runStep(name, options, fn) {
129
- if (typeof options === 'function') {
130
- fn = options;
131
- options = {};
132
- }
160
+ const invocation = normalizeStepInvocation(options, fn);
161
+ options = invocation.options;
162
+ fn = invocation.fn;
133
163
  const {
134
164
  retries = 3,
135
165
  backoffMs = DEFAULT_BACKOFF_MS,
@@ -139,33 +169,112 @@ async function runStep(name, options, fn) {
139
169
  onFail,
140
170
  logger,
141
171
  sleepFn = sleep,
172
+ sideEffect = false,
173
+ idempotencyKey: stepIdempotencyKey,
174
+ maxElapsedMs = Infinity,
175
+ jitterRatio = 0,
176
+ randomFn = Math.random,
142
177
  } = options || {};
143
178
 
144
- if (typeof fn !== 'function') {
145
- throw new TypeError(`runStep(${name}): fn must be a function`);
146
- }
179
+ validateStepConfiguration(name, fn, {
180
+ sideEffect,
181
+ stepIdempotencyKey,
182
+ maxElapsedMs,
183
+ });
147
184
 
148
185
  let lastErr;
186
+ const startedAt = Date.now();
149
187
  for (let attempt = 0; attempt <= retries; attempt++) {
150
- if (typeof onAttempt === 'function') onAttempt({ name, attempt });
188
+ const context = {
189
+ name,
190
+ attempt,
191
+ idempotencyKey: stepIdempotencyKey || null,
192
+ elapsedMs: Date.now() - startedAt,
193
+ };
194
+ callIfFunction(onAttempt, context);
151
195
  try {
152
- return await fn({ attempt });
196
+ return await fn(context);
153
197
  } catch (err) {
154
198
  lastErr = err;
155
199
  const outcome = handleStepError({
156
- err, attempt, retries, classify, backoffMs, name, onRetry, onFail, logger,
200
+ err,
201
+ attempt,
202
+ retries,
203
+ classify,
204
+ backoffMs,
205
+ name,
206
+ onRetry,
207
+ onFail,
208
+ logger,
209
+ jitterRatio: Math.max(0, Number(jitterRatio) || 0),
210
+ randomFn,
157
211
  });
158
212
  if (outcome.terminal) throw err;
213
+ enforceRetryBudget({
214
+ err,
215
+ attempt,
216
+ name,
217
+ onFail,
218
+ startedAt,
219
+ waitMs: outcome.waitMs,
220
+ maxElapsedMs,
221
+ });
159
222
  await sleepFn(outcome.waitMs);
160
223
  }
161
224
  }
162
225
  throw lastErr;
163
226
  }
164
227
 
228
+ function normalizeStepInvocation(options, fn) {
229
+ if (typeof options === 'function') return { options: {}, fn: options };
230
+ return { options: options || {}, fn };
231
+ }
232
+
233
+ function validateStepConfiguration(name, fn, options) {
234
+ if (typeof fn !== 'function') {
235
+ throw new TypeError(`runStep(${name}): fn must be a function`);
236
+ }
237
+ if (options.sideEffect && !String(options.stepIdempotencyKey || '').trim()) {
238
+ const error = new Error(`runStep(${name}): side-effecting steps require idempotencyKey`);
239
+ error.code = 'THUMBGATE_IDEMPOTENCY_KEY_REQUIRED';
240
+ error.nonRetryable = true;
241
+ throw error;
242
+ }
243
+ if (!Number.isFinite(Number(options.maxElapsedMs)) && options.maxElapsedMs !== Infinity) {
244
+ throw new TypeError(`runStep(${name}): maxElapsedMs must be finite or Infinity`);
245
+ }
246
+ }
247
+
248
+ function callIfFunction(callback, payload) {
249
+ if (typeof callback === 'function') callback(payload);
250
+ }
251
+
252
+ function enforceRetryBudget({
253
+ err,
254
+ attempt,
255
+ name,
256
+ onFail,
257
+ startedAt,
258
+ waitMs,
259
+ maxElapsedMs,
260
+ }) {
261
+ if (Date.now() - startedAt + waitMs <= maxElapsedMs) return;
262
+ err.code = err.code || 'THUMBGATE_RETRY_BUDGET_EXHAUSTED';
263
+ err.retryBudgetExhausted = true;
264
+ callIfFunction(onFail, {
265
+ name,
266
+ attempt,
267
+ err,
268
+ verdict: 'retry_budget_exhausted',
269
+ });
270
+ throw err;
271
+ }
272
+
165
273
  module.exports = {
166
274
  runStep,
167
275
  idempotencyKey,
168
276
  defaultClassify,
169
277
  TRANSIENT_CODES,
170
278
  DEFAULT_BACKOFF_MS,
279
+ retryAfterMs,
171
280
  };