thumbgate 1.27.18 → 1.27.19

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 (96) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +33 -12
  5. package/.well-known/mcp/server-card.json +8 -8
  6. package/README.md +249 -30
  7. package/adapters/chatgpt/openapi.yaml +12 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/gemini/function-declarations.json +1 -0
  11. package/adapters/mcp/server-stdio.js +263 -11
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bench/thumbgate-bench.json +2 -2
  14. package/bin/cli.js +1429 -121
  15. package/bin/postinstall.js +1 -8
  16. package/config/gate-classifier-routing.json +98 -0
  17. package/config/gate-templates.json +216 -0
  18. package/config/gates/claim-verification.json +12 -0
  19. package/config/gates/default.json +31 -2
  20. package/config/github-about.json +2 -2
  21. package/config/mcp-allowlists.json +23 -13
  22. package/config/merge-quality-checks.json +0 -1
  23. package/config/model-candidates.json +121 -6
  24. package/config/post-deploy-marketing-pages.json +80 -0
  25. package/config/tessl-tiles.json +1 -3
  26. package/openapi/openapi.yaml +12 -0
  27. package/package.json +1 -1
  28. package/public/blog.html +4 -4
  29. package/public/codex-plugin.html +72 -20
  30. package/public/compare.html +31 -8
  31. package/public/dashboard.html +930 -166
  32. package/public/federal.html +2 -2
  33. package/public/guide.html +33 -13
  34. package/public/index.html +469 -111
  35. package/public/learn.html +183 -18
  36. package/public/lessons.html +168 -10
  37. package/public/numbers.html +7 -7
  38. package/public/pro.html +34 -11
  39. package/scripts/agent-memory-lifecycle.js +211 -0
  40. package/scripts/agent-readiness.js +20 -3
  41. package/scripts/agent-reward-model.js +53 -1
  42. package/scripts/auto-promote-gates.js +82 -10
  43. package/scripts/auto-wire-hooks.js +14 -0
  44. package/scripts/billing.js +93 -1
  45. package/scripts/bot-detection.js +61 -3
  46. package/scripts/build-metadata.js +50 -10
  47. package/scripts/cli-feedback.js +4 -2
  48. package/scripts/cli-schema.js +97 -0
  49. package/scripts/cli-telemetry.js +6 -1
  50. package/scripts/commercial-offer.js +82 -2
  51. package/scripts/context-manager.js +74 -6
  52. package/scripts/dashboard.js +68 -2
  53. package/scripts/export-databricks-bundle.js +5 -1
  54. package/scripts/export-dpo-pairs.js +7 -2
  55. package/scripts/feedback-loop.js +123 -1
  56. package/scripts/feedback-quality.js +87 -0
  57. package/scripts/filesystem-search.js +35 -10
  58. package/scripts/gate-stats.js +89 -0
  59. package/scripts/gates-engine.js +1176 -85
  60. package/scripts/gemini-embedding-policy.js +2 -1
  61. package/scripts/hook-runtime.js +20 -14
  62. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  63. package/scripts/hybrid-feedback-context.js +142 -7
  64. package/scripts/lesson-inference.js +8 -3
  65. package/scripts/lesson-search.js +17 -1
  66. package/scripts/license.js +10 -10
  67. package/scripts/llm-client.js +169 -4
  68. package/scripts/local-model-profile.js +15 -8
  69. package/scripts/mcp-config.js +7 -1
  70. package/scripts/memory-scope-readiness.js +159 -0
  71. package/scripts/meta-agent-loop.js +36 -0
  72. package/scripts/operational-integrity.js +39 -5
  73. package/scripts/oss-pr-opportunity-scout.js +35 -5
  74. package/scripts/plausible-server-events.js +9 -6
  75. package/scripts/pro-local-dashboard.js +4 -4
  76. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  77. package/scripts/published-cli.js +0 -8
  78. package/scripts/rate-limiter.js +64 -13
  79. package/scripts/secret-scanner.js +44 -5
  80. package/scripts/security-scanner.js +260 -10
  81. package/scripts/self-distill-agent.js +3 -1
  82. package/scripts/seo-gsd.js +916 -7
  83. package/scripts/statusline-cache-path.js +17 -2
  84. package/scripts/statusline-local-stats.js +9 -1
  85. package/scripts/statusline-meta.js +28 -2
  86. package/scripts/statusline.sh +20 -4
  87. package/scripts/telemetry-analytics.js +357 -0
  88. package/scripts/thompson-sampling.js +31 -10
  89. package/scripts/thumbgate-bench.js +16 -1
  90. package/scripts/thumbgate-search.js +85 -19
  91. package/scripts/tool-registry.js +169 -1
  92. package/scripts/vector-store.js +45 -0
  93. package/scripts/workflow-sentinel.js +286 -53
  94. package/scripts/workspace-evolver.js +62 -2
  95. package/src/api/server.js +2683 -319
  96. 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})`);
@@ -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
 
@@ -56,6 +56,90 @@ const {
56
56
 
57
57
  const AUDIT_TRAIL_TAG = 'audit-trail';
58
58
 
59
+ /**
60
+ * Anonymous fire-and-forget CLI feedback telemetry.
61
+ *
62
+ * Pings the hosted /v1/telemetry/ping endpoint exactly once per successful
63
+ * local feedback capture so the dashboard can measure CLI-side lesson volume.
64
+ *
65
+ * Hard contract (do NOT widen without explicit approval):
66
+ * - ONE event type: `feedback_captured`
67
+ * - Payload: { installId, signal: 'up'|'down', tier, ts } only.
68
+ * No context strings, tags, file paths, or content of any kind.
69
+ * - Opt-out: THUMBGATE_DISABLE_TELEMETRY=1 (or 'true') short-circuits
70
+ * immediately. Legacy THUMBGATE_NO_TELEMETRY=1 / DO_NOT_TRACK=1 are
71
+ * also honored for parity with cli-telemetry.js.
72
+ * - Fire-and-forget: NEVER await this call. Errors are swallowed.
73
+ * - 2-second timeout via AbortSignal.timeout.
74
+ */
75
+ function emitAnonymousFeedbackPing(signal) {
76
+ try {
77
+ const env = process.env || {};
78
+ if (
79
+ env.THUMBGATE_DISABLE_TELEMETRY === '1' ||
80
+ env.THUMBGATE_DISABLE_TELEMETRY === 'true' ||
81
+ env.THUMBGATE_NO_TELEMETRY === '1' ||
82
+ env.DO_NOT_TRACK === '1'
83
+ ) {
84
+ return;
85
+ }
86
+
87
+ const normalizedSignal = signal === 'positive' ? 'up' : signal === 'negative' ? 'down' : null;
88
+ if (!normalizedSignal) return;
89
+
90
+ // Reuse the canonical installId from cli-telemetry.js (persisted at
91
+ // ~/.thumbgate/install-id). Falls back to a fresh UUID if that module
92
+ // is unavailable — better to ship an event we can dedup on the server
93
+ // than to drop the ping entirely.
94
+ let installId = null;
95
+ try {
96
+ const { getInstallId } = require('./cli-telemetry');
97
+ installId = getInstallId();
98
+ } catch (_) { /* fall through */ }
99
+ if (!installId) {
100
+ try {
101
+ installId = require('crypto').randomUUID();
102
+ } catch (_) {
103
+ return; // no crypto, no install id → drop silently
104
+ }
105
+ }
106
+
107
+ let tier = 'free';
108
+ try {
109
+ const { getStatuslineMeta } = require('./statusline-meta');
110
+ const meta = getStatuslineMeta({ env });
111
+ const rawTier = String(meta && meta.tier ? meta.tier : 'free').toLowerCase();
112
+ if (rawTier === 'pro' || rawTier === 'enterprise' || rawTier === 'free') {
113
+ tier = rawTier;
114
+ }
115
+ } catch (_) { /* default to 'free' */ }
116
+
117
+ const base = env.THUMBGATE_PUBLIC_APP_ORIGIN
118
+ || env.THUMBGATE_API_URL
119
+ || 'https://thumbgate-production.up.railway.app';
120
+
121
+ const body = JSON.stringify({
122
+ eventType: 'feedback_captured',
123
+ clientType: 'cli',
124
+ installId,
125
+ signal: normalizedSignal,
126
+ tier,
127
+ ts: new Date().toISOString(),
128
+ });
129
+
130
+ // Fire-and-forget. No await. AbortSignal.timeout enforces the 2s cap.
131
+ if (typeof fetch !== 'function' || typeof AbortSignal === 'undefined' || typeof AbortSignal.timeout !== 'function') {
132
+ return;
133
+ }
134
+ fetch(`${base.replace(/\/+$/, '')}/v1/telemetry/ping`, {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body,
138
+ signal: AbortSignal.timeout(2000),
139
+ }).catch(() => { /* fire-and-forget */ });
140
+ } catch (_) { /* telemetry must never disrupt CLI */ }
141
+ }
142
+
59
143
  function isAuditTrailEntry(entry = {}) {
60
144
  return Array.isArray(entry.tags) && entry.tags.includes(AUDIT_TRAIL_TAG);
61
145
  }
@@ -1059,6 +1143,7 @@ function captureFeedback(params) {
1059
1143
  : null),
1060
1144
  structuredRule: structuredRule || null,
1061
1145
  ...(reflection && { reflection }),
1146
+ gateAction: params.gateAction || null,
1062
1147
  timestamp: now,
1063
1148
  };
1064
1149
 
@@ -1112,6 +1197,7 @@ function captureFeedback(params) {
1112
1197
  summary.lastUpdated = now;
1113
1198
  saveSummary(summary);
1114
1199
  appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
1200
+ emitAnonymousFeedbackPing(signal);
1115
1201
  try { appendRejectionLedger(feedbackEvent, action.reason); } catch { /* non-critical */ }
1116
1202
  try {
1117
1203
  appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
@@ -1153,6 +1239,7 @@ function captureFeedback(params) {
1153
1239
  ...feedbackEvent,
1154
1240
  validationIssues: prepared.issues,
1155
1241
  });
1242
+ emitAnonymousFeedbackPing(signal);
1156
1243
  try { appendRejectionLedger(feedbackEvent, `Schema validation failed: ${prepared.issues.join('; ')}`); } catch { /* non-critical */ }
1157
1244
  try {
1158
1245
  appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
@@ -1227,6 +1314,7 @@ function captureFeedback(params) {
1227
1314
  }
1228
1315
 
1229
1316
  appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
1317
+ emitAnonymousFeedbackPing(signal);
1230
1318
 
1231
1319
  // Synthesis: merge similar lessons instead of creating duplicates
1232
1320
  let synthesisResult = null;
@@ -1398,7 +1486,7 @@ function captureFeedback(params) {
1398
1486
  if (feedbackEvent.signal === 'negative') {
1399
1487
  try {
1400
1488
  const autoPromote = require('./auto-promote-gates');
1401
- const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH);
1489
+ const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH, { gateAction: feedbackEvent.gateAction });
1402
1490
  // First-rule activation telemetry: anonymous ping the first time
1403
1491
  // a prevention rule auto-promotes for this install. Idempotent —
1404
1492
  // see scripts/activation-tracker.js. Critical for activation funnel
@@ -1411,10 +1499,44 @@ function captureFeedback(params) {
1411
1499
  totalGates: promoteResult.totalGates,
1412
1500
  });
1413
1501
  } catch { /* activation telemetry is non-critical */ }
1502
+
1503
+ // Trigger Self-Harness Optimizer to propagate the new rules to prompt files & validate
1504
+ try {
1505
+ const { fork } = require('child_process');
1506
+ const localOptimizerPath = path.join(process.cwd(), 'scripts', 'self-harness-optimizer.js');
1507
+ const packageOptimizerPath = path.join(__dirname, 'self-harness-optimizer.js');
1508
+
1509
+ if (fs.existsSync(localOptimizerPath)) {
1510
+ fork(localOptimizerPath, [], { stdio: 'ignore', detached: true }).unref();
1511
+ } else if (fs.existsSync(packageOptimizerPath)) {
1512
+ fork(packageOptimizerPath, [], { stdio: 'ignore', detached: true }).unref();
1513
+ }
1514
+ } catch (err) {
1515
+ console.error('Failed to trigger self-harness optimizer:', err);
1516
+ }
1414
1517
  }
1415
1518
  } catch { /* Gate promotion is non-critical */ }
1416
1519
  }
1417
1520
 
1521
+ // Auto-export to Obsidian if configured (deferred but tracked)
1522
+ if (process.env.THUMBGATE_OBSIDIAN_VAULT_PATH) {
1523
+ const exportPromise = new Promise((resolve) => {
1524
+ setImmediate(() => {
1525
+ try {
1526
+ const { exportAll } = require('./obsidian-export');
1527
+ exportAll({
1528
+ feedbackDir: FEEDBACK_DIR,
1529
+ outputDir: process.env.THUMBGATE_OBSIDIAN_VAULT_PATH,
1530
+ });
1531
+ } catch (_err) {
1532
+ // Non-critical, do not crash feedback loop
1533
+ }
1534
+ resolve();
1535
+ });
1536
+ });
1537
+ trackBackgroundSideEffect(exportPromise);
1538
+ }
1539
+
1418
1540
  // --- Deferred side-effects (contextFs, RLAIF — non-critical, potentially slow) ---
1419
1541
  setImmediate(() => {
1420
1542
  try {
@@ -62,6 +62,92 @@ function normalizeFeedbackText(value) {
62
62
  .trim();
63
63
  }
64
64
 
65
+ function editDistance(a, b) {
66
+ const left = String(a || '');
67
+ const right = String(b || '');
68
+ const dp = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
69
+ for (let i = 0; i <= left.length; i++) dp[i][0] = i;
70
+ for (let j = 0; j <= right.length; j++) dp[0][j] = j;
71
+ for (let i = 1; i <= left.length; i++) {
72
+ for (let j = 1; j <= right.length; j++) {
73
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
74
+ dp[i][j] = Math.min(
75
+ dp[i - 1][j] + 1,
76
+ dp[i][j - 1] + 1,
77
+ dp[i - 1][j - 1] + cost,
78
+ );
79
+ }
80
+ }
81
+ return dp[left.length][right.length];
82
+ }
83
+
84
+ function isNearThumbToken(token) {
85
+ const value = String(token || '');
86
+ if (value.length < 4) return false;
87
+ return editDistance(value, 'thumb') <= 1 || editDistance(value, 'thumbs') <= 2;
88
+ }
89
+
90
+ function isNearUpToken(token) {
91
+ const value = String(token || '');
92
+ return value === 'up' || editDistance(value, 'up') <= 1;
93
+ }
94
+
95
+ function isNearDownToken(token) {
96
+ const value = String(token || '');
97
+ if (value.length < 2) return false;
98
+ return editDistance(value, 'down') <= 1;
99
+ }
100
+
101
+ function detectFeedbackSignal(value) {
102
+ const raw = String(value || '');
103
+ if (/[👎👎🏻👎🏼👎🏽👎🏾👎🏿]/u.test(raw)) return { signal: 'down', confidence: 'emoji', match: '👎' };
104
+ if (/[👍👍🏻👍🏼👍🏽👍🏾👍🏿]/u.test(raw)) return { signal: 'up', confidence: 'emoji', match: '👍' };
105
+
106
+ const normalized = normalizeFeedbackText(raw);
107
+ if (!normalized) return null;
108
+
109
+ const exactDown = [
110
+ /\bthumbs?\s*down\b/,
111
+ /\bthumbs?down\b/,
112
+ /\bthat failed\b/,
113
+ /\bit failed\b/,
114
+ /\bthat was wrong\b/,
115
+ /\bfix this\b/,
116
+ ];
117
+ if (exactDown.some((pattern) => pattern.test(normalized))) {
118
+ return { signal: 'down', confidence: 'exact', match: normalized };
119
+ }
120
+
121
+ const exactUp = [
122
+ /\bthumbs?\s*up\b/,
123
+ /\bthumbs?up\b/,
124
+ /\bthat worked\b/,
125
+ /\bit worked\b/,
126
+ /\blooks good\b/,
127
+ /\bgood job\b/,
128
+ /\bgood work\b/,
129
+ /\bnice work\b/,
130
+ /\bperfect\b/,
131
+ /\blgtm\b/,
132
+ ];
133
+ if (exactUp.some((pattern) => pattern.test(normalized))) {
134
+ return { signal: 'up', confidence: 'exact', match: normalized };
135
+ }
136
+
137
+ const words = normalized.split(/\s+/).filter(Boolean);
138
+ for (let i = 0; i < words.length - 1; i++) {
139
+ if (!isNearThumbToken(words[i])) continue;
140
+ if (isNearDownToken(words[i + 1])) {
141
+ return { signal: 'down', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
142
+ }
143
+ if (isNearUpToken(words[i + 1])) {
144
+ return { signal: 'up', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
65
151
  function isGenericFeedbackText(value, signal) {
66
152
  const normalized = normalizeFeedbackText(value);
67
153
  if (!normalized) return false;
@@ -131,6 +217,7 @@ function buildClarificationMessage(params = {}) {
131
217
 
132
218
  module.exports = {
133
219
  GENERIC_PHRASE_RULES,
220
+ detectFeedbackSignal,
134
221
  normalizeFeedbackSignal,
135
222
  normalizeFeedbackText,
136
223
  isGenericFeedbackText,