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
@@ -581,6 +581,20 @@ async function main() {
581
581
  const args = process.argv.slice(2);
582
582
  const dryRun = args.includes('--dry-run');
583
583
  const verbose = args.includes('--verbose') || args.includes('-v');
584
+ let hookMode = args.includes('--hook');
585
+
586
+ // Settings may already be cached by a running agent process. Detect the
587
+ // current Stop payload as well, so the legacy command (without --hook) is
588
+ // immediately JSON-safe before the host reloads its settings.
589
+ if (!hookMode && !process.stdin.isTTY) {
590
+ try {
591
+ const raw = fs.readFileSync(0, 'utf8');
592
+ const payload = raw ? JSON.parse(raw) : {};
593
+ hookMode = payload.hook_event_name === 'Stop';
594
+ } catch {
595
+ // Normal CLI invocation with empty/non-JSON stdin.
596
+ }
597
+ }
584
598
 
585
599
  if (args.includes('--status')) {
586
600
  const status = getMetaAgentStatus();
@@ -593,9 +607,13 @@ async function main() {
593
607
  }
594
608
 
595
609
  const mode = dryRun ? 'DRY RUN' : 'LIVE';
596
- console.log(`Meta-agent loop starting [${mode}]...`);
610
+ if (!hookMode) console.log(`Meta-agent loop starting [${mode}]...`);
611
+
612
+ const manifest = await runMetaAgentLoop({ dryRun, verbose: hookMode ? false : true });
597
613
 
598
- const manifest = await runMetaAgentLoop({ dryRun, verbose: verbose || true });
614
+ // Stop hooks may emit only empty stdout or a valid JSON object. The loop's
615
+ // work is intentionally silent in hook mode.
616
+ if (hookMode) return;
599
617
 
600
618
  console.log(`Run ID : ${manifest.runId}`);
601
619
  console.log(`Analysis mode : ${manifest.analysisMode}`);
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Load operator-local observability credentials into process.env.
5
+ *
6
+ * Sources (first wins per key, never overwrite an already-set env var):
7
+ * 1. process.env
8
+ * 2. ~/.config/thumbgate/observability.json
9
+ * 3. ~/.config/thumbgate/operator.json (operatorKey / baseUrl only)
10
+ * 4. Stripe managed secret files via resolveStripeSecretKey
11
+ *
12
+ * The JSON file is gitignored operator state. Never print secret values.
13
+ */
14
+
15
+ const fs = require('node:fs');
16
+ const os = require('node:os');
17
+ const path = require('node:path');
18
+
19
+ const OBSERVABILITY_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'observability.json');
20
+ const OPERATOR_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
21
+
22
+ const JSON_KEY_TO_ENV = Object.freeze({
23
+ stripeSecretKey: 'STRIPE_SECRET_KEY',
24
+ plausibleApiKey: 'PLAUSIBLE_API_KEY',
25
+ plausibleSiteId: 'PLAUSIBLE_SITE_ID',
26
+ plausibleSiteIds: 'PLAUSIBLE_SITE_IDS',
27
+ plausibleRegisteredDomains: 'THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS',
28
+ posthogPersonalApiKey: 'POSTHOG_PERSONAL_API_KEY',
29
+ posthogProjectId: 'POSTHOG_PROJECT_ID',
30
+ operatorKey: 'THUMBGATE_OPERATOR_KEY',
31
+ apiKey: 'THUMBGATE_API_KEY',
32
+ publicAppOrigin: 'THUMBGATE_PUBLIC_APP_ORIGIN',
33
+ billingApiBaseUrl: 'THUMBGATE_BILLING_API_BASE_URL',
34
+ });
35
+
36
+ function normalizeText(value) {
37
+ if (value === undefined || value === null) return null;
38
+ const text = String(value).trim();
39
+ return text || null;
40
+ }
41
+
42
+ function readJsonFile(filePath) {
43
+ try {
44
+ if (!filePath || !fs.existsSync(filePath)) return null;
45
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function applyJsonToEnv(json, env) {
52
+ if (!json || typeof json !== 'object') return [];
53
+ const applied = [];
54
+ for (const [jsonKey, envKey] of Object.entries(JSON_KEY_TO_ENV)) {
55
+ if (normalizeText(env[envKey])) continue;
56
+ const value = normalizeText(json[jsonKey]);
57
+ if (!value) continue;
58
+ env[envKey] = value;
59
+ applied.push(envKey);
60
+ }
61
+ return applied;
62
+ }
63
+
64
+ function loadObservabilityEnv({
65
+ env = process.env,
66
+ observabilityPath = OBSERVABILITY_CONFIG_PATH,
67
+ operatorPath = OPERATOR_CONFIG_PATH,
68
+ applyStripeManagedFiles = true,
69
+ } = {}) {
70
+ const applied = [];
71
+
72
+ const observability = readJsonFile(observabilityPath);
73
+ applied.push(...applyJsonToEnv(observability, env));
74
+
75
+ const operator = readJsonFile(operatorPath);
76
+ if (operator) {
77
+ if (!normalizeText(env.THUMBGATE_OPERATOR_KEY) && normalizeText(operator.operatorKey)) {
78
+ env.THUMBGATE_OPERATOR_KEY = String(operator.operatorKey).trim();
79
+ applied.push('THUMBGATE_OPERATOR_KEY');
80
+ }
81
+ if (!normalizeText(env.THUMBGATE_BILLING_API_BASE_URL) && normalizeText(operator.baseUrl)) {
82
+ env.THUMBGATE_BILLING_API_BASE_URL = String(operator.baseUrl).trim();
83
+ applied.push('THUMBGATE_BILLING_API_BASE_URL');
84
+ }
85
+ }
86
+
87
+ if (applyStripeManagedFiles && !normalizeText(env.STRIPE_SECRET_KEY)) {
88
+ try {
89
+ const { resolveStripeSecretKey } = require('./stripe-credentials');
90
+ const resolved = resolveStripeSecretKey({ env });
91
+ if (resolved.secretKey) {
92
+ env.STRIPE_SECRET_KEY = resolved.secretKey;
93
+ applied.push('STRIPE_SECRET_KEY');
94
+ }
95
+ } catch {
96
+ // stripe-credentials optional at load time
97
+ }
98
+ }
99
+
100
+ // Product-primary Plausible domain is always part of the registered set for
101
+ // doctor/automation unless the operator explicitly overrides site id.
102
+ if (!normalizeText(env.PLAUSIBLE_SITE_ID) && !normalizeText(env.THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS)) {
103
+ env.THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS = 'thumbgate.ai,thumbgate-production.up.railway.app';
104
+ applied.push('THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS');
105
+ }
106
+
107
+ return {
108
+ applied: [...new Set(applied)],
109
+ observabilityPath,
110
+ operatorPath,
111
+ hasStripe: Boolean(normalizeText(env.STRIPE_SECRET_KEY)),
112
+ hasPlausible: Boolean(normalizeText(env.PLAUSIBLE_API_KEY) && normalizeText(env.PLAUSIBLE_SITE_ID)),
113
+ hasPosthog: Boolean(normalizeText(env.POSTHOG_PERSONAL_API_KEY) && normalizeText(env.POSTHOG_PROJECT_ID)),
114
+ hasOperator: Boolean(normalizeText(env.THUMBGATE_OPERATOR_KEY) || normalizeText(env.THUMBGATE_API_KEY)),
115
+ };
116
+ }
117
+
118
+ function observabilityConfigTemplate() {
119
+ return {
120
+ stripeSecretKey: '',
121
+ plausibleApiKey: '',
122
+ plausibleSiteId: 'thumbgate.ai',
123
+ plausibleRegisteredDomains: 'thumbgate.ai,thumbgate-production.up.railway.app',
124
+ posthogPersonalApiKey: '',
125
+ posthogProjectId: '',
126
+ publicAppOrigin: 'https://thumbgate.ai',
127
+ billingApiBaseUrl: 'https://thumbgate-production.up.railway.app',
128
+ };
129
+ }
130
+
131
+ module.exports = {
132
+ OBSERVABILITY_CONFIG_PATH,
133
+ OPERATOR_CONFIG_PATH,
134
+ JSON_KEY_TO_ENV,
135
+ loadObservabilityEnv,
136
+ observabilityConfigTemplate,
137
+ readJsonFile,
138
+ normalizeText,
139
+ };
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+ const {
8
+ OBSERVABILITY_CONFIG_PATH,
9
+ observabilityConfigTemplate,
10
+ loadObservabilityEnv,
11
+ } = require('./observability-env');
12
+
13
+ function ensureDir(filePath) {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ }
16
+
17
+ function main(argv = process.argv.slice(2)) {
18
+ const write = argv.includes('--write');
19
+ const print = argv.includes('--print') || !write;
20
+ const template = observabilityConfigTemplate();
21
+
22
+ if (print && !write) {
23
+ process.stdout.write(`${JSON.stringify(template, null, 2)}\n`);
24
+ process.stdout.write(
25
+ `\n# Write to ${OBSERVABILITY_CONFIG_PATH} with --write after filling secrets.\n` +
26
+ '# Never commit this file. Doctor/revenue tools load it automatically.\n'
27
+ );
28
+ }
29
+
30
+ if (write) {
31
+ ensureDir(OBSERVABILITY_CONFIG_PATH);
32
+ if (fs.existsSync(OBSERVABILITY_CONFIG_PATH)) {
33
+ process.stderr.write(`Refusing to overwrite existing ${OBSERVABILITY_CONFIG_PATH}\n`);
34
+ process.exitCode = 2;
35
+ return;
36
+ }
37
+ fs.writeFileSync(OBSERVABILITY_CONFIG_PATH, `${JSON.stringify(template, null, 2)}\n`, { mode: 0o600 });
38
+ process.stdout.write(`Wrote template ${OBSERVABILITY_CONFIG_PATH}\n`);
39
+ }
40
+
41
+ const status = loadObservabilityEnv({ env: { ...process.env } });
42
+ process.stdout.write(JSON.stringify({
43
+ configPath: OBSERVABILITY_CONFIG_PATH,
44
+ hasStripe: status.hasStripe,
45
+ hasPlausible: status.hasPlausible,
46
+ hasPosthog: status.hasPosthog,
47
+ hasOperator: status.hasOperator,
48
+ }, null, 2) + '\n');
49
+ }
50
+
51
+ if (require('node:path').resolve(process.argv[1] || '') === require('node:path').resolve(__filename)) {
52
+ main();
53
+ }
54
+
55
+ module.exports = { main };
@@ -30,7 +30,11 @@ function getConfiguredRegisteredDomains(env = process.env) {
30
30
  ...splitDomains(env.PLAUSIBLE_REGISTERED_DOMAINS),
31
31
  ].map(normalizeDomain).filter(Boolean);
32
32
 
33
+ // Both product surfaces are first-class Plausible site ids for ThumbGate.
34
+ // Emitting data-domain=thumbgate.ai while only registering the Railway host
35
+ // previously made primary-domain traffic invisible to automation.
33
36
  return [...new Set([
37
+ PRIMARY_PLAUSIBLE_DOMAIN,
34
38
  FALLBACK_REGISTERED_PLAUSIBLE_DOMAIN,
35
39
  ...configured,
36
40
  ])];
@@ -143,6 +143,8 @@ function handleRejectExpectation(checks, result, expected) {
143
143
 
144
144
  const wasRejected = result.accepted === false
145
145
  || result.status === 'rejected'
146
+ || result.status === 'clarification_required'
147
+ || result.needsClarification === true
146
148
  || result.actionType === 'no-action';
147
149
  checks.push({
148
150
  criterion: 'shouldReject',
@@ -466,8 +468,9 @@ function runSuiteObject(suite, options = {}) {
466
468
  const skipped = results.filter((r) => r.status === 'skip').length;
467
469
  const totalScore = results.length > 0
468
470
  ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length)
469
- : 100;
471
+ : 0;
470
472
  const minScore = options.minScore ?? 80;
473
+ const insufficientEvidence = results.length === 0;
471
474
 
472
475
  return {
473
476
  suite: suite.name,
@@ -478,8 +481,9 @@ function runSuiteObject(suite, options = {}) {
478
481
  skipped,
479
482
  score: totalScore,
480
483
  minScore,
481
- pass: totalScore >= minScore,
482
- noCases: results.length === 0,
484
+ pass: !insufficientEvidence && totalScore >= minScore,
485
+ noCases: insufficientEvidence,
486
+ evidenceStatus: insufficientEvidence ? 'insufficient_evidence' : 'measured',
483
487
  feedbackDerived: suite.source && suite.source.type === 'feedback-log',
484
488
  generatedAt: new Date().toISOString(),
485
489
  results,
@@ -685,6 +689,7 @@ function runSuite(suitePath = DEFAULT_SUITE, options = {}) {
685
689
 
686
690
  function compareReports(currentReport, baselineReport) {
687
691
  const baselineById = new Map((baselineReport?.results || []).map((result) => [result.id, result]));
692
+ const currentById = new Map((currentReport?.results || []).map((result) => [result.id, result]));
688
693
  const regressions = [];
689
694
  const improvements = [];
690
695
 
@@ -717,12 +722,83 @@ function compareReports(currentReport, baselineReport) {
717
722
  }
718
723
  }
719
724
 
725
+ for (const baseline of baselineReport?.results || []) {
726
+ if (currentById.has(baseline.id)) continue;
727
+ regressions.push({
728
+ id: baseline.id,
729
+ baselineScore: baseline.score,
730
+ currentScore: null,
731
+ delta: null,
732
+ baselineStatus: baseline.status,
733
+ currentStatus: 'missing',
734
+ });
735
+ }
736
+
720
737
  return {
721
738
  baselineSuite: baselineReport?.suite || null,
722
739
  baselineScore: Number.isFinite(Number(baselineReport?.score)) ? Number(baselineReport.score) : null,
723
740
  scoreDelta: Number.isFinite(Number(baselineReport?.score)) ? currentReport.score - Number(baselineReport.score) : null,
724
741
  regressions,
725
742
  improvements,
743
+ baselineCases: baselineById.size,
744
+ currentCases: currentById.size,
745
+ baselineCoverageRate: baselineById.size
746
+ ? Math.round((Array.from(baselineById.keys()).filter((id) => currentById.has(id)).length / baselineById.size) * 10000) / 10000
747
+ : null,
748
+ };
749
+ }
750
+
751
+ function logSafeResult(result, index) {
752
+ const allowedStatuses = new Set(['pass', 'fail', 'error', 'skip']);
753
+ return {
754
+ case: index + 1,
755
+ status: allowedStatuses.has(result.status) ? result.status : 'error',
756
+ score: Number(result.score || 0),
757
+ passCount: Number(result.passCount || 0),
758
+ totalChecks: Number(result.totalChecks || 0),
759
+ };
760
+ }
761
+
762
+ function logSafeReport(report, suite, fromFeedback) {
763
+ const source = suite?.source || {};
764
+ const comparison = report.comparison
765
+ ? {
766
+ baselineScore: Number(report.comparison.baselineScore || 0),
767
+ scoreDelta: Number(report.comparison.scoreDelta || 0),
768
+ regressionCount: Number(report.comparison.regressions?.length || 0),
769
+ improvementCount: Number(report.comparison.improvements?.length || 0),
770
+ baselineCases: Number(report.comparison.baselineCases || 0),
771
+ currentCases: Number(report.comparison.currentCases || 0),
772
+ baselineCoverageRate: report.comparison.baselineCoverageRate,
773
+ }
774
+ : undefined;
775
+ return {
776
+ suiteType: fromFeedback ? 'feedback-derived' : 'configured',
777
+ total: Number(report.total || 0),
778
+ passed: Number(report.passed || 0),
779
+ failed: Number(report.failed || 0),
780
+ errors: Number(report.errors || 0),
781
+ skipped: Number(report.skipped || 0),
782
+ score: Number(report.score || 0),
783
+ minScore: Number(report.minScore || 0),
784
+ pass: report.pass === true,
785
+ noCases: report.noCases === true,
786
+ evidenceStatus: report.evidenceStatus === 'measured' ? 'measured' : 'insufficient_evidence',
787
+ feedbackDerived: report.feedbackDerived === true,
788
+ syntheticCount: Number(report.syntheticCount || 0),
789
+ comparison,
790
+ results: (report.results || []).map(logSafeResult),
791
+ suiteDefinition: fromFeedback
792
+ ? {
793
+ version: Number(suite?.version || 0),
794
+ source: {
795
+ type: 'feedback-log',
796
+ totalEntries: Number(source.totalEntries || 0),
797
+ selectedCases: Number(source.selectedCases || 0),
798
+ },
799
+ evaluationCount: Number(suite?.evaluations?.length || 0),
800
+ }
801
+ : undefined,
726
802
  };
727
803
  }
728
804
 
@@ -847,7 +923,7 @@ if (isCliInvocation()) {
847
923
  }
848
924
 
849
925
  if (json) {
850
- console.log(JSON.stringify({ ...report, suiteDefinition: fromFeedback ? suite : undefined }, null, 2));
926
+ console.log(JSON.stringify(logSafeReport(report, suite, fromFeedback), null, 2));
851
927
  } else {
852
928
  console.log(`\n${report.suite}`);
853
929
  console.log('='.repeat(50));
@@ -883,6 +959,7 @@ module.exports = {
883
959
  gradeOutput,
884
960
  loadSuite,
885
961
  loadReport,
962
+ logSafeReport,
886
963
  compareReports,
887
964
  readJsonl,
888
965
  runEvaluation,