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
@@ -36,13 +36,18 @@ const BOT_PATTERNS = [
36
36
  /\bbaiduspider\b/i,
37
37
  /\bduckduckbot\b/i,
38
38
  /\bapplebot\b/i,
39
+ /slurp/i,
40
+ /petalbot/i,
41
+ /mediapartners/i,
39
42
  // LLM / AI crawlers — these started exploding in 2024+
40
43
  /\bgptbot\b/i,
41
- /\bchatgpt-user\b/i,
44
+ /chatgpt/i,
42
45
  /\boai-searchbot\b/i,
43
- /\bperplexitybot\b/i,
44
- /\banthropic-ai\b/i,
46
+ /perplexity/i,
47
+ /anthropic/i,
45
48
  /\bclaude(?:bot|-web)\b/i,
49
+ /claude-searchbot/i,
50
+ /google-extended/i,
46
51
  /\bccbot\b/i,
47
52
  /\bcohere-ai\b/i,
48
53
  /\bbytespider\b/i,
@@ -86,6 +91,8 @@ const BOT_PATTERNS = [
86
91
  /\blibwww-perl\b/i,
87
92
  /\bjava\//i,
88
93
  /\bgo-http-client\b/i,
94
+ /scrapy/i,
95
+ /httpclient/i,
89
96
  /\bruby\b/i,
90
97
  // API/test tools
91
98
  /\bpostman(?:runtime)?\b/i,
@@ -158,8 +165,59 @@ function isProbablyBot(headers) {
158
165
  return classifyRequester(headers).isBot;
159
166
  }
160
167
 
168
+ // --- Visitor classification (analytics filtering) ---------------------------
169
+ // Classifies telemetry/analytics traffic as bot / owner / real_user.
170
+ // Owner traffic is identified via the THUMBGATE_OWNER_EMAILS env var
171
+ // (comma-separated, case-insensitive). There is no built-in default:
172
+ // operators opt in via config, so the shipped artifact carries no
173
+ // personal data.
174
+
175
+ function getOwnerEmails(env = process.env) {
176
+ const raw = env.THUMBGATE_OWNER_EMAILS;
177
+ if (!raw) return [];
178
+ return raw
179
+ .split(',')
180
+ .map((entry) => entry.trim().toLowerCase())
181
+ .filter(Boolean);
182
+ }
183
+
184
+ function classifyVisitor(req) {
185
+ const ua = (req.headers && req.headers['user-agent']) || '';
186
+ const email = req.email || (req.query && req.query.email) || '';
187
+
188
+ for (const pattern of BOT_PATTERNS) {
189
+ if (pattern.test(ua)) {
190
+ return { type: 'bot', reason: `UA matches: ${pattern}`, userAgent: ua };
191
+ }
192
+ }
193
+ if (!ua || ua.length < 10) {
194
+ return { type: 'bot', reason: 'Empty or short user-agent', userAgent: ua };
195
+ }
196
+ if (email) {
197
+ const normalized = email.toLowerCase();
198
+ if (getOwnerEmails().some((ownerEmail) => normalized.includes(ownerEmail))) {
199
+ return { type: 'owner', reason: 'Email matches configured owner list', userAgent: ua };
200
+ }
201
+ }
202
+ return { type: 'real_user', reason: 'No bot pattern matched', userAgent: ua };
203
+ }
204
+
205
+ function shouldExcludeFromAnalytics(req) {
206
+ const classification = req.visitorClass || classifyVisitor(req);
207
+ return classification.type === 'bot';
208
+ }
209
+
210
+ function botFilterMiddleware(req, res, next) {
211
+ req.visitorClass = classifyVisitor(req);
212
+ next();
213
+ }
214
+
161
215
  module.exports = {
162
216
  classifyRequester,
163
217
  isProbablyBot,
218
+ classifyVisitor,
219
+ shouldExcludeFromAnalytics,
220
+ botFilterMiddleware,
221
+ getOwnerEmails,
164
222
  BOT_PATTERNS,
165
223
  };
@@ -5,6 +5,11 @@ 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';
8
13
 
9
14
  function normalizeNullableText(value) {
10
15
  if (typeof value !== 'string') {
@@ -16,35 +21,69 @@ function normalizeNullableText(value) {
16
21
  }
17
22
 
18
23
  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.
19
31
  const resolvedPath =
20
32
  normalizeNullableText(filePath) ||
21
33
  normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
22
34
  DEFAULT_BUILD_METADATA_PATH;
23
35
  const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
36
+ const railwayGitSha = normalizeNullableText(env[RAILWAY_GIT_COMMIT_SHA_ENV_KEY]);
24
37
  const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
25
38
 
26
- if (envBuildSha || envGeneratedAt) {
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) {
27
50
  return {
28
51
  path: resolvedPath,
29
- buildSha: envBuildSha,
30
- generatedAt: envGeneratedAt,
52
+ buildSha: fileBuildSha,
53
+ generatedAt: fileGeneratedAt || envGeneratedAt,
31
54
  };
32
55
  }
33
56
 
34
- try {
35
- const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
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) {
36
64
  return {
37
65
  path: resolvedPath,
38
- buildSha: normalizeNullableText(parsed.buildSha),
39
- generatedAt: normalizeNullableText(parsed.generatedAt),
66
+ buildSha: railwayGitSha,
67
+ generatedAt: envGeneratedAt,
40
68
  };
41
- } catch {
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) {
42
75
  return {
43
76
  path: resolvedPath,
44
- buildSha: null,
45
- generatedAt: null,
77
+ buildSha: envBuildSha,
78
+ generatedAt: envGeneratedAt,
46
79
  };
47
80
  }
81
+
82
+ return {
83
+ path: resolvedPath,
84
+ buildSha: null,
85
+ generatedAt: fileGeneratedAt || envGeneratedAt,
86
+ };
48
87
  }
49
88
 
50
89
  function writeBuildMetadataFile({ sha, outputPath, generatedAt = new Date().toISOString() }) {
@@ -105,6 +144,7 @@ if (require.main === module) {
105
144
  module.exports = {
106
145
  BUILD_GENERATED_AT_ENV_KEY,
107
146
  BUILD_SHA_ENV_KEY,
147
+ RAILWAY_GIT_COMMIT_SHA_ENV_KEY,
108
148
  DEFAULT_BUILD_METADATA_PATH,
109
149
  resolveBuildMetadata,
110
150
  writeBuildMetadataFile,
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', 'config', 'gate-classifier-routing.json');
8
+
9
+ function clamp01(value, fallback = 0) {
10
+ const number = Number(value);
11
+ if (!Number.isFinite(number)) return fallback;
12
+ return Math.min(1, Math.max(0, number));
13
+ }
14
+
15
+ function riskRank(risk) {
16
+ const normalized = String(risk || 'medium').toLowerCase();
17
+ if (['critical', 'block', 'regulated'].includes(normalized)) return 4;
18
+ if (['high', 'dangerous'].includes(normalized)) return 3;
19
+ if (['medium', 'warn'].includes(normalized)) return 2;
20
+ return 1;
21
+ }
22
+
23
+ function loadClassifierRoutingConfig(configPath = DEFAULT_CONFIG_PATH) {
24
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
+ }
26
+
27
+ function lane(config, laneName, reason, input, overrides = {}) {
28
+ const laneConfig = (config.lanes && config.lanes[laneName]) || {};
29
+ return {
30
+ lane: laneName,
31
+ reason,
32
+ requiresEvidence: Boolean(laneConfig.requiresEvidence || overrides.requiresEvidence),
33
+ cloudAllowed: Boolean(laneConfig.cloudAllowed),
34
+ maxLatencyMs: laneConfig.maxLatencyMs,
35
+ risk: String(input.risk || 'medium').toLowerCase(),
36
+ ambiguity: clamp01(input.ambiguity),
37
+ };
38
+ }
39
+
40
+ function routeClassifier(input = {}, config = loadClassifierRoutingConfig()) {
41
+ const thresholds = config.thresholds || {};
42
+ const labelCount = Number(input.labelCount || input.examples || 0);
43
+ const latencyBudgetMs = Number(input.latencyBudgetMs || 0);
44
+ const ambiguity = clamp01(input.ambiguity);
45
+ const risk = riskRank(input.risk);
46
+ const largeBatch = Number(input.batchRows || 0) >= Number(thresholds.largeBatchRows || 50);
47
+ const privacySensitive = Boolean(input.privacySensitive || input.containsSecrets || input.customerData);
48
+ const allowCloud = Boolean(input.allowCloud);
49
+
50
+ if (input.hasHardRule || input.exactPolicyMatch) {
51
+ return lane(config, 'deterministic', 'exact hard rule or policy match; do not spend model tokens', input);
52
+ }
53
+
54
+ if (input.semanticCacheHit || input.equivalentRepeat) {
55
+ return lane(config, 'semantic_cache', 'semantically equivalent repeat; reuse the proven prior decision without a model call', input);
56
+ }
57
+
58
+ if (input.rubricFailed || input.missingEvidence || input.completionClaimWithoutProof) {
59
+ return lane(config, 'rubric_gate', 'rubric or completion evidence failed; block done claims until proof exists', input);
60
+ }
61
+
62
+ if (input.structuredDataset && (input.missingProvenance || input.missingSources)) {
63
+ return lane(config, 'rubric_gate', 'structured data claim is missing source provenance', input);
64
+ }
65
+
66
+ if (privacySensitive && !allowCloud && risk >= 3 && ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
67
+ return lane(config, 'human_review', 'private high-risk ambiguous action; keep data local and require approval', input);
68
+ }
69
+
70
+ if (labelCount >= Number(thresholds.classicalMinExamples || 40) && (largeBatch || latencyBudgetMs <= Number(thresholds.lowLatencyBudgetMs || 300)) && ambiguity < Number(thresholds.mediumAmbiguity || 0.35)) {
71
+ return lane(config, 'local_classical', 'enough examples and low ambiguity; use cheap local classification', input);
72
+ }
73
+
74
+ if (risk >= 3 && ambiguity >= Number(thresholds.highRiskAmbiguity || 0.65)) {
75
+ if (allowCloud && latencyBudgetMs >= Number(thresholds.llmMinLatencyBudgetMs || 2000)) {
76
+ return lane(config, 'llm_judge', 'high-risk semantic ambiguity; use a budget-capped LLM judge with evidence', input);
77
+ }
78
+ return lane(config, 'human_review', 'high-risk ambiguity without approved cloud/budget route', input);
79
+ }
80
+
81
+ if (labelCount < Number(thresholds.classicalMinExamples || 40) || ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
82
+ return lane(config, 'local_semantic', 'sparse labels or fuzzy intent; use local semantic recall before any LLM', input);
83
+ }
84
+
85
+ return lane(config, config.defaultLane || 'local_classical', 'default local route for routine gate classification', input);
86
+ }
87
+
88
+ function explainClassifierRoute(input = {}, config = loadClassifierRoutingConfig()) {
89
+ const decision = routeClassifier(input, config);
90
+ const laneConfig = (config.lanes && config.lanes[decision.lane]) || {};
91
+ return {
92
+ ...decision,
93
+ description: laneConfig.description || '',
94
+ useFor: laneConfig.useFor || [],
95
+ };
96
+ }
97
+
98
+ function parseArgs(argv) {
99
+ const input = {};
100
+ for (const arg of argv) {
101
+ if (arg === '--hard-rule') input.hasHardRule = true;
102
+ else if (arg === '--privacy-sensitive') input.privacySensitive = true;
103
+ else if (arg === '--allow-cloud') input.allowCloud = true;
104
+ else if (arg === '--customer-data') input.customerData = true;
105
+ else if (arg === '--semantic-cache-hit') input.semanticCacheHit = true;
106
+ else if (arg === '--equivalent-repeat') input.equivalentRepeat = true;
107
+ else if (arg === '--rubric-failed') input.rubricFailed = true;
108
+ else if (arg === '--missing-evidence') input.missingEvidence = true;
109
+ else if (arg === '--structured-dataset') input.structuredDataset = true;
110
+ else if (arg === '--missing-provenance') input.missingProvenance = true;
111
+ else if (arg.startsWith('--risk=')) input.risk = arg.slice('--risk='.length);
112
+ else if (arg.startsWith('--ambiguity=')) input.ambiguity = Number(arg.slice('--ambiguity='.length));
113
+ else if (arg.startsWith('--labels=')) input.labelCount = Number(arg.slice('--labels='.length));
114
+ else if (arg.startsWith('--latency-ms=')) input.latencyBudgetMs = Number(arg.slice('--latency-ms='.length));
115
+ else if (arg.startsWith('--batch-rows=')) input.batchRows = Number(arg.slice('--batch-rows='.length));
116
+ }
117
+ return input;
118
+ }
119
+
120
+ module.exports = {
121
+ DEFAULT_CONFIG_PATH,
122
+ explainClassifierRoute,
123
+ loadClassifierRoutingConfig,
124
+ routeClassifier,
125
+ };
126
+
127
+ if (require.main === module) {
128
+ const decision = explainClassifierRoute(parseArgs(process.argv.slice(2)));
129
+ process.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
130
+ }
@@ -88,11 +88,13 @@ function formatCliOutput(result) {
88
88
  if (result.feedbackResult && result.feedbackResult.accepted !== false) {
89
89
  lines.push(`${isDown ? R : G}${BD}${isDown ? '👎 Thumbs down recorded' : '👍 Thumbs up recorded'}${RST}`);
90
90
  const feedbackId = (result.feedbackResult.feedbackEvent && result.feedbackResult.feedbackEvent.id) || result.feedbackResult.id;
91
+ const memoryId = (result.feedbackResult.memoryRecord && result.feedbackResult.memoryRecord.id) || result.feedbackResult.memoryId;
91
92
  if (feedbackId) {
92
- lines.push(`${D} ID: ${feedbackId}${RST}`);
93
+ lines.push(`${D} Feedback ID: ${feedbackId}${RST}`);
94
+ if (memoryId) lines.push(`${D} Memory ID : ${memoryId}${RST}`);
93
95
  // Echo feedback ID to stderr so it's visible directly in the terminal,
94
96
  // not hidden behind Claude Code's "ctrl+o to expand" MCP call collapse.
95
- process.stderr.write(`✅ Feedback captured (${feedbackId})\n`);
97
+ process.stderr.write(`✅ Feedback captured (${feedbackId}${memoryId ? `, ${memoryId}` : ''})\n`);
96
98
  }
97
99
  } else {
98
100
  lines.push(`${R}Feedback not accepted: ${(result.feedbackResult && result.feedbackResult.reason) || 'unknown'}${RST}`);
@@ -51,6 +51,20 @@ const CLI_COMMANDS = [
51
51
  { name: 'json', type: 'boolean', description: 'Output as JSON' },
52
52
  ],
53
53
  },
54
+ {
55
+ name: 'feedback-self-test',
56
+ aliases: ['dogfood'],
57
+ description: 'Prove thumbs feedback capture works in the current runtime',
58
+ group: 'capture',
59
+ mcpTool: 'capture_feedback',
60
+ flags: [
61
+ { name: 'feedback', type: 'string', description: 'Signal to test: up or down (default down)' },
62
+ { name: 'context', type: 'string', description: 'Context to store in the test capture' },
63
+ { name: 'persist', type: 'boolean', description: 'Use the active ThumbGate store instead of an isolated test store' },
64
+ { name: 'feedback-dir', type: 'string', description: 'Explicit feedback directory for the self-test' },
65
+ { name: 'json', type: 'boolean', description: 'Output as JSON' },
66
+ ],
67
+ },
54
68
 
55
69
  // -------------------------------------------------------------------------
56
70
  // Discovery
@@ -80,6 +94,25 @@ const CLI_COMMANDS = [
80
94
  { name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
81
95
  ],
82
96
  },
97
+ {
98
+ name: 'brain',
99
+ aliases: ['customer-brain', 'repo-brain'],
100
+ description: 'Scaffold, query, or build the governed customer/repo context brain',
101
+ group: 'discovery',
102
+ flags: [
103
+ { name: 'json', type: 'boolean', description: 'Output as JSON' },
104
+ { name: 'task', type: 'string', description: 'Task description for routed context loading' },
105
+ { name: 'type', type: 'string', description: 'Memory type for remember: decision | pattern | feedback | log' },
106
+ { name: 'title', type: 'string', description: 'Title for a sourced memory entry' },
107
+ { name: 'content', type: 'string', description: 'Body for a sourced memory entry' },
108
+ { name: 'source', type: 'string', description: 'Required provenance for factual memory writes' },
109
+ { name: 'tags', type: 'string', description: 'Comma-separated memory tags' },
110
+ { name: 'text', type: 'string', description: 'Text/action to check against never-do rules' },
111
+ { name: 'stale-days', type: 'number', description: 'Age threshold for cleanup report (default 60)' },
112
+ { name: 'write', type: 'boolean', description: 'Save to .thumbgate/BRAIN.md (versioned, deterministic)' },
113
+ { name: 'limit', type: 'number', description: 'Max lessons to include (default 15)' },
114
+ ],
115
+ },
83
116
  {
84
117
  name: 'stats',
85
118
  description: 'Feedback analytics — approval rate, Revenue-at-Risk, recent trend',
@@ -337,6 +370,19 @@ const CLI_COMMANDS = [
337
370
  { name: 'high-risk-workflows', type: 'string', description: 'Comma-separated workflows touching money, prod, secrets, data, or publishing' },
338
371
  ],
339
372
  }),
373
+ discoveryCommand({
374
+ name: 'ai-inventory',
375
+ aliases: ['ai-component-inventory', 'ml-bom', 'mlbom'],
376
+ description: 'Scan AI/ML components and export enterprise ML-BOM evidence',
377
+ mcpTool: 'ai_component_inventory',
378
+ flags: [
379
+ jsonFlag(),
380
+ { name: 'root', type: 'string', description: 'Project root to scan' },
381
+ { name: 'format', type: 'string', description: 'summary, json, or cyclonedx' },
382
+ { name: 'output', type: 'string', description: 'Write evidence to this path' },
383
+ { name: 'max-files', type: 'number', description: 'Maximum files to scan' },
384
+ ],
385
+ }),
340
386
  discoveryCommand({
341
387
  name: 'long-running-agent-context-guardrails',
342
388
  aliases: ['agent-context-guardrails', 'slack-context-guardrails'],
@@ -459,6 +505,12 @@ const CLI_COMMANDS = [
459
505
  group: 'gates',
460
506
  flags: [],
461
507
  },
508
+ {
509
+ name: 'hermes-gate',
510
+ description: 'Hermes Agent pre_tool_call hook: gate runtime tool calls (incl. skill_manage) before they run',
511
+ group: 'gates',
512
+ flags: [],
513
+ },
462
514
  {
463
515
  name: 'force-gate',
464
516
  description: 'Immediately create a blocking gate from a pattern string',
@@ -525,6 +577,14 @@ const CLI_COMMANDS = [
525
577
  { name: 'json', type: 'boolean', description: 'Output as JSON' },
526
578
  ],
527
579
  },
580
+ {
581
+ name: 'audit',
582
+ description: 'Audit an agent transcript for repeat-mistake patterns and estimated token waste',
583
+ group: 'ops',
584
+ flags: [
585
+ { name: 'file', type: 'string', description: 'Path to the agent transcript to audit' },
586
+ ],
587
+ },
528
588
  {
529
589
  name: 'init',
530
590
  description: 'Scaffold .thumbgate/ config and wire agent hooks',
@@ -582,6 +642,43 @@ const CLI_COMMANDS = [
582
642
  { name: 'info', type: 'boolean', description: 'Show Pro feature list' },
583
643
  ],
584
644
  },
645
+ {
646
+ name: 'diagnostic',
647
+ aliases: ['workflow-diagnostic', 'sprint-diagnostic'],
648
+ description: '$499 Workflow Hardening Diagnostic for one repeated AI-agent workflow failure',
649
+ group: 'ops',
650
+ flags: [],
651
+ },
652
+
653
+ {
654
+ name: 'workflow',
655
+ aliases: ['swarm'],
656
+ description: 'Execute a dynamic parallel workflow for security audit, benchmarking, or exploration',
657
+ group: 'ops',
658
+ mcpTool: 'parallel_workflow',
659
+ flags: [
660
+ { name: 'objective', type: 'string', required: true, description: 'The objective to plan and execute (e.g. security audit, performance benchmark)' },
661
+ { name: 'concurrency', type: 'number', description: 'Maximum parallel subtasks (default 3)' },
662
+ { name: 'timeoutMs', type: 'number', description: 'Timeout in milliseconds (default 60000)' },
663
+ { name: 'json', type: 'boolean', description: 'Output results as JSON' },
664
+ ],
665
+ },
666
+ {
667
+ name: 'check-update',
668
+ aliases: ['upgrade-check'],
669
+ description: 'Check for newer versions of ThumbGate from npm or GitHub',
670
+ group: 'ops',
671
+ flags: [
672
+ { name: 'json', type: 'boolean', description: 'Output results as JSON' },
673
+ ],
674
+ },
675
+ {
676
+ name: 'self-update',
677
+ aliases: ['upgrade-cli'],
678
+ description: 'Automatically install the latest version of ThumbGate globally',
679
+ group: 'ops',
680
+ flags: [],
681
+ },
585
682
  ];
586
683
 
587
684
  /**
@@ -10,6 +10,9 @@ const _DEFAULT_TELEMETRY_HOST = 'https://thumbgate-production.up.railway.app';
10
10
  const TELEMETRY_ENDPOINT = `${process.env.THUMBGATE_API_URL || _DEFAULT_TELEMETRY_HOST}/v1/telemetry/ping`;
11
11
  const INSTALL_ID_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thumbgate', 'install-id');
12
12
 
13
+ // Session ID: random per process invocation. Groups all events from one CLI run.
14
+ const SESSION_ID = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
15
+
13
16
  /**
14
17
  * Get or create a stable anonymous install ID.
15
18
  * This is NOT tied to any personal info — it's a random UUID stored locally.
@@ -61,7 +64,9 @@ function trackEvent(eventType, metadata = {}) {
61
64
  const payload = JSON.stringify({
62
65
  eventType,
63
66
  installId: getInstallId(),
67
+ sessionId: SESSION_ID,
64
68
  visitorType: classifyInstall(),
69
+ clientType: 'cli',
65
70
  platform: os.platform(),
66
71
  arch: os.arch(),
67
72
  nodeVersion: process.version,
@@ -87,4 +92,4 @@ function trackEvent(eventType, metadata = {}) {
87
92
  } catch (_) {} // never crash the CLI
88
93
  }
89
94
 
90
- module.exports = { trackEvent, getInstallId, classifyInstall, INSTALL_ID_PATH };
95
+ module.exports = { trackEvent, getInstallId, classifyInstall, INSTALL_ID_PATH, SESSION_ID };
@@ -14,7 +14,12 @@ const TEAM_ANNUAL_PRICE_DOLLARS = 588;
14
14
  const TEAM_MIN_SEATS = 3;
15
15
 
16
16
  const PRO_PRICE_LABEL = '$19/mo or $149/yr (individual)';
17
- const TEAM_PRICE_LABEL = '$49/seat/mo Agent governance for engineering teams';
17
+ // Enterprise is the contact-sales tier (absorbs the former Team workflow + the
18
+ // regulated-industry lane). Pricing is scoped after intake — no self-serve seat
19
+ // price is surfaced. The dormant TEAM_* Stripe constants below remain only as
20
+ // inert billing plumbing pending a dedicated cleanup; they are no longer a
21
+ // customer-facing tier.
22
+ const ENTERPRISE_PRICE_LABEL = 'Custom pricing, scoped after intake — Enterprise agent governance';
18
23
 
19
24
  function normalizePlanId(value) {
20
25
  const text = String(value || '').trim().toLowerCase();
@@ -35,6 +40,78 @@ function normalizeSeatCount(value, fallback = TEAM_MIN_SEATS) {
35
40
  return Math.max(TEAM_MIN_SEATS, Math.round(parsed));
36
41
  }
37
42
 
43
+ function trackedProUrl(source = 'cli_receipt', content = 'value_receipt') {
44
+ try {
45
+ const url = new URL(PRO_MONTHLY_PAYMENT_LINK);
46
+ url.searchParams.set('utm_source', source);
47
+ url.searchParams.set('utm_medium', 'cli');
48
+ url.searchParams.set('utm_campaign', 'pro_conversion');
49
+ url.searchParams.set('utm_content', content);
50
+ return url.toString();
51
+ } catch (_) {
52
+ return PRO_MONTHLY_PAYMENT_LINK;
53
+ }
54
+ }
55
+
56
+ function pluralize(count, singular, plural = `${singular}s`) {
57
+ return Number(count) === 1 ? singular : plural;
58
+ }
59
+
60
+ function buildCaptureReceipt({ signal, feedbackId, memoryId, actionType } = {}) {
61
+ const normalizedSignal = String(signal || '').toUpperCase() || 'UNKNOWN';
62
+ const lines = [
63
+ '',
64
+ 'Value receipt',
65
+ '─'.repeat(50),
66
+ ` Stored proof : ${normalizedSignal} feedback${feedbackId ? ` (${feedbackId})` : ''}`,
67
+ memoryId ? ` Local memory : ${memoryId}` : ' Local memory : saved locally',
68
+ actionType ? ` Rule pressure : ${actionType}` : ' Rule pressure : available for promotion',
69
+ ' Free today : this proof protects this local machine',
70
+ ' Pro sync : keep this lesson, rule, and dashboard synced across machines and agent runtimes',
71
+ ' Next proof : npx thumbgate stats',
72
+ ' Cost proof : npx thumbgate cost',
73
+ '',
74
+ ` Solo Pro : ${PRO_PRICE_LABEL} for hosted sync, search, dashboard, and exports`,
75
+ ` Upgrade : ${trackedProUrl('cli_capture_receipt', actionType || normalizedSignal.toLowerCase())}`,
76
+ ` Enterprise : ${ENTERPRISE_PRICE_LABEL}; start with one repeated workflow failure`,
77
+ ' https://thumbgate.ai/#workflow-sprint-intake',
78
+ '',
79
+ ];
80
+ return lines.join('\n');
81
+ }
82
+
83
+ function buildStatsReceipt(stats = {}) {
84
+ const negatives = Number(stats.negatives || stats.totalNegative || 0);
85
+ const blocked = Number(stats.gatesBlocked || stats.blocked || 0);
86
+ const warned = Number(stats.gatesWarned || stats.warned || 0);
87
+ const gates = Number(stats.totalGates || 0);
88
+ const autoPromoted = Number(stats.autoPromotedGates || 0);
89
+ const hasFriction = negatives > 0 || blocked > 0 || warned > 0 || gates > 0;
90
+ if (!hasFriction) return '';
91
+
92
+ const interventions = blocked + warned;
93
+ const lines = [
94
+ '',
95
+ 'Paid-intent next step',
96
+ '─'.repeat(50),
97
+ ];
98
+ if (interventions > 0) {
99
+ lines.push(` Proof already seen : ${interventions} gate ${pluralize(interventions, 'intervention')}`);
100
+ }
101
+ if (gates > 0) {
102
+ lines.push(` Active prevention : ${gates} ${pluralize(gates, 'gate')} (${autoPromoted} auto-promoted)`);
103
+ }
104
+ if (negatives > 0) {
105
+ lines.push(` Failure pressure : ${negatives} negative ${pluralize(negatives, 'signal')}`);
106
+ }
107
+ lines.push(' Show the buyer : npx thumbgate cost');
108
+ lines.push(' Pro sync value : keep these lessons/rules visible across laptops, CI, containers, and agent runtimes');
109
+ lines.push(` Solo Pro : ${trackedProUrl('cli_stats_receipt', 'proof_seen')}`);
110
+ lines.push(' Enterprise : https://thumbgate.ai/#workflow-sprint-intake');
111
+ lines.push('');
112
+ return lines.join('\n');
113
+ }
114
+
38
115
  module.exports = {
39
116
  PRO_MONTHLY_PAYMENT_LINK,
40
117
  PRO_ANNUAL_PAYMENT_LINK,
@@ -47,8 +124,11 @@ module.exports = {
47
124
  TEAM_ANNUAL_PRICE_DOLLARS,
48
125
  TEAM_MIN_SEATS,
49
126
  PRO_PRICE_LABEL,
50
- TEAM_PRICE_LABEL,
127
+ ENTERPRISE_PRICE_LABEL,
51
128
  normalizePlanId,
52
129
  normalizeBillingCycle,
53
130
  normalizeSeatCount,
131
+ buildCaptureReceipt,
132
+ buildStatsReceipt,
133
+ trackedProUrl,
54
134
  };