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
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
4
7
  /**
5
8
  * Context Manager — Unified Context-Augmented Generation (CAG) Orchestrator
6
9
  *
@@ -27,8 +30,9 @@ const {
27
30
  recordProvenance,
28
31
  } = require('./contextfs');
29
32
  const { loadOptionalModule } = require('./private-core-boundary');
30
- const { retrieveRelevantLessons } = loadOptionalModule('./lesson-retrieval', () => ({
33
+ const { retrieveRelevantLessons, calculateRetrievalEntropy } = loadOptionalModule('./lesson-retrieval', () => ({
31
34
  retrieveRelevantLessons: () => [],
35
+ calculateRetrievalEntropy: () => 0,
32
36
  }));
33
37
  const { evaluatePretool } = require('./hybrid-feedback-context');
34
38
  const { loadProfile } = require('./user-profile');
@@ -93,13 +97,21 @@ function assembleSession() {
93
97
 
94
98
  function assembleLessons(query, agentProfile, options = {}) {
95
99
  try {
96
- return retrieveRelevantLessons(
100
+ const lessons = retrieveRelevantLessons(
97
101
  options.toolName || '',
98
102
  query,
99
103
  { maxResults: agentProfile.maxLessons, feedbackDir: options.feedbackDir },
100
104
  );
105
+
106
+ const entropy = calculateRetrievalEntropy(lessons);
107
+
108
+ return {
109
+ items: lessons,
110
+ entropy,
111
+ highConflict: entropy > 0.7
112
+ };
101
113
  } catch {
102
- return [];
114
+ return { items: [], entropy: 0, highConflict: false };
103
115
  }
104
116
  }
105
117
 
@@ -111,13 +123,41 @@ function assembleGuards(toolName, toolInput) {
111
123
  }
112
124
  }
113
125
 
114
- function assembleContextPack(query, agentProfile) {
126
+ function assembleContextPack(query, agentProfile, options = {}) {
127
+ const { guards } = options;
115
128
  try {
116
129
  ensureContextFs();
130
+
131
+ // 1. Proactive Governance: Filter what the agent sees based on prevention rules
132
+ let structuredQuery = query;
133
+ if (guards && guards.mode === 'block') {
134
+ structuredQuery = `${query} (Active Block Policy: ${guards.reason})`;
135
+ }
136
+
137
+ // 2. Elevate Thompson Sampling to Architecture Level
138
+ let strategy = null;
139
+ try {
140
+ const ts = require('./thompson-sampling');
141
+ const model = ts.loadModel();
142
+ const bestCategory = ts.argmaxPosteriors(model);
143
+
144
+ // Route between context-building strategies based on TS posterior mean
145
+ if (bestCategory === 'architecture' || bestCategory === 'infra') {
146
+ strategy = 'hierarchical';
147
+ } else if (bestCategory === 'observability' || bestCategory === 'debugging') {
148
+ strategy = 'summarize-then-expand';
149
+ } else {
150
+ strategy = 'semantic';
151
+ }
152
+ } catch (e) {
153
+ // Fallback to default routing
154
+ }
155
+
117
156
  return constructContextPack({
118
- query,
157
+ query: structuredQuery,
119
158
  maxItems: Math.min(8, Math.ceil(agentProfile.contextBudget / 1000)),
120
159
  maxChars: agentProfile.contextBudget,
160
+ strategy
121
161
  });
122
162
  } catch {
123
163
  return null;
@@ -196,7 +236,8 @@ function assembleUnifiedContext(params = {}) {
196
236
  // Assemble all components — each is fault-tolerant
197
237
  const session = assembleSession();
198
238
  const userProfile = assembleUserProfile();
199
- const lessons = assembleLessons(query, agentProfile, { toolName, feedbackDir });
239
+ const lessonData = assembleLessons(query, agentProfile, { toolName, feedbackDir });
240
+ const lessons = lessonData.items;
200
241
  const guards = assembleGuards(toolName, toolInput);
201
242
  const contextPack = assembleContextPack(query, agentProfile);
202
243
  const codeGraph = assembleCodeGraph(query, repoPath, agentProfile);
@@ -204,9 +245,24 @@ function assembleUnifiedContext(params = {}) {
204
245
  const components = { session, userProfile, lessons, guards, contextPack, codeGraph };
205
246
  const tier = classifyTier(components);
206
247
 
248
+ // v4.2: Entropy-Aware Reliability Directive
249
+ let reliabilityDirective = null;
250
+ if (lessonData.highConflict) {
251
+ reliabilityDirective = 'CAUTION: Conflicting past patterns detected for this action. Prioritize absolute ground truth verification over rapid completion.';
252
+ }
253
+
254
+ // v1.26.0: CodeRabbit Planning Directive
255
+ const planPath = path.join(repoPath || process.cwd(), 'PLAN.md');
256
+ if (!fs.existsSync(planPath) && ['Bash', 'Write', 'Edit', 'Deploy'].includes(toolName)) {
257
+ const planReminder = 'ORCHESTRATION: High-risk action detected without a PLAN.md. Please document your intent, assumptions, and verification steps before proceeding.';
258
+ reliabilityDirective = reliabilityDirective ? `${reliabilityDirective}\n\n${planReminder}` : planReminder;
259
+ }
260
+
207
261
  const result = {
208
262
  tier,
209
263
  agentType: agentType || 'default',
264
+ reliabilityDirective,
265
+ entropy: lessonData.entropy,
210
266
  agentProfile: {
211
267
  maxLessons: agentProfile.maxLessons,
212
268
  contextBudget: agentProfile.contextBudget,
@@ -228,6 +284,12 @@ function assembleUnifiedContext(params = {}) {
228
284
  })),
229
285
  visibility: contextPack.visibility || null,
230
286
  cached: !!(contextPack.cache && contextPack.cache.hit),
287
+ layers: {
288
+ localState: session || null,
289
+ graphState: codeGraph || null,
290
+ policyState: guards || null,
291
+ sessionState: contextPack.items ? contextPack.items.filter(i => i.namespace === 'session') : []
292
+ }
231
293
  } : null,
232
294
  codeGraph: codeGraph || null,
233
295
  assembledAt: new Date().toISOString(),
@@ -306,6 +368,12 @@ function formatUnifiedContext(ctx) {
306
368
 
307
369
  // Context pack
308
370
  if (ctx.contextPack) {
371
+ lines.push(`### Context Architecture Layers`);
372
+ lines.push(`- Local State: ${ctx.contextPack.layers.localState ? 'Active' : 'Empty'}`);
373
+ lines.push(`- Graph State: ${ctx.contextPack.layers.graphState ? 'Active' : 'Empty'}`);
374
+ lines.push(`- Policy State: ${ctx.contextPack.layers.policyState ? ctx.contextPack.layers.policyState.mode : 'Empty'}`);
375
+ lines.push(`- Session State: ${ctx.contextPack.layers.sessionState.length} items`);
376
+ lines.push('');
309
377
  lines.push(`### Context Pack (${ctx.contextPack.itemCount} items)`);
310
378
  ctx.contextPack.items.forEach((item) => {
311
379
  lines.push(`- [${item.namespace}] ${item.title} (score: ${item.score})`);
@@ -0,0 +1,332 @@
1
+ 'use strict';
2
+
3
+ // scripts/dashboard-chat.js
4
+ // -----------------------------------------------------------------------------
5
+ // "Chat with your data" — the dashboard chat backend. Local-first RAG over
6
+ // this install's ThumbGate data (lessons, raw feedback memories via LanceDB
7
+ // vectors, receipts, gate stats). Retrieval is local (lesson search + optional
8
+ // vector-store.searchSimilar). Generation uses your configured LLM: a local
9
+ // OpenAI-compatible endpoint first, then Gemini or Perplexity when explicitly
10
+ // configured.
11
+ //
12
+ // Dialogflow/Google is not the dashboard chatbot brain. It remains an optional
13
+ // guard-adapter path for buyers who already run their own Google agent tenancy.
14
+ // -----------------------------------------------------------------------------
15
+
16
+ const path = require('path');
17
+
18
+ const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models';
19
+ const PERPLEXITY_ENDPOINT = 'https://api.perplexity.ai/chat/completions';
20
+ const DEFAULT_MODEL = 'gemini-2.5-flash';
21
+ const MAX_QUESTION_CHARS = 2000;
22
+ const MAX_CONTEXT_LESSONS = 8;
23
+
24
+ // Allowlist the model so a user-supplied `model` cannot route the call to an
25
+ // arbitrary / unexpected (or more expensive) endpoint. Anything not on the list
26
+ // falls back to the default.
27
+ const ALLOWED_MODELS = new Set([
28
+ 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro',
29
+ 'gemini-2.0-flash', 'gemini-2.0-flash-lite',
30
+ 'gemini-flash-latest', 'gemini-flash-lite-latest', 'gemini-pro-latest',
31
+ ]);
32
+
33
+ function resolveModel(requested) {
34
+ const r = String(requested || '').trim();
35
+ if (r && ALLOWED_MODELS.has(r)) return r;
36
+ const envModel = String(process.env.THUMBGATE_GEMINI_MODEL || '').trim();
37
+ if (envModel && ALLOWED_MODELS.has(envModel)) return envModel;
38
+ return DEFAULT_MODEL;
39
+ }
40
+
41
+ function resolveApiKey(opts = {}) {
42
+ let key = '';
43
+ if (Object.hasOwn(opts, 'apiKey')) {
44
+ key = opts.apiKey || '';
45
+ } else {
46
+ key = opts.apiKey || process.env.GEMINI_API_KEY || process.env.THUMBGATE_GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.PERPLEXITY_API_KEY || process.env.THUMBGATE_PERPLEXITY_API_KEY || '';
47
+ }
48
+ if (!key) return '';
49
+ return key.trim().replace(/^["']|["']$/g, '');
50
+ }
51
+
52
+ function debugChatFallback(label, err) {
53
+ if (process.env.THUMBGATE_DEBUG_CHAT !== '1') return;
54
+ const detail = err?.message ? err.message : String(err);
55
+ console.warn(`[dashboard-chat] ${label}: ${detail}`);
56
+ }
57
+
58
+ function loadLessonSearcher() {
59
+ try {
60
+ return require(path.join(__dirname, 'lesson-search')).searchLessons;
61
+ } catch (err) {
62
+ debugChatFallback('lesson search unavailable', err);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function lessonToContextItem(lesson) {
68
+ return {
69
+ id: lesson.id,
70
+ signal: lesson.signal || lesson.feedback || '',
71
+ title: (lesson.title || '').replace(/^(?:MISTAKE|SUCCESS):\s*/i, '').slice(0, 160),
72
+ content: String(lesson.content || lesson.context || '').replace(/\s+/g, ' ').trim().slice(0, 600),
73
+ tags: lesson.tags || [],
74
+ source: 'lessons',
75
+ };
76
+ }
77
+
78
+ function vectorMatchToContextItem(match, index) {
79
+ return {
80
+ id: match.id || `vec-${index}`,
81
+ signal: match.signal || '',
82
+ title: String(match.context || match.text || '').slice(0, 100),
83
+ content: match.text || match.context || '',
84
+ tags: match.tags ? String(match.tags).split(',').filter(Boolean) : [],
85
+ source: 'lancedb-vector',
86
+ };
87
+ }
88
+
89
+ function dedupeContextItems(items, limit = MAX_CONTEXT_LESSONS + 3) {
90
+ const seen = new Set();
91
+ return items.filter((item) => {
92
+ if (!(item.content || item.title)) return false;
93
+ const key = item.id || item.content.slice(0, 80);
94
+ if (seen.has(key)) return false;
95
+ seen.add(key);
96
+ return true;
97
+ }).slice(0, limit);
98
+ }
99
+
100
+ function retrieveLessonContext(question, opts = {}) {
101
+ const searchLessons = loadLessonSearcher();
102
+ if (!searchLessons) return [];
103
+ try {
104
+ const res = searchLessons(String(question || ''), {
105
+ limit: MAX_CONTEXT_LESSONS,
106
+ feedbackDir: opts.feedbackDir,
107
+ });
108
+ const rows = res?.results || res?.lessons || [];
109
+ return rows.slice(0, MAX_CONTEXT_LESSONS).map(lessonToContextItem);
110
+ } catch (err) {
111
+ debugChatFallback('lesson retrieval failed', err);
112
+ return [];
113
+ }
114
+ }
115
+
116
+ async function retrieveVectorContext(question, opts = {}) {
117
+ if (opts.useVectorSearch === false) return [];
118
+ try {
119
+ const vectorStore = require(path.join(__dirname, 'vector-store'));
120
+ const vecResults = vectorStore.searchSimilar
121
+ ? await vectorStore.searchSimilar(String(question || ''), opts.vectorLimit || 4)
122
+ : [];
123
+ return vecResults
124
+ .filter((match) => match?.text)
125
+ .map(vectorMatchToContextItem);
126
+ } catch (err) {
127
+ debugChatFallback('vector retrieval failed', err);
128
+ return [];
129
+ }
130
+ }
131
+
132
+ // Live numeric snapshot from gate-stats + feedback analyzer. Always injected
133
+ // (~120 tokens) so the LLM can answer count/quantity questions like
134
+ // "how many were blocked today" without hallucinating.
135
+ function retrieveMetricsContext() {
136
+ const snapshot = {};
137
+ try {
138
+ const gs = require(path.join(__dirname, 'gate-stats'));
139
+ const s = gs.calculateStats();
140
+ snapshot.gates = {
141
+ total: s.totalGates,
142
+ blockRules: s.blockGates,
143
+ warnRules: s.warnGates,
144
+ totalBlockedEvents: s.totalBlocked,
145
+ totalWarnedEvents: s.totalWarned,
146
+ estimatedHoursSaved: s.estimatedHoursSaved,
147
+ topBlockedTrigger: s.topBlocked?.trigger || null,
148
+ topBlockedOccurrences: s.topBlocked?.occurrences || 0,
149
+ };
150
+ } catch (err) {
151
+ debugChatFallback('gate-stats unavailable', err);
152
+ }
153
+ try {
154
+ const fl = require(path.join(__dirname, 'feedback-loop'));
155
+ const a = fl.analyzeFeedback();
156
+ snapshot.feedback = {
157
+ total: a.total,
158
+ positive: a.totalPositive,
159
+ negative: a.totalNegative,
160
+ approvalRate: a.approvalRate,
161
+ last7d: a.windows?.['7d'],
162
+ last30d: a.windows?.['30d'],
163
+ trend: a.trend,
164
+ };
165
+ } catch (err) {
166
+ debugChatFallback('feedback-loop analyze unavailable', err);
167
+ }
168
+ return snapshot;
169
+ }
170
+
171
+ // Retrieve relevant stored lessons and optional raw feedback vector matches.
172
+ async function retrieveContext(question, opts = {}) {
173
+ const lessons = retrieveLessonContext(question, opts);
174
+ const vectors = await retrieveVectorContext(question, opts);
175
+ return dedupeContextItems([...lessons, ...vectors]);
176
+ }
177
+
178
+ // Build a grounded RAG prompt. Pure function (testable).
179
+ function buildChatPrompt(question, lessons, metrics) {
180
+ const q = String(question || '').slice(0, MAX_QUESTION_CHARS).trim();
181
+ const context = (lessons || []).map((l, i) => {
182
+ const mark = /pos|up/i.test(l.signal) ? 'WORKED' : (/neg|down/i.test(l.signal) ? 'MISTAKE' : 'NOTE');
183
+ const tags = (l.tags || []).length ? ` [tags: ${l.tags.join(', ')}]` : '';
184
+ return `(${i + 1}) [${mark}] ${l.title || ''}${tags}\n ${l.content}`;
185
+ }).join('\n');
186
+
187
+ const metricsBlock = metrics && Object.keys(metrics).length
188
+ ? `\n=== Live numeric snapshot (your data, current) ===\n${JSON.stringify(metrics, null, 2)}\n`
189
+ : '';
190
+
191
+ const system = [
192
+ 'You are ThumbGate\'s "chat with your data" assistant. Answer the user\'s question',
193
+ 'using ONLY the captured lessons and the live numeric snapshot below (this team\'s real data).',
194
+ 'For count/quantity questions ("how many X", "what\'s our rate"), use the numeric snapshot.',
195
+ 'For pattern/why questions, cite the lesson numbers like [1], [3].',
196
+ 'If neither source contains the answer, say so plainly — do not invent facts.',
197
+ ].join(' ');
198
+
199
+ return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n${metricsBlock}\n=== Question ===\n${q}`;
200
+ }
201
+
202
+ // Parse the Gemini generateContent response into plain text. Pure (testable).
203
+ function parseGeminiAnswer(body) {
204
+ const parts = body?.candidates?.[0]?.content?.parts;
205
+ if (!Array.isArray(parts)) return '';
206
+ return parts.map((p) => (p && typeof p.text === 'string' ? p.text : '')).join('').trim();
207
+ }
208
+
209
+ function buildOpenAiChatPayload(prompt, model) {
210
+ return JSON.stringify({
211
+ model,
212
+ messages: [{ role: 'user', content: prompt }],
213
+ temperature: 0.2,
214
+ max_tokens: 1024,
215
+ });
216
+ }
217
+
218
+ function parseOpenAiChatAnswer(json) {
219
+ return json?.choices?.[0]?.message?.content || '';
220
+ }
221
+
222
+ function parseModelError(json, status) {
223
+ return json?.error?.message ? String(json.error.message).split('\n')[0] : `HTTP ${status}`;
224
+ }
225
+
226
+ function trimTrailingSlashes(value) {
227
+ let text = String(value || '');
228
+ while (text.endsWith('/')) {
229
+ text = text.slice(0, -1);
230
+ }
231
+ return text;
232
+ }
233
+
234
+ async function callLocalOpenAiEndpoint({ endpoint, apiKey, model, prompt, fetchImpl, sources }) {
235
+ const url = endpoint.includes('/chat/completions')
236
+ ? endpoint
237
+ : `${trimTrailingSlashes(endpoint)}/chat/completions`;
238
+ const res = await fetchImpl(url, {
239
+ method: 'POST',
240
+ headers: {
241
+ 'content-type': 'application/json',
242
+ 'Authorization': `Bearer ${apiKey || 'local'}`
243
+ },
244
+ body: buildOpenAiChatPayload(prompt, model),
245
+ });
246
+ const json = await res.json().catch(() => ({}));
247
+ if (!res.ok) {
248
+ return { ok: false, error: 'local_llm_error', status: res.status, message: parseModelError(json, res.status), sources };
249
+ }
250
+ const answer = parseOpenAiChatAnswer(json);
251
+ return { ok: true, answer: answer.trim() || '(no answer returned)', sources, model: json.model || model };
252
+ }
253
+
254
+ async function callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources }) {
255
+ const res = await fetchImpl(PERPLEXITY_ENDPOINT, {
256
+ method: 'POST',
257
+ headers: { 'content-type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
258
+ body: buildOpenAiChatPayload(prompt, 'sonar'),
259
+ });
260
+ const json = await res.json().catch(() => ({}));
261
+ if (!res.ok) {
262
+ return { ok: false, error: 'perplexity_error', status: res.status, message: parseModelError(json, res.status), sources };
263
+ }
264
+ const answer = parseOpenAiChatAnswer(json);
265
+ return { ok: true, answer: answer.trim() || '(no answer returned)', sources, model: json.model || 'perplexity-hybrid' };
266
+ }
267
+
268
+ async function callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources }) {
269
+ const res = await fetchImpl(`${GEMINI_ENDPOINT}/${encodeURIComponent(model)}:generateContent`, {
270
+ method: 'POST',
271
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
272
+ body: JSON.stringify({
273
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
274
+ generationConfig: { temperature: 0.2, maxOutputTokens: 1024 },
275
+ }),
276
+ });
277
+ const json = await res.json().catch(() => ({}));
278
+ if (!res.ok) {
279
+ return { ok: false, error: 'gemini_error', status: res.status, message: parseModelError(json, res.status), sources };
280
+ }
281
+ const answer = parseGeminiAnswer(json);
282
+ return { ok: true, answer: answer || '(no answer returned)', sources, model: json.modelVersion || model };
283
+ }
284
+
285
+ // Answer a question grounded in this install's lessons. Returns
286
+ // { ok, answer, sources, model } or { ok:false, error, ... }.
287
+ async function answerDataQuestion(question, opts = {}) {
288
+ const q = String(question || '').trim();
289
+ if (!q) return { ok: false, error: 'empty_question', message: 'Ask a question about your data.' };
290
+ if (q.length > MAX_QUESTION_CHARS) {
291
+ return { ok: false, error: 'question_too_long', message: `Question exceeds ${MAX_QUESTION_CHARS} characters.` };
292
+ }
293
+
294
+ const localEndpoint = opts.localEndpoint || process.env.THUMBGATE_LOCAL_LLM_ENDPOINT || '';
295
+ const localModel = opts.localModel || process.env.THUMBGATE_LOCAL_LLM_MODEL || 'llama3';
296
+ const apiKey = resolveApiKey(opts);
297
+ const lessons = await retrieveContext(q, opts);
298
+ const sources = lessons.map((l) => ({ id: l.id, title: l.title, signal: l.signal }));
299
+
300
+ if (!apiKey && !localEndpoint) {
301
+ return {
302
+ ok: false,
303
+ error: 'no_api_key',
304
+ message: 'Chat is not configured. Set a valid GEMINI_API_KEY, PERPLEXITY_API_KEY, or THUMBGATE_LOCAL_LLM_ENDPOINT in the project .env.',
305
+ sources,
306
+ };
307
+ }
308
+
309
+ const model = resolveModel(opts.model);
310
+ const metrics = retrieveMetricsContext();
311
+ const prompt = buildChatPrompt(q, lessons, metrics);
312
+ const fetchImpl = opts.fetch || globalThis.fetch;
313
+ const isPerplexity = apiKey && (apiKey.startsWith('pplx-') || apiKey.includes('perplexity'));
314
+
315
+ try {
316
+ if (localEndpoint) return await callLocalOpenAiEndpoint({ endpoint: localEndpoint, apiKey, model: localModel, prompt, fetchImpl, sources });
317
+ if (isPerplexity) return await callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources });
318
+ return await callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources });
319
+ } catch (err) {
320
+ const safeMessage = (err && err.message) ? String(err.message).split('\n')[0].slice(0, 100) : 'An unexpected error occurred.';
321
+ return { ok: false, error: 'network', message: safeMessage, sources };
322
+ }
323
+ }
324
+
325
+ module.exports = {
326
+ answerDataQuestion,
327
+ buildChatPrompt,
328
+ parseGeminiAnswer,
329
+ retrieveContext,
330
+ DEFAULT_MODEL,
331
+ MAX_QUESTION_CHARS,
332
+ };
@@ -17,6 +17,7 @@ const { filterEntriesForWindow, resolveAnalyticsWindow } = require('./analytics-
17
17
  const { resolveHostedBillingConfig } = require('./hosted-config');
18
18
  const { generateAgentReadinessReport } = require('./agent-readiness');
19
19
  const { summarizeGateTemplates } = require('./gate-templates');
20
+ const { mergeRepeatMetricIntoGateStats } = require('./repeat-metric');
20
21
  const { buildPredictiveInsights } = loadOptionalModule('./predictive-insights', () => ({
21
22
  buildPredictiveInsights: () => ({
22
23
  upgradePropensity: {
@@ -48,6 +49,10 @@ const {
48
49
  readDecisionLog,
49
50
  } = require('./decision-journal');
50
51
  const { analyzeFeedback } = require('./feedback-loop');
52
+ const {
53
+ collectAggregateLogEntries,
54
+ shouldAggregateFeedback,
55
+ } = require('./feedback-aggregate');
51
56
 
52
57
  const PROJECT_ROOT = path.join(__dirname, '..');
53
58
  const DEFAULT_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
@@ -967,6 +972,20 @@ function computeAnalyticsSummary(feedbackDir, options = {}) {
967
972
  return {
968
973
  window: telemetry.window || analyticsWindow,
969
974
  telemetry,
975
+ firstPartyTrafficQuality: telemetry.trafficQuality || {
976
+ rawEvents: telemetry.totalEvents || 0,
977
+ externalEvents: 0,
978
+ excludedEvents: 0,
979
+ exclusionRate: 0,
980
+ byAudience: {},
981
+ byExclusionReason: {},
982
+ external: {
983
+ uniqueVisitors: 0,
984
+ pageViews: 0,
985
+ checkoutStarts: 0,
986
+ },
987
+ verdict: 'missing',
988
+ },
970
989
  funnel: {
971
990
  visitors: uniqueVisitors,
972
991
  sessions: telemetry.visitors ? telemetry.visitors.uniqueSessions || 0 : 0,
@@ -1149,16 +1168,42 @@ function computeInstrumentationReadiness(analytics, billing) {
1149
1168
  const coverage = billing && billing.coverage ? billing.coverage : {};
1150
1169
  const telemetry = analytics.telemetry || {};
1151
1170
  const visitors = telemetry.visitors || {};
1171
+ const quality = telemetry.trafficQuality || analytics.firstPartyTrafficQuality || {};
1172
+ const external = quality.external || {};
1152
1173
  const cli = telemetry.cli || {};
1174
+ const plausibleExportConfigured = Boolean(
1175
+ process.env.PLAUSIBLE_API_KEY && (process.env.PLAUSIBLE_SITE_ID || process.env.PLAUSIBLE_DOMAIN)
1176
+ );
1177
+ const posthogExportConfigured = Boolean(
1178
+ (process.env.POSTHOG_PERSONAL_API_KEY || process.env.POSTHOG_API_KEY) && process.env.POSTHOG_PROJECT_ID
1179
+ );
1180
+ const ga4ExportConfigured = Boolean(
1181
+ process.env.GA4_PROPERTY_ID && (process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_CLIENT_EMAIL)
1182
+ );
1183
+ const dashboardGradeExportReady = plausibleExportConfigured || posthogExportConfigured || ga4ExportConfigured;
1153
1184
 
1154
1185
  return {
1155
- plausibleConfigured: /plausible\.io\/js\/script\.js|\/js\/analytics\.js/.test(landingPage),
1186
+ plausibleConfigured: /plausible\.io\/js\/script(?:\.tagged-events)?\.js|\/js\/analytics\.js/.test(landingPage),
1156
1187
  ga4Configured: Boolean(runtimeConfig.gaMeasurementId),
1157
1188
  googleSearchConsoleConfigured: Boolean(runtimeConfig.googleSiteVerification),
1158
1189
  softwareApplicationSchemaPresent: /"@type": "SoftwareApplication"/.test(landingPage),
1159
1190
  faqSchemaPresent: /"@type": "FAQPage"/.test(landingPage),
1160
1191
  telemetryEventsPresent: (telemetry.totalEvents || 0) > 0,
1161
1192
  uniqueVisitorsTracked: visitors.uniqueVisitors || 0,
1193
+ rawTelemetryEvents: quality.rawEvents || telemetry.totalEvents || 0,
1194
+ externalTelemetryEvents: quality.externalEvents || 0,
1195
+ excludedTelemetryEvents: quality.excludedEvents || 0,
1196
+ externalVisitorsTracked: external.uniqueVisitors || 0,
1197
+ externalPageViewsTracked: external.pageViews || 0,
1198
+ externalCheckoutStartsTracked: external.checkoutStarts || 0,
1199
+ externalVisitorPathsTracked: Array.isArray(external.visitorPaths) ? external.visitorPaths.length : 0,
1200
+ internalTestPollutionRate: quality.exclusionRate || 0,
1201
+ trafficQualityVerdict: quality.verdict || 'missing',
1202
+ topExcludedTrafficReason: quality.topExclusionReason || null,
1203
+ plausibleExportConfigured,
1204
+ posthogExportConfigured,
1205
+ ga4ExportConfigured,
1206
+ dashboardGradeExportReady,
1162
1207
  cliInstallsTracked: cli.uniqueInstalls || 0,
1163
1208
  funnelEventsPresent: (analytics.reconciliation.telemetryCheckoutStarts || 0) > 0,
1164
1209
  seoSignalsPresent: (analytics.seo.landingViews || 0) > 0,
@@ -1504,6 +1549,10 @@ function resolveTeamWindowHours(analyticsWindow) {
1504
1549
  // ---------------------------------------------------------------------------
1505
1550
 
1506
1551
  function collectAllFeedbackEntries(feedbackDir) {
1552
+ if (shouldAggregateFeedback()) {
1553
+ return collectAggregateLogEntries('feedback-log.jsonl', { feedbackDir }).entries;
1554
+ }
1555
+
1507
1556
  const entries = [];
1508
1557
  const seen = new Set();
1509
1558
 
@@ -1573,7 +1622,11 @@ function generateDashboard(feedbackDir, options = {}) {
1573
1622
  const billingSummary = options.billingSummary || getBillingSummary(analyticsWindow);
1574
1623
 
1575
1624
  const approval = computeApprovalStats(entries);
1576
- const gateStats = computeGateStats();
1625
+ // Surface the "repeat-attempts blocked before execution" metric on the
1626
+ // dashboard JSON and the /v1/dashboard HTTP route. Use the non-mutating
1627
+ // helper (mirrors server-stdio.js) instead of mutating computeGateStats()'s
1628
+ // return value. (mergeRepeatMetricIntoGateStats is imported at top of file.)
1629
+ const gateStats = mergeRepeatMetricIntoGateStats(computeGateStats());
1577
1630
  const prevention = computePreventionImpact(feedbackDir, gateStats);
1578
1631
  const trend = computeSessionTrend(entries, 10);
1579
1632
  const health = computeSystemHealth(feedbackDir, gateStats);
@@ -1865,8 +1918,10 @@ function printDashboard(data) {
1865
1918
  console.log('');
1866
1919
  console.log('\uD83D\uDCBC Growth Analytics');
1867
1920
  console.log(` Unique Visitors : ${analytics.trafficMetrics.visitors}`);
1921
+ console.log(` External Visitors: ${instrumentation.externalVisitorsTracked}`);
1868
1922
  console.log(` Sessions : ${analytics.trafficMetrics.sessions}`);
1869
1923
  console.log(` Page Views : ${analytics.trafficMetrics.pageViews}`);
1924
+ console.log(` External Views : ${instrumentation.externalPageViewsTracked}`);
1870
1925
  console.log(` CTA Clicks : ${analytics.trafficMetrics.ctaClicks}`);
1871
1926
  console.log(` Leads : ${analytics.funnel.acquisitionLeads}`);
1872
1927
  console.log(` Sprint Leads : ${analytics.pipeline.workflowSprintLeads.total}`);
@@ -1877,6 +1932,7 @@ function printDashboard(data) {
1877
1932
  console.log(` Booked Revenue : $${(analytics.revenue.bookedRevenueCents / 100).toFixed(2)}`);
1878
1933
  console.log(` Matched Journeys : ${analytics.reconciliation.matchedPaidOrders}/${analytics.reconciliation.telemetryCheckoutStarts}`);
1879
1934
  console.log(` Buyer Loss : ${analytics.buyerLoss.totalSignals}`);
1935
+ console.log(` Data Quality : ${analytics.firstPartyTrafficQuality.verdict} (${instrumentation.excludedTelemetryEvents}/${instrumentation.rawTelemetryEvents} excluded)`);
1880
1936
  if (analytics.telemetry.visitors.topSource) {
1881
1937
  console.log(` Top Source : ${analytics.telemetry.visitors.topSource.key} (${analytics.telemetry.visitors.topSource.count}\u00D7)`);
1882
1938
  }
@@ -1896,6 +1952,15 @@ function printDashboard(data) {
1896
1952
  console.log(` GA4 : ${instrumentation.ga4Configured ? 'configured' : 'missing'}`);
1897
1953
  console.log(` Search Console : ${instrumentation.googleSearchConsoleConfigured ? 'configured' : 'missing'}`);
1898
1954
  console.log(` Telemetry Events : ${instrumentation.telemetryEventsPresent ? instrumentation.uniqueVisitorsTracked : 0} visitors`);
1955
+ console.log(` Clean Visitors : ${instrumentation.externalVisitorsTracked} external (${Math.round((instrumentation.internalTestPollutionRate || 0) * 100)}% internal/test/bot events)`);
1956
+ console.log(` Clean Paths : ${instrumentation.externalVisitorPathsTracked} first-party paths`);
1957
+ if (instrumentation.topExcludedTrafficReason) {
1958
+ console.log(` Top Exclusion : ${instrumentation.topExcludedTrafficReason.key} (${instrumentation.topExcludedTrafficReason.count}\u00D7)`);
1959
+ }
1960
+ console.log(` Plausible Export : ${instrumentation.plausibleExportConfigured ? 'configured' : 'missing API credentials'}`);
1961
+ console.log(` PostHog Export : ${instrumentation.posthogExportConfigured ? 'configured' : 'missing API credentials'}`);
1962
+ console.log(` GA4 Export : ${instrumentation.ga4ExportConfigured ? 'configured' : 'missing API credentials'}`);
1963
+ console.log(` Visitor Paths : ${instrumentation.dashboardGradeExportReady ? 'export-ready' : 'not dashboard-grade in repo'}`);
1899
1964
  console.log(` SEO Signals : ${instrumentation.seoSignalsPresent ? analytics.seo.landingViews : 0}`);
1900
1965
  console.log(` Buyer Loss : ${instrumentation.buyerLossSignalsPresent ? analytics.buyerLoss.totalSignals : 0}`);
1901
1966
  console.log(` Attribution : ${Math.round((instrumentation.trafficAttributionCoverage || 0) * 100)}% page-view coverage`);
@@ -2025,6 +2090,7 @@ module.exports = {
2025
2090
  computeObservabilityStats,
2026
2091
  readJSONL,
2027
2092
  readJsonFile,
2093
+ collectAllFeedbackEntries,
2028
2094
  };
2029
2095
 
2030
2096
  if (require.main === module) {
@@ -6,6 +6,7 @@ const path = require('path');
6
6
 
7
7
  const { getFeedbackPaths } = require('./feedback-loop');
8
8
  const { ensureDir } = require('./fs-utils');
9
+ const { redactSecretsDeep } = require('./secret-redaction');
9
10
 
10
11
  const PROJECT_ROOT = path.join(__dirname, '..');
11
12
  const DEFAULT_PROOF_DIR = process.env.THUMBGATE_PROOF_DIR
@@ -47,7 +48,10 @@ function readJSON(filePath) {
47
48
  }
48
49
 
49
50
  function writeJSONL(filePath, rows) {
50
- const content = rows.map((row) => JSON.stringify(row)).join('\n');
51
+ // Redact secrets from every bundle row this is the single choke point for all bundle tables
52
+ // (feedback_events, memory_records, sequences, attributions, proof_reports). A shared/published
53
+ // dataset must never ship a captured credential. See scripts/secret-redaction.js.
54
+ const content = rows.map((row) => JSON.stringify(redactSecretsDeep(row))).join('\n');
51
55
  fs.writeFileSync(filePath, content ? `${content}\n` : '');
52
56
  }
53
57
 
@@ -9,6 +9,7 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { traceForDpoPair, aggregateTraces } = require('./code-reasoning');
11
11
  const { resolveFeedbackDir } = require('./feedback-paths');
12
+ const { redactSecretsDeep } = require('./secret-redaction');
12
13
 
13
14
  const DEFAULT_LOCAL_MEMORY_LOG = path.join(resolveFeedbackDir(), 'memory-log.jsonl');
14
15
 
@@ -201,14 +202,18 @@ function exportDpoFromMemories(memories) {
201
202
  },
202
203
  }));
203
204
 
205
+ // Redact secrets before the pairs leave this module — they are derived from memory content and
206
+ // are shipped to disk here AND consumed by export-hf-dataset.js. See scripts/secret-redaction.js.
207
+ const redactedPairs = pairsWithTraces.map((pair) => redactSecretsDeep(pair));
208
+
204
209
  return {
205
- pairs: pairsWithTraces,
210
+ pairs: redactedPairs,
206
211
  unpairedErrors: result.unpairedErrors,
207
212
  unpairedLearnings: result.unpairedLearnings,
208
213
  errors,
209
214
  learnings,
210
215
  reasoning,
211
- jsonl: toJSONL(pairsWithTraces),
216
+ jsonl: toJSONL(redactedPairs),
212
217
  };
213
218
  }
214
219