thumbgate 1.27.18 → 1.27.20

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 (170) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +4 -3
  9. package/.well-known/agentic-verify.txt +1 -0
  10. package/.well-known/llms.txt +33 -12
  11. package/.well-known/mcp/server-card.json +8 -8
  12. package/README.md +246 -30
  13. package/adapters/claude/.mcp.json +2 -2
  14. package/adapters/codex/config.toml +2 -2
  15. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  16. package/adapters/gemini/function-declarations.json +1 -0
  17. package/adapters/letta/README.md +41 -0
  18. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  19. package/adapters/mcp/server-stdio.js +263 -11
  20. package/adapters/opencode/opencode.json +1 -1
  21. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  22. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  23. package/bench/observability-eval-suite.json +26 -0
  24. package/bench/thumbgate-bench.json +2 -2
  25. package/bin/cli.js +1456 -122
  26. package/bin/dashboard-cli.js +7 -0
  27. package/bin/postinstall.js +11 -27
  28. package/commands/dashboard.md +15 -0
  29. package/commands/thumbgate-dashboard.md +15 -0
  30. package/config/gate-classifier-routing.json +98 -0
  31. package/config/gate-templates.json +216 -0
  32. package/config/gates/claim-verification.json +12 -0
  33. package/config/gates/default.json +31 -2
  34. package/config/github-about.json +2 -2
  35. package/config/mcp-allowlists.json +23 -13
  36. package/config/merge-quality-checks.json +0 -1
  37. package/config/model-candidates.json +121 -6
  38. package/config/post-deploy-marketing-pages.json +80 -0
  39. package/config/tessl-tiles.json +1 -3
  40. package/openapi/openapi.yaml +12 -0
  41. package/package.json +225 -100
  42. package/public/about.html +162 -0
  43. package/public/agent-manager.html +179 -0
  44. package/public/agents-cost-savings.html +153 -0
  45. package/public/ai-malpractice-prevention.html +818 -0
  46. package/public/assets/brand/github-social-preview.png +0 -0
  47. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  48. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  49. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  50. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  51. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  52. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  53. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  54. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  55. package/public/assets/brand/thumbgate-mark.svg +21 -0
  56. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  57. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  58. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  59. package/public/assets/legal-intake-control-flow.svg +66 -0
  60. package/public/blog.html +4 -4
  61. package/public/brand/thumbgate-mark.svg +19 -0
  62. package/public/brand/thumbgate-og.svg +16 -0
  63. package/public/chatgpt-app.html +330 -0
  64. package/public/codex-enterprise.html +123 -0
  65. package/public/codex-plugin.html +72 -20
  66. package/public/compare.html +31 -8
  67. package/public/dashboard.html +930 -166
  68. package/public/diagnostic.html +345 -0
  69. package/public/federal.html +2 -2
  70. package/public/guide.html +33 -13
  71. package/public/index.html +469 -111
  72. package/public/install.html +193 -0
  73. package/public/js/buyer-intent.js +672 -0
  74. package/public/learn.html +183 -18
  75. package/public/lessons.html +168 -10
  76. package/public/numbers.html +7 -7
  77. package/public/pricing.html +399 -0
  78. package/public/pro.html +34 -11
  79. package/scripts/action-receipts.js +324 -0
  80. package/scripts/activation-quickstart.js +187 -0
  81. package/scripts/agent-memory-lifecycle.js +211 -0
  82. package/scripts/agent-operations-planner.js +621 -0
  83. package/scripts/agent-readiness.js +20 -3
  84. package/scripts/agent-reward-model.js +53 -1
  85. package/scripts/ai-component-inventory.js +367 -0
  86. package/scripts/async-eval-observability.js +236 -0
  87. package/scripts/audit.js +65 -0
  88. package/scripts/auto-promote-gates.js +82 -10
  89. package/scripts/auto-wire-hooks.js +14 -0
  90. package/scripts/aws-blocks-guardrails.js +272 -0
  91. package/scripts/billing.js +93 -1
  92. package/scripts/bot-detection.js +61 -3
  93. package/scripts/build-metadata.js +50 -10
  94. package/scripts/classifier-routing.js +130 -0
  95. package/scripts/cli-feedback.js +4 -2
  96. package/scripts/cli-schema.js +97 -0
  97. package/scripts/cli-telemetry.js +6 -1
  98. package/scripts/commercial-offer.js +82 -2
  99. package/scripts/context-manager.js +74 -6
  100. package/scripts/dashboard-chat.js +332 -0
  101. package/scripts/dashboard.js +68 -2
  102. package/scripts/export-databricks-bundle.js +5 -1
  103. package/scripts/export-dpo-pairs.js +7 -2
  104. package/scripts/feedback-aggregate.js +281 -0
  105. package/scripts/feedback-loop.js +123 -1
  106. package/scripts/feedback-quality.js +87 -0
  107. package/scripts/feedback-sanitizer.js +105 -0
  108. package/scripts/filesystem-search.js +35 -10
  109. package/scripts/gate-stats.js +89 -0
  110. package/scripts/gates-engine.js +1176 -85
  111. package/scripts/gemini-embedding-policy.js +2 -1
  112. package/scripts/hook-runtime.js +20 -14
  113. package/scripts/hook-stop-anti-claim.js +301 -0
  114. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  115. package/scripts/hybrid-feedback-context.js +142 -7
  116. package/scripts/install-shim.js +87 -0
  117. package/scripts/lesson-inference.js +8 -3
  118. package/scripts/lesson-search.js +17 -1
  119. package/scripts/license.js +10 -10
  120. package/scripts/llm-client.js +169 -4
  121. package/scripts/local-model-profile.js +15 -8
  122. package/scripts/mcp-config.js +7 -1
  123. package/scripts/mcp-oauth.js +293 -0
  124. package/scripts/memory-scope-readiness.js +159 -0
  125. package/scripts/meta-agent-loop.js +36 -0
  126. package/scripts/noop-detect.js +285 -0
  127. package/scripts/operational-integrity.js +39 -5
  128. package/scripts/oss-pr-opportunity-scout.js +35 -5
  129. package/scripts/parallel-workflow-orchestrator.js +293 -0
  130. package/scripts/plan-gate.js +243 -0
  131. package/scripts/plausible-domain-config.js +99 -0
  132. package/scripts/plausible-server-events.js +9 -6
  133. package/scripts/pro-local-dashboard.js +4 -4
  134. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  135. package/scripts/published-cli.js +0 -8
  136. package/scripts/qa-scenario-planner.js +136 -0
  137. package/scripts/rate-limiter.js +64 -13
  138. package/scripts/repeat-metric.js +137 -0
  139. package/scripts/secret-fixture-tokens.js +61 -0
  140. package/scripts/secret-redaction.js +166 -0
  141. package/scripts/secret-scanner.js +44 -5
  142. package/scripts/security-scanner.js +260 -10
  143. package/scripts/self-distill-agent.js +3 -1
  144. package/scripts/self-harness-optimizer.js +141 -0
  145. package/scripts/self-healing-check.js +193 -0
  146. package/scripts/self-protection.js +90 -0
  147. package/scripts/seo-gsd.js +916 -7
  148. package/scripts/silent-failure-cluster.js +531 -0
  149. package/scripts/statusline-cache-path.js +17 -2
  150. package/scripts/statusline-cache-read.js +57 -0
  151. package/scripts/statusline-local-stats.js +9 -1
  152. package/scripts/statusline-meta.js +28 -2
  153. package/scripts/statusline.sh +20 -4
  154. package/scripts/sync-telemetry-from-prod.js +374 -0
  155. package/scripts/telemetry-analytics.js +357 -0
  156. package/scripts/thompson-sampling.js +31 -10
  157. package/scripts/thumbgate-bench.js +16 -1
  158. package/scripts/thumbgate-search.js +85 -19
  159. package/scripts/tool-contract-validator.js +76 -0
  160. package/scripts/tool-registry.js +169 -1
  161. package/scripts/trajectory-scorer.js +63 -0
  162. package/scripts/vector-store.js +45 -0
  163. package/scripts/verify-marketing-pages-deployed.js +212 -0
  164. package/scripts/visitor-journey.js +172 -0
  165. package/scripts/workflow-sentinel.js +286 -53
  166. package/scripts/workspace-evolver.js +62 -2
  167. package/src/api/server.js +2683 -319
  168. package/.claude-plugin/marketplace.json +0 -85
  169. package/adapters/chatgpt/openapi.yaml +0 -1695
  170. package/scripts/bot-detector.js +0 -50
@@ -16,7 +16,7 @@ const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
16
16
  * 2. Env var: THUMBGATE_DEV_BYPASS=[set via THUMBGATE_DEV_SECRET env var]
17
17
  * Requires a specific non-obvious value (not boolean) to prevent accidental activation.
18
18
  */
19
- function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
19
+ function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE || os.homedir() } = {}) {
20
20
  // Layer 1: env var with specific value
21
21
  if (CREATOR_BYPASS_VALUE && String(env[CREATOR_BYPASS_ENV] || '') === CREATOR_BYPASS_VALUE) {
22
22
  return true;
@@ -37,7 +37,7 @@ function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
37
37
  * with any non-empty bypass value. No env var needed — just the config file.
38
38
  * Used by the server to skip auth on localhost during local development.
39
39
  */
40
- function hasDevOverride(homeDir = os.homedir()) {
40
+ function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
41
41
  // Disabled during test runs to avoid interfering with auth assertions
42
42
  if (process.env.NODE_TEST_CONTEXT || process.env.THUMBGATE_TESTING) return false;
43
43
  try {
@@ -47,11 +47,11 @@ function hasDevOverride(homeDir = os.homedir()) {
47
47
  } catch { return false; }
48
48
  }
49
49
 
50
- function getLicenseDir(homeDir = os.homedir()) {
50
+ function getLicenseDir(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
51
51
  return path.join(homeDir, '.thumbgate');
52
52
  }
53
53
 
54
- function getLicensePath(homeDir = os.homedir()) {
54
+ function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
55
55
  return path.join(getLicenseDir(homeDir), 'license.json');
56
56
  }
57
57
 
@@ -39,9 +39,23 @@ function normalizeOptions(options = {}) {
39
39
  ...splitCsv(options.documents),
40
40
  ...splitCsv(options['document-ids']),
41
41
  ]);
42
+ const sourcePointers = unique([
43
+ ...splitCsv(options['source-pointers']),
44
+ ...splitCsv(options.pointers),
45
+ ...splitCsv(options.sources),
46
+ ]);
42
47
  const candidateImages = Number.isFinite(Number(options['candidate-images']))
43
48
  ? Number(options['candidate-images'])
44
49
  : null;
50
+ const extractedEntities = Number.isFinite(Number(options['extracted-entities']))
51
+ ? Number(options['extracted-entities'])
52
+ : 0;
53
+ const extractedRelations = Number.isFinite(Number(options['extracted-relations']))
54
+ ? Number(options['extracted-relations'])
55
+ : 0;
56
+ const promotionThreshold = Number.isFinite(Number(options['promotion-threshold']))
57
+ ? Number(options['promotion-threshold'])
58
+ : 3;
45
59
 
46
60
  return {
47
61
  ragTool: String(options['rag-tool'] || options.tool || 'proxy-pointer-rag').trim() || 'proxy-pointer-rag',
@@ -49,10 +63,15 @@ function normalizeOptions(options = {}) {
49
63
  sectionIds,
50
64
  imagePointers,
51
65
  documentIds,
66
+ sourcePointers,
52
67
  candidateImages,
68
+ extractedEntities,
69
+ extractedRelations,
70
+ promotionThreshold,
53
71
  crossDocumentPolicy: String(options['cross-doc-policy'] || options['cross-document-policy'] || '').trim().toLowerCase(),
54
72
  visionFilter: normalizeBoolean(options['vision-filter']),
55
73
  visualClaims: normalizeBoolean(options['visual-claims']),
74
+ pointerFirst: normalizeBoolean(options['pointer-first']) || normalizeBoolean(options['proxy-pointer']),
56
75
  };
57
76
  }
58
77
 
@@ -72,6 +91,14 @@ function gateApplicability(template, options) {
72
91
  return false;
73
92
  }
74
93
 
94
+ function hasExtractionSprawl(options) {
95
+ const extractedFacts = options.extractedEntities + options.extractedRelations;
96
+ if (extractedFacts === 0) return false;
97
+ if (options.pointerFirst) return true;
98
+ if (options.sourcePointers.length === 0) return true;
99
+ return extractedFacts > options.sourcePointers.length * Math.max(2, options.promotionThreshold);
100
+ }
101
+
75
102
  function buildSignalSummary(options) {
76
103
  const signals = [];
77
104
  if (options.treePath || options.sectionIds.length > 0) {
@@ -110,6 +137,19 @@ function buildSignalSummary(options) {
110
137
  risk: 'answers that describe image content may need a vision-model sanity check',
111
138
  });
112
139
  }
140
+ if (hasExtractionSprawl(options)) {
141
+ signals.push({
142
+ id: 'entity_relation_sprawl',
143
+ label: 'Entity/relation extraction sprawl',
144
+ values: unique([
145
+ `${options.extractedEntities} extracted entities`,
146
+ `${options.extractedRelations} extracted relations`,
147
+ `${options.sourcePointers.length} source pointers`,
148
+ `promotion threshold ${options.promotionThreshold}`,
149
+ ]),
150
+ risk: 'eager graph extraction can create stale aliases, weak edges, and unauditable memory; keep source pointers first and promote relations only after repeated retrieval value',
151
+ });
152
+ }
113
153
  return signals;
114
154
  }
115
155
 
@@ -139,11 +179,12 @@ function buildProxyPointerRagGuardrailsPlan(rawOptions = {}, templatesPath) {
139
179
  templates: recommendedTemplates,
140
180
  nextActions: [
141
181
  'Preserve document hierarchy, section IDs, and image file paths during ingestion.',
182
+ 'Store source pointers before extracting entities or relations; promote a relation only after repeated retrieval value and source verification.',
142
183
  'Pass section-tree and image-pointer metadata into the agent before it answers with visuals.',
143
184
  'Enable the recommended Document RAG Safety templates as pre-action gates.',
144
185
  'Use a vision filter only for high-impact answers that make claims about visual content.',
145
186
  ],
146
- exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --image-pointers=paper-1/figures/fig2.png --documents=paper-1 --visual-claims --json',
187
+ exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --source-pointers=lesson/fb_123,tool/run_456 --extracted-entities=120 --extracted-relations=80 --pointer-first --json',
147
188
  };
148
189
  }
149
190
 
@@ -37,14 +37,6 @@ function publishedCliShellCommand(pkgVersion, commandArgs = [], options = {}) {
37
37
  const escapedArgs = commandArgs.map(shellQuote).join(' ');
38
38
  const fastPath = `[ -x ${shellQuote(runtimeBin)} ] && exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`;
39
39
  const installPath = `mkdir -p ${shellQuote(prefixDir)} && exec npm ${publishedCliArgs(pkgVersion, commandArgs, { prefixDir }).map(shellQuote).join(' ')}`;
40
- if (options.preferInstalled === false) {
41
- const packageSpec = `thumbgate@${pkgVersion}`;
42
- return [
43
- `mkdir -p ${shellQuote(prefixDir)}`,
44
- `npm "install" "--prefix" ${shellQuote(prefixDir)} "--no-save" "--omit=dev" ${shellQuote(packageSpec)} >/dev/null 2>&1`,
45
- `exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`,
46
- ].join(' && ');
47
- }
48
40
  return `${fastPath} || ${installPath}`;
49
41
  }
50
42
 
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+
6
+ const RUNTIME_PATTERNS = [
7
+ { pattern: /^public\/.*\.(html|css|js)$/i, surface: 'browser', reason: 'public UI asset changed' },
8
+ { pattern: /^src\/api\//i, surface: 'api', reason: 'API route or server behavior changed' },
9
+ { pattern: /^bin\//i, surface: 'cli', reason: 'CLI entrypoint changed' },
10
+ { pattern: /^scripts\/(dashboard|pro-local-dashboard|.*gate|.*scanner|.*reward|.*routing).*\.js$/i, surface: 'agent-runtime', reason: 'agent runtime or gate behavior changed' },
11
+ { pattern: /^adapters\//i, surface: 'agent-adapter', reason: 'agent adapter changed' },
12
+ { pattern: /^plugins\//i, surface: 'plugin', reason: 'plugin install path changed' },
13
+ { pattern: /^package\.json$/i, surface: 'package', reason: 'package manifest changed' },
14
+ ];
15
+
16
+ const SKIP_PATTERNS = [
17
+ /^README\.md$/i,
18
+ /^docs\//i,
19
+ /^reports\//i,
20
+ /^proof\//i,
21
+ /^tests\/.*\.test\.js$/i,
22
+ /^\.claude\/implementation-notes\//i,
23
+ ];
24
+
25
+ function normalizeFiles(files = []) {
26
+ return Array.from(new Set(files
27
+ .map((file) => String(file || '').trim().replace(/^\.?\//, ''))
28
+ .filter(Boolean)));
29
+ }
30
+
31
+ function classifyFile(file) {
32
+ for (const entry of RUNTIME_PATTERNS) {
33
+ if (entry.pattern.test(file)) return { ...entry, file };
34
+ }
35
+ for (const pattern of SKIP_PATTERNS) {
36
+ if (pattern.test(file)) return { surface: 'skip', reason: 'no runtime impact', file };
37
+ }
38
+ return { surface: 'focused', reason: 'unknown runtime impact; run focused checks', file };
39
+ }
40
+
41
+ function parseChangedFilesFromDiff(diff = '') {
42
+ const files = [];
43
+ for (const line of String(diff || '').split('\n')) {
44
+ const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
45
+ if (match) files.push(match[2]);
46
+ }
47
+ return normalizeFiles(files);
48
+ }
49
+
50
+ function planQaScenario(input = {}) {
51
+ const files = normalizeFiles(input.files || parseChangedFilesFromDiff(input.diff || ''));
52
+ const classifications = files.map(classifyFile);
53
+ const surfaces = Array.from(new Set(classifications.map((entry) => entry.surface)));
54
+ const runtimeChanges = classifications.filter((entry) => entry.surface !== 'skip');
55
+ const skipOnly = files.length > 0 && runtimeChanges.length === 0;
56
+
57
+ const recommendedRunner = chooseRunner(surfaces, input);
58
+ const userScenario = buildUserScenario(runtimeChanges, input);
59
+ return {
60
+ name: 'thumbgate-user-impact-qa-scenario',
61
+ status: skipOnly ? 'skip' : 'actionable',
62
+ files,
63
+ classifications,
64
+ recommendedRunner,
65
+ userScenario,
66
+ commands: buildCommands(recommendedRunner, runtimeChanges),
67
+ regressionPolicy: skipOnly
68
+ ? 'skip durable QA; no runtime-impact files changed'
69
+ : 'if the QA agent finds a deterministic failure, convert it into a focused regression test before opening a fix PR',
70
+ transientFailurePolicy: 'doctor the browser/computer-use runner once, retry once, then label as infrastructure-flaky instead of product-regression',
71
+ };
72
+ }
73
+
74
+ function chooseRunner(surfaces, input = {}) {
75
+ if (input.forceComputerUse || surfaces.includes('plugin') || surfaces.includes('agent-adapter')) return 'computer-use-qa';
76
+ if (surfaces.includes('browser') || surfaces.includes('api')) return 'browser-qa';
77
+ if (surfaces.includes('cli') || surfaces.includes('agent-runtime') || surfaces.includes('package')) return 'focused-node-qa';
78
+ if (surfaces.every((surface) => surface === 'skip')) return 'skip';
79
+ return 'focused-node-qa';
80
+ }
81
+
82
+ function buildUserScenario(runtimeChanges, input = {}) {
83
+ if (runtimeChanges.length === 0) return 'No user-impact scenario required; changed files are docs, tests, reports, or proof artifacts only.';
84
+ const surfaces = Array.from(new Set(runtimeChanges.map((entry) => entry.surface)));
85
+ if (surfaces.includes('browser') || surfaces.includes('api')) {
86
+ return 'Open the affected page as a user, perform the primary CTA or dashboard action, verify visible state changes, then check the related API response.';
87
+ }
88
+ if (surfaces.includes('plugin') || surfaces.includes('agent-adapter')) {
89
+ return 'Install or reload the affected agent integration, run one thumbs-up and one thumbs-down capture, then verify the next risky action is gated.';
90
+ }
91
+ if (surfaces.includes('cli')) {
92
+ return 'Run the changed CLI command with --help and one realistic command path, then verify exit code, JSON output, and no stale command copy.';
93
+ }
94
+ return input.scenario || 'Run the focused test for the changed runtime surface, then verify the behavior with one realistic operator workflow.';
95
+ }
96
+
97
+ function buildCommands(runner, runtimeChanges) {
98
+ if (runner === 'skip') return [];
99
+ const commands = ['npm test -- --test-concurrency=1'];
100
+ if (runner === 'browser-qa') commands.push('npx playwright test tests/e2e --project=chromium');
101
+ if (runner === 'computer-use-qa') commands.push('node scripts/qa-scenario-planner.js --doctor-runner');
102
+ if (runtimeChanges.some((entry) => entry.surface === 'package')) commands.push('npm pack --dry-run');
103
+ return commands;
104
+ }
105
+
106
+ function parseArgs(argv = process.argv.slice(2)) {
107
+ const args = {};
108
+ for (const arg of argv) {
109
+ if (arg === '--json') args.json = true;
110
+ else if (arg === '--doctor-runner') args.doctorRunner = true;
111
+ else if (arg.startsWith('--files=')) args.files = arg.slice('--files='.length).split(',');
112
+ else if (arg.startsWith('--diff-file=')) args.diff = fs.readFileSync(arg.slice('--diff-file='.length), 'utf8');
113
+ else if (arg.startsWith('--scenario=')) args.scenario = arg.slice('--scenario='.length);
114
+ }
115
+ return args;
116
+ }
117
+
118
+ if (require.main === module) {
119
+ const args = parseArgs();
120
+ if (args.doctorRunner) {
121
+ console.log('QA runner doctor: verify browser/computer-use target, screenshot capture, and network reachability before blaming product code.');
122
+ process.exit(0);
123
+ }
124
+ const report = planQaScenario(args);
125
+ if (args.json) console.log(JSON.stringify(report, null, 2));
126
+ else {
127
+ console.log(`${report.status.toUpperCase()}: ${report.userScenario}`);
128
+ for (const command of report.commands) console.log(`- ${command}`);
129
+ }
130
+ }
131
+
132
+ module.exports = {
133
+ classifyFile,
134
+ parseChangedFilesFromDiff,
135
+ planQaScenario,
136
+ };
@@ -6,42 +6,78 @@ const path = require('path');
6
6
  const {
7
7
  PRO_MONTHLY_PAYMENT_LINK,
8
8
  PRO_PRICE_LABEL,
9
- TEAM_PRICE_LABEL,
9
+ ENTERPRISE_PRICE_LABEL,
10
10
  } = require('./commercial-offer');
11
11
 
12
12
  const USAGE_FILE = path.join(process.env.HOME || '/tmp', '.thumbgate', 'usage-limits.json');
13
13
 
14
14
  // ──────────────────────────────────────────────────────────
15
- // Free tier: generous on captures (habit formation) and rules
16
- // (5 active gates), gated on Pro-only features (recall, search,
17
- // exports). Dashboard, exports, and unlimited rules drive Pro.
15
+ // Free tier: tight enough to create upgrade pressure after
16
+ // real usage. Captures and rules are capped so heavy users
17
+ // hit the wall within the first week, not the first quarter.
18
18
  // ──────────────────────────────────────────────────────────
19
19
  const FREE_TIER_LIMITS = {
20
- capture_feedback: { daily: Infinity, lifetime: Infinity, label: 'feedback captures' },
21
- prevention_rules: { daily: Infinity, lifetime: Infinity, label: 'prevention rules generated' },
20
+ capture_feedback: { daily: 2, lifetime: 10, label: 'feedback captures (2/day, 10 total on free)' },
21
+ prevention_rules: { daily: 2, lifetime: 3, label: 'prevention rules generated (2/day on free)' },
22
22
  recall: { daily: 0, lifetime: 0, label: 'recall queries (Pro only)' },
23
23
  search_lessons: { daily: 0, lifetime: 0, label: 'lesson searches (Pro only)' },
24
24
  search_thumbgate: { daily: 0, lifetime: 0, label: 'ThumbGate searches (Pro only)' },
25
25
  commerce_recall: { daily: 0, lifetime: 0, label: 'commerce recalls (Pro only)' },
26
26
  export_dpo: { daily: 0, lifetime: 0, label: 'DPO exports (Pro only)' },
27
27
  export_databricks: { daily: 0, lifetime: 0, label: 'Databricks exports (Pro only)' },
28
- construct_context_pack: { daily: Infinity, lifetime: Infinity, label: 'context packs' },
28
+ construct_context_pack: { daily: 3, lifetime: Infinity, label: 'context packs (3/day on free)' },
29
29
  };
30
30
 
31
- const FREE_TIER_MAX_GATES = 5; // 5 active prevention rules on free; Pro is unlimited
31
+ const FREE_TIER_MAX_GATES = 3; // 3 active prevention rules on free; Pro is unlimited
32
+ const FREE_TIER_DAILY_BLOCKS = 2; // 3 gate blocks/day on free; after limit, deny → warn + upgrade CTA
32
33
 
33
- const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
34
+ const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
34
35
 
35
36
  const PAYWALL_MESSAGES = {
36
- prevention_rules: 'Free tier includes 5 active prevention rules. Promote more or unlock unlimited rules with Pro.',
37
+ capture_feedback: 'Free tier: 2 captures/day (10 total). Your feedback is stored locally upgrade to capture unlimited.',
38
+ prevention_rules: 'Free tier includes 3 active prevention rules and 2 rule generations/day. Upgrade to Pro for unlimited rules.',
37
39
  recall: 'Recall is a Pro feature. Your past feedback is stored locally — upgrade to search and reuse it.',
38
40
  search_lessons: 'Lesson search is a Pro feature. Upgrade to find patterns in your agent\'s mistakes.',
41
+ construct_context_pack: 'Free tier: 3 context packs/day. Upgrade to Pro for unlimited.',
39
42
  default: 'This feature requires Pro. Start Pro — card required; billed today.',
40
43
  };
41
44
 
45
+ const TRIAL_DAYS = 7;
46
+
47
+ function getInstallAgeDays() {
48
+ try {
49
+ const { INSTALL_ID_PATH } = require('./cli-telemetry');
50
+ if (!fs.existsSync(INSTALL_ID_PATH)) return null;
51
+ // Use mtimeMs — birthtimeMs is unreliable on Linux (ext4 doesn't backdate creation time).
52
+ // The install-id file is written once at install, so mtime == creation time in practice.
53
+ const stat = fs.statSync(INSTALL_ID_PATH);
54
+ const created = stat.mtimeMs || stat.birthtimeMs;
55
+ if (!Number.isFinite(created) || created <= 0) return null;
56
+ return (Date.now() - created) / (1000 * 60 * 60 * 24);
57
+ } catch (_) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function isInTrialPeriod() {
63
+ if (process.env.CI || process.env.GITHUB_ACTIONS) return false;
64
+ if (process.env.THUMBGATE_NO_TRIAL === '1') return false;
65
+ const age = getInstallAgeDays();
66
+ if (age === null) return false;
67
+ if (age < 0.0007) return false; // <1 minute old — just-created install, not a real trial
68
+ return age < TRIAL_DAYS;
69
+ }
70
+
71
+ function trialDaysRemaining() {
72
+ const age = getInstallAgeDays();
73
+ if (age === null) return 0;
74
+ return Math.max(0, Math.ceil(TRIAL_DAYS - age));
75
+ }
76
+
42
77
  function isProTier(authContext) {
43
78
  if (authContext && authContext.tier === 'pro') return true;
44
- if (process.env.THUMBGATE_API_KEY || process.env.THUMBGATE_PRO_MODE === '1' || process.env.THUMBGATE_NO_RATE_LIMIT === '1') return true;
79
+ if (process.env.THUMBGATE_API_KEY) return true;
80
+ if (process.env.THUMBGATE_NO_RATE_LIMIT === '1') return true;
45
81
  // Creator/dogfooding bypass: when the owner has the dev secret + bypass
46
82
  // configured (env or ~/.config/thumbgate/dev.json), treat the install as Pro
47
83
  // so marketing nudges and rate limits stop firing on the maintainer's own
@@ -57,6 +93,9 @@ function isProTier(authContext) {
57
93
  const { isProLicensed } = require('./license');
58
94
  if (isProLicensed()) return true;
59
95
  } catch (_) {}
96
+ // 7-day reverse trial: new installs get full Pro access, then hit a clear
97
+ // hosted-sync/unlimited-rules pay moment while the product is still fresh.
98
+ if (isInTrialPeriod()) return true;
60
99
  return false;
61
100
  }
62
101
 
@@ -114,12 +153,18 @@ function checkLimit(action, authContext) {
114
153
  const dailyCurrent = usage.counts[action] || 0;
115
154
  const lifetimeCurrent = usage.lifetime[action] || 0;
116
155
 
156
+ const isCapture = action === 'capture_feedback';
157
+ const monthlyLink = isCapture
158
+ ? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
159
+ : PRO_MONTHLY_PAYMENT_LINK;
160
+ const upgradeMessage = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${monthlyLink}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
161
+
117
162
  // Check lifetime limit first (the hard wall)
118
163
  if (lifetimeLimit !== Infinity && lifetimeCurrent >= lifetimeLimit) {
119
164
  const paywallMsg = PAYWALL_MESSAGES[action] || PAYWALL_MESSAGES.default;
120
165
  return {
121
166
  allowed: false,
122
- message: `${paywallMsg}\n\n${UPGRADE_MESSAGE}`,
167
+ message: `${paywallMsg}\n\n${upgradeMessage}`,
123
168
  used: lifetimeCurrent,
124
169
  limit: lifetimeLimit,
125
170
  limitType: 'lifetime',
@@ -128,9 +173,10 @@ function checkLimit(action, authContext) {
128
173
 
129
174
  // Check daily limit
130
175
  if (dailyLimit !== Infinity && dailyCurrent >= dailyLimit) {
176
+ const paywallMsg = PAYWALL_MESSAGES[action] || PAYWALL_MESSAGES.default;
131
177
  return {
132
178
  allowed: false,
133
- message: `Daily limit reached. ${UPGRADE_MESSAGE}`,
179
+ message: `Daily limit reached. ${paywallMsg}\n\n${upgradeMessage}`,
134
180
  used: dailyCurrent,
135
181
  limit: dailyLimit,
136
182
  limitType: 'daily',
@@ -179,12 +225,17 @@ function getUsage(action, authContext) {
179
225
  module.exports = {
180
226
  checkLimit,
181
227
  getUsage,
228
+ getInstallAgeDays,
182
229
  isProTier,
230
+ isInTrialPeriod,
231
+ trialDaysRemaining,
183
232
  loadUsage,
184
233
  saveUsage,
185
234
  todayKey,
186
235
  FREE_TIER_LIMITS,
187
236
  FREE_TIER_MAX_GATES,
237
+ FREE_TIER_DAILY_BLOCKS,
238
+ TRIAL_DAYS,
188
239
  UPGRADE_MESSAGE,
189
240
  PAYWALL_MESSAGES,
190
241
  USAGE_FILE,
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // repeat-metric — first-class "repeat-attempts blocked before execution" metric
5
+ //
6
+ // This module exposes data ThumbGate already collects in gate-stats state. It
7
+ // does NOT write to disk; it is a pure function over gates-engine.loadStats().
8
+ //
9
+ // The headline number is stats.recurringBlocks — incremented by recordStat()
10
+ // in gates-engine.js every time the same gate blocks/warns the same sanitized
11
+ // action fingerprint within one session bucket. That is "a pre-action gate fire
12
+ // that stopped a tool call the agent had already been blocked on", rather than
13
+ // merely "the same noisy gate fired again."
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const gatesEngine = require('./gates-engine');
17
+
18
+ /**
19
+ * Derive a per-gate { firstBlocks, repeatBlocks } split from the raw stats.
20
+ *
21
+ * Modern stats record, per session bucket, which sanitized action fingerprints
22
+ * each gate fired on:
23
+ * stats.sessionFiredActions[sessionKey][gateId][fingerprint] === true
24
+ *
25
+ * firstBlocks is the count of distinct first action fingerprints. Legacy stats
26
+ * without fingerprints fall back to the old per-session-gate split.
27
+ *
28
+ * total block+warn events come from stats.byGate[id] (blocked + warned), which
29
+ * recordStat() also maintains. repeatBlocks is clamped to >= 0 to stay robust
30
+ * against partially-written / legacy state.
31
+ *
32
+ * @param {object} stats raw object returned by gates-engine.loadStats()
33
+ * @returns {Object<string,{firstBlocks:number, repeatBlocks:number}>}
34
+ */
35
+ function computeByGateSplit(stats) {
36
+ const byGate = {};
37
+ const sessionFiredActions = (stats && stats.sessionFiredActions) || {};
38
+ const sessionFiredGates = (stats && stats.sessionFiredGates) || {};
39
+ const rawByGate = (stats && stats.byGate) || {};
40
+
41
+ // Count distinct action fingerprints each gate fired on => firstBlocks.
42
+ const firstBlocksByGate = {};
43
+ const gatesWithActionStats = new Set();
44
+ for (const sessionKey of Object.keys(sessionFiredActions)) {
45
+ const fired = sessionFiredActions[sessionKey] || {};
46
+ for (const gateId of Object.keys(fired)) {
47
+ const fingerprints = fired[gateId] || {};
48
+ const count = Object.values(fingerprints).filter(Boolean).length;
49
+ if (count > 0) {
50
+ gatesWithActionStats.add(gateId);
51
+ firstBlocksByGate[gateId] = (firstBlocksByGate[gateId] || 0) + count;
52
+ }
53
+ }
54
+ }
55
+
56
+ // Legacy fallback: old stats only tracked gate fired per session bucket.
57
+ for (const sessionKey of Object.keys(sessionFiredGates)) {
58
+ const fired = sessionFiredGates[sessionKey] || {};
59
+ for (const gateId of Object.keys(fired)) {
60
+ if (fired[gateId] && !gatesWithActionStats.has(gateId)) {
61
+ firstBlocksByGate[gateId] = (firstBlocksByGate[gateId] || 0) + 1;
62
+ }
63
+ }
64
+ }
65
+
66
+ // Union of every gate id we know about from either source.
67
+ const gateIds = new Set([
68
+ ...Object.keys(rawByGate),
69
+ ...Object.keys(firstBlocksByGate),
70
+ ...Object.keys(sessionFiredActions).flatMap((sessionKey) => Object.keys(sessionFiredActions[sessionKey] || {})),
71
+ ]);
72
+
73
+ for (const gateId of gateIds) {
74
+ const gateStat = rawByGate[gateId] || {};
75
+ const totalFires = (gateStat.blocked || 0) + (gateStat.warned || 0);
76
+ const firstBlocks = firstBlocksByGate[gateId] || 0;
77
+ // Repeat fires are total fires beyond the first fire per session bucket.
78
+ const repeatBlocks = Math.max(0, totalFires - firstBlocks);
79
+ byGate[gateId] = { firstBlocks, repeatBlocks };
80
+ }
81
+
82
+ return byGate;
83
+ }
84
+
85
+ /**
86
+ * Compute the repeat-attempts-blocked-before-execution metric.
87
+ *
88
+ * Pure read of gates-engine.loadStats(); no disk writes.
89
+ *
90
+ * @returns {{
91
+ * repeatBlocksBeforeExecution: number,
92
+ * recurringBlocks: number,
93
+ * totalBlocked: number,
94
+ * byGate: Object<string,{firstBlocks:number, repeatBlocks:number}>
95
+ * }}
96
+ */
97
+ function computeRepeatMetric() {
98
+ let stats;
99
+ try {
100
+ stats = gatesEngine.loadStats() || {};
101
+ } catch (_) {
102
+ stats = {};
103
+ }
104
+
105
+ const recurringBlocks = Number(stats.recurringBlocks || 0);
106
+ const totalBlocked = Number(stats.blocked || 0);
107
+
108
+ return {
109
+ // Headline: a pre-action block that stopped a tool call the agent had
110
+ // already been blocked on this session.
111
+ repeatBlocksBeforeExecution: recurringBlocks,
112
+ recurringBlocks,
113
+ totalBlocked,
114
+ byGate: computeByGateSplit(stats),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Add a `repeat` sub-key to a gate-stats object WITHOUT mutating the original.
120
+ *
121
+ * Takes the object returned by gate-stats.calculateStats() or
122
+ * dashboard.computeGateStats() and returns a shallow copy with the repeat
123
+ * metric attached. The caller's file does not need to import any internals.
124
+ *
125
+ * @param {object} gateStatsObject
126
+ * @returns {object} copy of gateStatsObject with `.repeat`
127
+ */
128
+ function mergeRepeatMetricIntoGateStats(gateStatsObject) {
129
+ const base = gateStatsObject && typeof gateStatsObject === 'object' ? gateStatsObject : {};
130
+ return Object.assign({}, base, { repeat: computeRepeatMetric() });
131
+ }
132
+
133
+ module.exports = {
134
+ computeRepeatMetric,
135
+ mergeRepeatMetricIntoGateStats,
136
+ computeByGateSplit,
137
+ };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const FIXTURE_TOKENS = {
4
+ awsAccessKeyId: '__TG_FIXTURE_AWS_ACCESS_KEY_ID__',
5
+ githubPat: '__TG_FIXTURE_GITHUB_PAT__',
6
+ openAiLegacyKey: '__TG_FIXTURE_OPENAI_LEGACY_KEY__',
7
+ openAiProjectKey: '__TG_FIXTURE_OPENAI_PROJECT_KEY__',
8
+ rsaPrivateKeyHeader: '__TG_FIXTURE_RSA_PRIVATE_KEY_HEADER__',
9
+ ecPrivateKeyHeader: '__TG_FIXTURE_EC_PRIVATE_KEY_HEADER__',
10
+ privateKeyHeader: '__TG_FIXTURE_PRIVATE_KEY_HEADER__',
11
+ };
12
+
13
+ function buildAwsAccessKeyId() {
14
+ return ['AKIA', 'IOSFODNN7EXAMPLE'].join('');
15
+ }
16
+
17
+ function buildGitHubPat() {
18
+ return ['gh', 'p_', 'x'.repeat(36)].join('');
19
+ }
20
+
21
+ function buildOpenAiLegacyKey() {
22
+ return ['sk', '-', 'abcdefghijklmnopqrstuvwxyz01234567890'].join('');
23
+ }
24
+
25
+ function buildOpenAiProjectKey() {
26
+ return ['sk', '-proj-', 'abcdefghijklmnopqrstuvwxyz01234567890'].join('');
27
+ }
28
+
29
+ function buildPemHeader(prefix = '') {
30
+ return ['-----BEGIN ', prefix, 'PRIVATE KEY-----'].join('');
31
+ }
32
+
33
+ function fixtureReplacements() {
34
+ return [
35
+ [FIXTURE_TOKENS.awsAccessKeyId, buildAwsAccessKeyId()],
36
+ [FIXTURE_TOKENS.githubPat, buildGitHubPat()],
37
+ [FIXTURE_TOKENS.openAiLegacyKey, buildOpenAiLegacyKey()],
38
+ [FIXTURE_TOKENS.openAiProjectKey, buildOpenAiProjectKey()],
39
+ [FIXTURE_TOKENS.rsaPrivateKeyHeader, buildPemHeader('RSA ')],
40
+ [FIXTURE_TOKENS.ecPrivateKeyHeader, buildPemHeader('EC ')],
41
+ [FIXTURE_TOKENS.privateKeyHeader, buildPemHeader('')],
42
+ ];
43
+ }
44
+
45
+ function expandFixturePlaceholders(value) {
46
+ let expanded = String(value || '');
47
+ for (const [token, replacement] of fixtureReplacements()) {
48
+ expanded = expanded.split(token).join(replacement);
49
+ }
50
+ return expanded;
51
+ }
52
+
53
+ module.exports = {
54
+ FIXTURE_TOKENS,
55
+ buildAwsAccessKeyId,
56
+ buildGitHubPat,
57
+ buildOpenAiLegacyKey,
58
+ buildOpenAiProjectKey,
59
+ buildPemHeader,
60
+ expandFixturePlaceholders,
61
+ };