thumbgate 1.27.16 → 1.27.18

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -215
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +98 -228
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -123
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -331,55 +331,6 @@ function buildRewardReport(episodes = [], options = {}) {
331
331
  deep: 'destructive, public, or production-adjacent work',
332
332
  xhigh: 'payments, secrets, deploy-prod, data-loss, force-push-main',
333
333
  },
334
- continualAdapterPlan: buildContinualAdapterTrainingPlan(episodes, options),
335
- };
336
- }
337
-
338
- function buildContinualAdapterTrainingPlan(episodes = [], options = {}) {
339
- const maxAdapters = Math.max(1, Number(options.maxAdapters || 4));
340
- const minExamples = Math.max(1, Number(options.minExamples || 2));
341
- const pairs = buildPreferencePairs(episodes, { ...options, maxPairs: Number(options.maxPairs || 24) });
342
- const candidates = rankGateCandidatesByReward(episodes, {
343
- ...options,
344
- minOccurrences: minExamples,
345
- maxGateCandidates: maxAdapters,
346
- }).slice(0, maxAdapters);
347
-
348
- const adapters = candidates.map((candidate, index) => ({
349
- id: `lora_${candidate.gateId || `adapter_${index + 1}`}`.slice(0, 96),
350
- source: candidate.key,
351
- trainingMode: 'concurrent-lora',
352
- examples: candidate.occurrences,
353
- priorityScore: candidate.priorityScore,
354
- targetBehavior: candidate.recommendation,
355
- retentionChecks: [
356
- 'baseline gate pass-rate must not regress',
357
- 'previous adapter reward must stay within tolerance',
358
- 'new adapter must improve blocked-repeat or verification reward on held-out episodes',
359
- ],
360
- }));
361
-
362
- const enoughData = pairs.length >= minExamples && adapters.length > 0;
363
- return {
364
- status: enoughData ? 'ready' : 'needs_more_feedback',
365
- trainingStack: 'multi-lora-continual-learning',
366
- baseModelPolicy: 'freeze base model; train small adapters from reward-ranked DPO pairs',
367
- scheduling: adapters.length > 1 ? 'batch adapters concurrently when they share the same frozen base' : 'train serially until at least two adapter candidates exist',
368
- adapters,
369
- dpoPairsAvailable: pairs.length,
370
- retentionGate: 'ship no adapter unless reward, regression, and prior-domain retention checks pass',
371
- nextActions: enoughData
372
- ? [
373
- 'Export reward-ranked DPO pairs.',
374
- 'Train candidate LoRA adapters concurrently on the same frozen base.',
375
- 'Evaluate each adapter against held-out negative episodes and prior positive episodes.',
376
- 'Promote only the adapter that improves reward without retention regression.',
377
- ]
378
- : [
379
- 'Capture more thumbs-up/down outcomes before training.',
380
- 'Promote deterministic gates first when examples are sparse.',
381
- 'Use local semantic recall until enough reward-ranked pairs exist for adapter training.',
382
- ],
383
334
  };
384
335
  }
385
336
 
@@ -468,10 +419,8 @@ if (isCliInvocation()) {
468
419
  console.log(JSON.stringify(buildPreferencePairs(episodes), null, 2));
469
420
  } else if (args.command === 'gates') {
470
421
  console.log(JSON.stringify(rankGateCandidatesByReward(episodes), null, 2));
471
- } else if (args.command === 'adapters') {
472
- console.log(JSON.stringify(buildContinualAdapterTrainingPlan(episodes), null, 2));
473
422
  } else {
474
- console.error(`Unknown command: ${args.command}. Use: report, pairs, gates, adapters`);
423
+ console.error(`Unknown command: ${args.command}. Use: report, pairs, gates`);
475
424
  process.exit(1);
476
425
  }
477
426
  }
@@ -479,7 +428,6 @@ if (isCliInvocation()) {
479
428
  module.exports = {
480
429
  HIGH_RISK_TAGS,
481
430
  allocateTestTimeCompute,
482
- buildContinualAdapterTrainingPlan,
483
431
  buildPreferencePairFromEpisodes,
484
432
  buildPreferencePairs,
485
433
  buildRewardReport,
@@ -58,47 +58,6 @@ function readJSONL(filePath) {
58
58
  }).filter(Boolean);
59
59
  }
60
60
 
61
- // --- Self-Harness stage 3: regression-gated promotion -----------------------
62
- // Inspired by "Self-Harness: Harnesses That Improve Themselves" (arXiv 2606.09498).
63
- // Stages 1-2 (weakness mining -> rule extraction) already exist via lesson
64
- // inference + this promoter. Stage 3 — accept a harness change only after
65
- // regression-testing it does not degrade behavior — was missing: a noisy 3x
66
- // capture could hard-block an over-broad pattern with no check that it wouldn't
67
- // have wrongly blocked actions that were previously ALLOWED. This replays a
68
- // candidate BLOCK rule against the audit trail's prior `allow` decisions; if it
69
- // would have blocked safe actions, the caller quarantines it to `warn` instead.
70
- const REGRESSION_FALSE_BLOCK_LIMIT = 0; // any prior safe action it would block => quarantine
71
-
72
- function getAuditTrailPath() {
73
- return path.join(path.dirname(getFeedbackLogPath()), 'audit-trail.jsonl');
74
- }
75
-
76
- // Returns { falseBlocks, allowSampleSize } or null when there is no history /
77
- // matcher available — in which case the caller promotes as usual (fail-open to
78
- // existing behavior, since regression gating is an enhancement, not a hard gate).
79
- function regressionCheck(gate, options = {}) {
80
- const auditPath = options.auditTrailPath || getAuditTrailPath();
81
- const entries = readJSONL(auditPath);
82
- if (!entries.length) return null;
83
- // Lazy-require to avoid the gates-engine <-> auto-promote-gates require cycle.
84
- let matchesGate;
85
- try { ({ matchesGate } = require('./gates-engine')); } catch { return null; }
86
- if (typeof matchesGate !== 'function') return null;
87
- const allowed = entries.filter((e) => e && e.decision === 'allow' && e.toolName);
88
- if (!allowed.length) return null;
89
- let falseBlocks = 0;
90
- for (const e of allowed) {
91
- try {
92
- if (matchesGate(gate, e.toolName, e.toolInput || {})) falseBlocks += 1;
93
- } catch { /* a bad pattern/entry never counts as a false block */ }
94
- }
95
- return { falseBlocks, allowSampleSize: allowed.length };
96
- }
97
-
98
- function safeRegressionCheck(gate, options) {
99
- try { return regressionCheck(gate, options); } catch { return null; }
100
- }
101
-
102
61
  function loadAutoGates() {
103
62
  const autoGatesPath = getAutoGatesPath();
104
63
  if (!fs.existsSync(autoGatesPath)) {
@@ -234,9 +193,9 @@ function patternToGateId(key) {
234
193
  return 'auto-' + key.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 50).toLowerCase();
235
194
  }
236
195
 
237
- function buildGateRule(group, actionOverride) {
238
- const action = actionOverride || (group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn'));
239
- const severity = action === 'block' ? 'critical' : action === 'approve' ? 'high' : 'medium';
196
+ function buildGateRule(group) {
197
+ const action = group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn');
198
+ const severity = action === 'block' ? 'critical' : 'medium';
240
199
  const context = group.latestContext.slice(0, 120);
241
200
  const kind = group.key.startsWith('diagnosis:')
242
201
  ? 'repeated diagnosis'
@@ -373,8 +332,7 @@ function forcePromote(context, action = 'block') {
373
332
  return { gateId, action, totalGates: data.gates.length };
374
333
  }
375
334
 
376
- function promote(feedbackLogPath, options) {
377
- const opts = options || {};
335
+ function promote(feedbackLogPath) {
378
336
  const logPath = feedbackLogPath || getFeedbackLogPath();
379
337
  const entries = readJSONL(logPath);
380
338
  const groups = groupNegativeFeedback(entries, WINDOW_DAYS);
@@ -399,38 +357,17 @@ function promote(feedbackLogPath, options) {
399
357
  const existing = data.gates[existingIdx];
400
358
  const newAction = group.count >= BLOCK_THRESHOLD ? 'block' : 'warn';
401
359
  if (existing.action !== newAction && newAction === 'block') {
402
- // Self-Harness stage 3: regression-test before upgrading warn -> block.
403
- const regression = opts.skipRegression ? null : safeRegressionCheck(buildGateRule(group, 'block'), opts);
404
- if (regression && regression.falseBlocks > REGRESSION_FALSE_BLOCK_LIMIT) {
405
- // Would block prior safe actions — hold at warn instead of upgrading.
406
- promotions.push({ type: 'upgrade-quarantined', gateId, from: existing.action, occurrences: group.count, falseBlocks: regression.falseBlocks });
407
- } else {
408
- // Upgrade from warn to block
409
- data.gates[existingIdx] = { ...existing, action: 'block', severity: 'critical', occurrences: group.count, upgradedAt: new Date().toISOString() };
410
- promotions.push({ type: 'upgrade', gateId, from: existing.action, to: 'block', occurrences: group.count });
411
- }
360
+ // Upgrade from warn to block
361
+ data.gates[existingIdx] = { ...existing, action: 'block', severity: 'critical', occurrences: group.count, upgradedAt: new Date().toISOString() };
362
+ promotions.push({ type: 'upgrade', gateId, from: existing.action, to: 'block', occurrences: group.count });
412
363
  }
413
364
  // Update occurrence count even if no action change
414
365
  data.gates[existingIdx].occurrences = group.count;
415
366
  continue;
416
367
  }
417
368
 
418
- // New gate — respect explicit gateAction override (e.g. 'approve' for human-approval rules)
419
- const gate = buildGateRule(group, opts.gateAction);
420
-
421
- // Self-Harness stage 3: before a feedback rule goes live as a hard block,
422
- // regression-test it against prior allowed actions. If it would have blocked
423
- // safe actions, quarantine it to `warn` instead of `block`.
424
- let regression = null;
425
- if (gate.action === 'block' && !opts.gateAction && !opts.skipRegression) {
426
- regression = safeRegressionCheck(gate, opts);
427
- if (regression && regression.falseBlocks > REGRESSION_FALSE_BLOCK_LIMIT) {
428
- gate.action = 'warn';
429
- gate.severity = 'medium';
430
- gate.quarantined = true;
431
- gate.regression = regression;
432
- }
433
- }
369
+ // New gate
370
+ const gate = buildGateRule(group);
434
371
 
435
372
  // Enforce max limit — rotate oldest
436
373
  if (data.gates.length >= MAX_AUTO_GATES) {
@@ -439,13 +376,7 @@ function promote(feedbackLogPath, options) {
439
376
  }
440
377
 
441
378
  data.gates.push(gate);
442
- promotions.push({
443
- type: gate.quarantined ? 'new-quarantined' : 'new',
444
- gateId: gate.id,
445
- action: gate.action,
446
- occurrences: group.count,
447
- ...(gate.quarantined ? { falseBlocks: regression.falseBlocks, allowSampleSize: regression.allowSampleSize } : {}),
448
- });
379
+ promotions.push({ type: 'new', gateId: gate.id, action: gate.action, occurrences: group.count });
449
380
  }
450
381
 
451
382
  // Log promotions
@@ -506,9 +437,6 @@ module.exports = {
506
437
  groupNegativeFeedback,
507
438
  patternToGateId,
508
439
  buildGateRule,
509
- regressionCheck,
510
- getAuditTrailPath,
511
- REGRESSION_FALSE_BLOCK_LIMIT,
512
440
  extractPatternKey,
513
441
  normalizeCommandSignature,
514
442
  isNegative,
@@ -27,7 +27,6 @@ const {
27
27
  statuslineCommand,
28
28
  userPromptHookCommand,
29
29
  } = require('./hook-runtime');
30
- const { installShim } = require('./install-shim');
31
30
 
32
31
  function getHome() {
33
32
  return process.env.HOME || process.env.USERPROFILE || '';
@@ -339,19 +338,6 @@ function wireClaudeHooks(options) {
339
338
  options.projectSettingsPath || claudeProjectSettingsPath(options.projectDir);
340
339
  const dryRun = options.dryRun || false;
341
340
  const projectDir = options.projectDir || process.cwd();
342
-
343
- // --- Install stable shim before resolving hook commands ---
344
- // The shim at ~/.thumbgate/bin/thumbgate-hook always resolves @latest,
345
- // so hooks never go stale across version bumps (Volta-style pattern).
346
- // Skip in source-checkout mode — developers use direct node commands.
347
- if (!dryRun && !require('./mcp-config').isSourceCheckout(path.join(__dirname, '..'))) {
348
- try {
349
- installShim();
350
- } catch {
351
- // Non-fatal: fall back to version-pinned commands
352
- }
353
- }
354
-
355
341
  const desiredStatusLine = statuslineCommand();
356
342
 
357
343
  // --- Step 0: clean up stale hooks from BOTH settings locations ---
@@ -51,7 +51,6 @@ const {
51
51
  } = require('./analytics-window');
52
52
  const { ensureParentDir } = require('./fs-utils');
53
53
  const mailer = require('./mailer');
54
- const { recordCheckoutFunnelEvent } = require('./plausible-server-events');
55
54
 
56
55
  function loadWorkflowSprintIntakeModule() {
57
56
  const modulePath = path.resolve(__dirname, 'workflow-sprint-intake.js');
@@ -139,10 +138,6 @@ const IS_TEST = !!(
139
138
  process.env.NODE_ENV === 'test'
140
139
  );
141
140
 
142
- function allowUnsignedStripeWebhooks() {
143
- return IS_TEST && process.env.THUMBGATE_ALLOW_UNSIGNED_STRIPE_WEBHOOKS === '1';
144
- }
145
-
146
141
  function shouldMergeLegacyBillingData() {
147
142
  return process.env._TEST_INCLUDE_LEGACY_BILLING_DATA === '1'
148
143
  || process.env.THUMBGATE_INCLUDE_LEGACY_BILLING_DATA === '1';
@@ -449,57 +444,6 @@ function buildCheckoutProductData({ name, description, appOrigin, planId }) {
449
444
  };
450
445
  }
451
446
 
452
- /**
453
- * Verify an ACTIVE Stripe product exists for the given plan name before
454
- * we let buildSubscriptionPriceData create inline price_data under it.
455
- *
456
- * Stripe matches product_data by `name`. If only an archived product
457
- * matches, new prices created via that path inherit active=false and the
458
- * generated checkout URL renders "Something went wrong / The page you
459
- * were looking for could not be found." for the buyer. Stripe Dashboard
460
- * shows the session as `open` with no email captured — looks like the
461
- * buyer abandoned, but they were never given a working page.
462
- *
463
- * Failing here surfaces the misconfiguration at the first checkout
464
- * attempt instead of silently breaking every buyer for days.
465
- *
466
- * Verified incident: ThumbGate#2188 (May 2026) — 20 sessions abandoned in
467
- * 7 days, all because the only product named "ThumbGate Pro" matching the
468
- * inline product_data was archived (prod_UXxOHAfbDsPyRb), while an active
469
- * product with the same name existed (prod_UW82THPxfNvwKT) that should
470
- * have been used instead.
471
- */
472
- async function verifyActiveProductForPlan(stripe, planId) {
473
- const expectedName = planId === 'team' ? 'ThumbGate Team' : 'ThumbGate Pro';
474
- let products;
475
- try {
476
- products = await stripe.products.list({ limit: 100 });
477
- } catch (err) {
478
- // Network/transient failures shouldn't block checkout creation.
479
- // The original session.create call will surface real Stripe errors.
480
- return;
481
- }
482
- const matching = (products && products.data ? products.data : [])
483
- .filter((p) => p && p.name === expectedName);
484
- if (matching.length === 0) {
485
- // No product with this name exists; Stripe will create a new one when
486
- // session.create fires with inline product_data. Safe path.
487
- return;
488
- }
489
- const active = matching.find((p) => p.active === true);
490
- if (!active) {
491
- const archived = matching[0];
492
- throw new Error(
493
- `Refusing to create checkout session: Stripe product named "${expectedName}" ` +
494
- `exists only in archived state (id=${archived.id}, active=false). New prices ` +
495
- `created via inline product_data would inherit active=false, rendering ` +
496
- `"page not found" on Stripe checkout for every buyer. Fix: reactivate the ` +
497
- `archived product in Stripe Dashboard, or rename the active product to ` +
498
- `match "${expectedName}". See ThumbGate#2188 for the May 2026 incident.`
499
- );
500
- }
501
- }
502
-
503
447
  function buildSubscriptionPriceData(checkoutSelection, appOrigin) {
504
448
  const isTeam = checkoutSelection.planId === 'team';
505
449
  const annual = checkoutSelection.billingCycle === 'annual';
@@ -2581,18 +2525,6 @@ async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, ins
2581
2525
  }
2582
2526
 
2583
2527
  const stripe = getStripeClient();
2584
-
2585
- // Defensive guard against ThumbGate#2188:
2586
- // When buildSubscriptionPriceData passes inline `product_data` to Stripe,
2587
- // Stripe name-matches existing products. If the only existing product with
2588
- // that name is ARCHIVED (active=false), the new price inherits active=false
2589
- // and every Stripe checkout page renders "page not found" for the buyer.
2590
- // That bug burnt 20+ silent abandoned sessions in May 2026. Fail fast
2591
- // instead of letting the broken page ship.
2592
- if (!packId) {
2593
- await verifyActiveProductForPlan(stripe, checkoutSelection.planId);
2594
- }
2595
-
2596
2528
  const sessionPayload = buildCheckoutSessionPayload({
2597
2529
  successUrl,
2598
2530
  cancelUrl,
@@ -2905,7 +2837,7 @@ function disableCustomerKeys(customerId) {
2905
2837
  }
2906
2838
 
2907
2839
  function verifyWebhookSignature(rawBody, signature) {
2908
- if (!CONFIG.STRIPE_WEBHOOK_SECRET) return allowUnsignedStripeWebhooks();
2840
+ if (!CONFIG.STRIPE_WEBHOOK_SECRET) return true;
2909
2841
  if (!signature || !rawBody) return false;
2910
2842
 
2911
2843
  // Stripe signature format: t=<timestamp>,v1=<hmac>,...
@@ -2935,13 +2867,6 @@ function verifyWebhookSignature(rawBody, signature) {
2935
2867
 
2936
2868
  async function handleWebhook(rawBody, signature) {
2937
2869
  if (LOCAL_MODE()) return { handled: false, reason: 'local_mode' };
2938
- if (!CONFIG.STRIPE_WEBHOOK_SECRET && !allowUnsignedStripeWebhooks()) {
2939
- return {
2940
- handled: false,
2941
- reason: 'invalid_signature',
2942
- error: 'STRIPE_WEBHOOK_SECRET is required before Stripe webhooks can be processed.',
2943
- };
2944
- }
2945
2870
  let event;
2946
2871
  try {
2947
2872
  if (CONFIG.STRIPE_WEBHOOK_SECRET) {
@@ -3050,22 +2975,6 @@ async function handleWebhook(rawBody, signature) {
3050
2975
  attribution,
3051
2976
  });
3052
2977
  }
3053
- // Fire Plausible purchase event so the funnel poller can measure
3054
- // end-to-end conversion: visitor → CTA → checkout → email → Stripe → purchase.
3055
- // Fire-and-forget (never blocks the webhook response).
3056
- void recordCheckoutFunnelEvent('purchase', {
3057
- page: '/success',
3058
- props: {
3059
- sessionId: session.id,
3060
- customerId,
3061
- traceId: traceId || '',
3062
- packId: packId || '',
3063
- amount: session.amount_total != null ? String(session.amount_total) : '',
3064
- currency: session.currency || '',
3065
- ...attribution,
3066
- },
3067
- });
3068
-
3069
2978
  return {
3070
2979
  handled: true,
3071
2980
  action: 'provisioned_api_key',
@@ -3218,7 +3127,6 @@ module.exports = {
3218
3127
  _buildTrialActivationEmail: buildTrialActivationEmail,
3219
3128
  _sendTrialActivationEmail: sendTrialActivationEmail,
3220
3129
  _resolveSubscriptionCheckoutSelection: resolveSubscriptionCheckoutSelection,
3221
- _verifyActiveProductForPlan: verifyActiveProductForPlan,
3222
3130
  _API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
3223
3131
  _FUNNEL_LEDGER_PATH: () => CONFIG.FUNNEL_LEDGER_PATH,
3224
3132
  _REVENUE_LEDGER_PATH: () => CONFIG.REVENUE_LEDGER_PATH,
@@ -5,11 +5,6 @@ const PROJECT_ROOT = path.resolve(__dirname, '..');
5
5
  const DEFAULT_BUILD_METADATA_PATH = path.join(PROJECT_ROOT, 'config', 'build-metadata.json');
6
6
  const BUILD_SHA_ENV_KEY = 'THUMBGATE_BUILD_SHA';
7
7
  const BUILD_GENERATED_AT_ENV_KEY = 'THUMBGATE_BUILD_GENERATED_AT';
8
- // Railway injects this automatically for GitHub-connected deployments: the git
9
- // SHA of the commit that triggered the deploy. It is the ground truth for what
10
- // code is actually live, and unlike THUMBGATE_BUILD_SHA it cannot drift (Railway
11
- // sets it per deploy). https://docs.railway.com/reference/variables
12
- const RAILWAY_GIT_COMMIT_SHA_ENV_KEY = 'RAILWAY_GIT_COMMIT_SHA';
13
8
 
14
9
  function normalizeNullableText(value) {
15
10
  if (typeof value !== 'string') {
@@ -21,69 +16,35 @@ function normalizeNullableText(value) {
21
16
  }
22
17
 
23
18
  function resolveBuildMetadata({ env = process.env, filePath } = {}) {
24
- // Precedence: immutable JSON file (baked into Docker image at build time, so it
25
- // ALWAYS matches the deployed code) wins over runtime env vars. Env vars are
26
- // mutable Railway/host config that can drift — they shadowed the freshly-stamped
27
- // SHA in prod on 2026-05-20 and made /health lie about the deployed commit.
28
- // Fall back to env vars only when the file is missing or its values are null,
29
- // and require an explicit SHA env var (not just a stray GENERATED_AT) before
30
- // trusting the env branch.
31
19
  const resolvedPath =
32
20
  normalizeNullableText(filePath) ||
33
21
  normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
34
22
  DEFAULT_BUILD_METADATA_PATH;
35
23
  const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
36
- const railwayGitSha = normalizeNullableText(env[RAILWAY_GIT_COMMIT_SHA_ENV_KEY]);
37
24
  const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
38
25
 
39
- let fileBuildSha = null;
40
- let fileGeneratedAt = null;
41
- try {
42
- const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
43
- fileBuildSha = normalizeNullableText(parsed.buildSha);
44
- fileGeneratedAt = normalizeNullableText(parsed.generatedAt);
45
- } catch {
46
- // file missing or unreadable — fall through to env branch
47
- }
48
-
49
- if (fileBuildSha) {
26
+ if (envBuildSha || envGeneratedAt) {
50
27
  return {
51
28
  path: resolvedPath,
52
- buildSha: fileBuildSha,
53
- generatedAt: fileGeneratedAt || envGeneratedAt,
29
+ buildSha: envBuildSha,
30
+ generatedAt: envGeneratedAt,
54
31
  };
55
32
  }
56
33
 
57
- // No SHA baked into the image. Prefer Railway's own per-deploy commit SHA over
58
- // THUMBGATE_BUILD_SHA: the latter is set out-of-band by the deploy workflow and
59
- // has drifted in prod (stuck reporting an old commit while newer code was live,
60
- // because RAILWAY_SYNC_VARIABLES is off and `railway up` stamping is unreliable).
61
- // RAILWAY_GIT_COMMIT_SHA is injected by Railway per deploy, so it always matches
62
- // the code actually serving traffic on a GitHub-connected service.
63
- if (railwayGitSha) {
34
+ try {
35
+ const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
64
36
  return {
65
37
  path: resolvedPath,
66
- buildSha: railwayGitSha,
67
- generatedAt: envGeneratedAt,
38
+ buildSha: normalizeNullableText(parsed.buildSha),
39
+ generatedAt: normalizeNullableText(parsed.generatedAt),
68
40
  };
69
- }
70
-
71
- // Last resort: the workflow-managed env var. Only trust it when an explicit SHA
72
- // is set. (Previously a bare GENERATED_AT with no SHA could short-circuit and
73
- // return { buildSha: null }, losing both signals; now we require the SHA.)
74
- if (envBuildSha) {
41
+ } catch {
75
42
  return {
76
43
  path: resolvedPath,
77
- buildSha: envBuildSha,
78
- generatedAt: envGeneratedAt,
44
+ buildSha: null,
45
+ generatedAt: null,
79
46
  };
80
47
  }
81
-
82
- return {
83
- path: resolvedPath,
84
- buildSha: null,
85
- generatedAt: fileGeneratedAt || envGeneratedAt,
86
- };
87
48
  }
88
49
 
89
50
  function writeBuildMetadataFile({ sha, outputPath, generatedAt = new Date().toISOString() }) {
@@ -144,7 +105,6 @@ if (require.main === module) {
144
105
  module.exports = {
145
106
  BUILD_GENERATED_AT_ENV_KEY,
146
107
  BUILD_SHA_ENV_KEY,
147
- RAILWAY_GIT_COMMIT_SHA_ENV_KEY,
148
108
  DEFAULT_BUILD_METADATA_PATH,
149
109
  resolveBuildMetadata,
150
110
  writeBuildMetadataFile,
@@ -14,7 +14,6 @@
14
14
  */
15
15
 
16
16
  const { captureFeedback } = require('./feedback-loop');
17
- const { isGenericFeedbackText, normalizeFeedbackSignal } = require('./feedback-quality');
18
17
  const { loadOptionalModule } = require('./private-core-boundary');
19
18
  // `history-distiller` is a PRIVATE_CORE_MODULE — present in this checkout and
20
19
  // in ThumbGate-Core, but intentionally excluded from the public npm tarball.
@@ -48,21 +47,15 @@ const RST = '\x1b[0m';
48
47
  */
49
48
  function processInlineFeedback({ signal, context, chatHistory, whatWentWrong, whatWorked } = {}) {
50
49
  const isDown = signal === 'down' || signal === 'negative';
51
- const normalizedQualitySignal = normalizeFeedbackSignal(isDown ? 'down' : 'up');
52
- const providedContext = String(context || '').trim();
53
- const genericContext = isGenericFeedbackText(providedContext, normalizedQualitySignal);
54
- const effectiveWhatWentWrong = whatWentWrong || (isDown && providedContext && !genericContext ? providedContext : undefined);
55
- const effectiveWhatWorked = whatWorked || (!isDown && providedContext && !genericContext ? providedContext : undefined);
56
50
 
57
51
  // 1. Capture the feedback
58
52
  let feedbackResult;
59
53
  try {
60
54
  feedbackResult = captureFeedback({
61
55
  signal: isDown ? 'down' : 'up',
62
- context: providedContext,
63
- chatHistory,
64
- whatWentWrong: effectiveWhatWentWrong,
65
- whatWorked: effectiveWhatWorked,
56
+ context: context || (isDown ? 'Thumbs down from CLI' : 'Thumbs up from CLI'),
57
+ whatWentWrong: whatWentWrong || undefined,
58
+ whatWorked: whatWorked || undefined,
66
59
  });
67
60
  } catch (err) {
68
61
  feedbackResult = { accepted: false, reason: err.message };
@@ -95,19 +88,14 @@ function formatCliOutput(result) {
95
88
  if (result.feedbackResult && result.feedbackResult.accepted !== false) {
96
89
  lines.push(`${isDown ? R : G}${BD}${isDown ? '👎 Thumbs down recorded' : '👍 Thumbs up recorded'}${RST}`);
97
90
  const feedbackId = (result.feedbackResult.feedbackEvent && result.feedbackResult.feedbackEvent.id) || result.feedbackResult.id;
98
- const memoryId = (result.feedbackResult.memoryRecord && result.feedbackResult.memoryRecord.id) || result.feedbackResult.memoryId;
99
91
  if (feedbackId) {
100
- lines.push(`${D} Feedback ID: ${feedbackId}${RST}`);
101
- if (memoryId) lines.push(`${D} Memory ID : ${memoryId}${RST}`);
92
+ lines.push(`${D} ID: ${feedbackId}${RST}`);
102
93
  // Echo feedback ID to stderr so it's visible directly in the terminal,
103
94
  // not hidden behind Claude Code's "ctrl+o to expand" MCP call collapse.
104
- process.stderr.write(`✅ Feedback captured (${feedbackId}${memoryId ? `, ${memoryId}` : ''})\n`);
95
+ process.stderr.write(`✅ Feedback captured (${feedbackId})\n`);
105
96
  }
106
97
  } else {
107
- const clarification = result.feedbackResult && result.feedbackResult.needsClarification
108
- ? ` ${result.feedbackResult.prompt || result.feedbackResult.message || ''}`.trimEnd()
109
- : '';
110
- lines.push(`${R}Feedback not accepted: ${(result.feedbackResult && result.feedbackResult.reason) || 'unknown'}${clarification ? ` — ${clarification}` : ''}${RST}`);
98
+ lines.push(`${R}Feedback not accepted: ${(result.feedbackResult && result.feedbackResult.reason) || 'unknown'}${RST}`);
111
99
  }
112
100
 
113
101
  // Distilled lesson (if thumbs down)