thumbgate 1.23.1 → 1.25.0

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 (44) hide show
  1. package/.claude-plugin/marketplace.json +5 -5
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.well-known/llms.txt +26 -11
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +84 -38
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/mcp/server-stdio.js +1 -1
  8. package/adapters/opencode/opencode.json +1 -1
  9. package/bin/cli.js +39 -16
  10. package/bin/postinstall.js +11 -22
  11. package/config/gate-templates.json +72 -0
  12. package/config/github-about.json +1 -1
  13. package/config/post-deploy-marketing-pages.json +6 -1
  14. package/package.json +20 -8
  15. package/public/agent-manager.html +3 -3
  16. package/public/agents-cost-savings.html +3 -3
  17. package/public/ai-malpractice-prevention.html +335 -7
  18. package/public/blog.html +3 -3
  19. package/public/codex-enterprise.html +3 -3
  20. package/public/codex-plugin.html +4 -4
  21. package/public/compare.html +6 -6
  22. package/public/dashboard.html +211 -126
  23. package/public/guide.html +5 -5
  24. package/public/index.html +156 -47
  25. package/public/learn.html +24 -10
  26. package/public/lessons.html +2 -2
  27. package/public/numbers.html +6 -6
  28. package/public/pricing.html +6 -5
  29. package/public/pro.html +1 -0
  30. package/scripts/billing.js +17 -0
  31. package/scripts/commercial-offer.js +4 -1
  32. package/scripts/dashboard.js +53 -1
  33. package/scripts/gates-engine.js +101 -16
  34. package/scripts/mcp-oauth.js +293 -0
  35. package/scripts/plausible-server-events.js +2 -1
  36. package/scripts/rate-limiter.js +16 -12
  37. package/scripts/security-scanner.js +80 -10
  38. package/scripts/seo-gsd.js +167 -1
  39. package/scripts/telemetry-analytics.js +310 -0
  40. package/scripts/tool-registry.js +35 -1
  41. package/scripts/vector-store.js +1 -0
  42. package/scripts/visitor-journey.js +172 -0
  43. package/src/api/server.js +226 -31
  44. package/adapters/chatgpt/openapi.yaml +0 -1705
@@ -10,9 +10,9 @@
10
10
  <meta property="og:title" content="ThumbGate — The Numbers">
11
11
  <meta property="og:description" content="Generated first-party operational snapshot: configured gates, recorded interventions, and explicit zero-evidence caveats.">
12
12
  <meta property="og:type" content="website">
13
- <meta property="og:url" content="https://thumbgate-production.up.railway.app/numbers">
13
+ <meta property="og:url" content="https://thumbgate.ai/numbers">
14
14
  <meta name="twitter:card" content="summary_large_image">
15
- <link rel="canonical" href="https://thumbgate-production.up.railway.app/numbers">
15
+ <link rel="canonical" href="https://thumbgate.ai/numbers">
16
16
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
17
17
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
18
18
  <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
@@ -25,8 +25,8 @@
25
25
  "alternateName": "thumbgate",
26
26
  "applicationCategory": "DeveloperApplication",
27
27
  "operatingSystem": "Cross-platform, Node.js >=18.18.0",
28
- "softwareVersion": "1.23.1",
29
- "url": "https://thumbgate-production.up.railway.app/numbers",
28
+ "softwareVersion": "1.25.0",
29
+ "url": "https://thumbgate.ai/numbers",
30
30
  "dateModified": "2026-05-07",
31
31
  "creator": {
32
32
  "@type": "Person",
@@ -46,7 +46,7 @@
46
46
  "@type": "Dataset",
47
47
  "name": "ThumbGate Operational Snapshot",
48
48
  "description": "First-party operational snapshot from the ThumbGate pre-action check runtime: configured checks, recorded block/warn events, estimated token savings from recorded blocks, and Bayes error rate when the sample supports it.",
49
- "url": "https://thumbgate-production.up.railway.app/numbers",
49
+ "url": "https://thumbgate.ai/numbers",
50
50
  "license": "https://opensource.org/licenses/MIT",
51
51
  "creator": {
52
52
  "@type": "Person",
@@ -202,7 +202,7 @@
202
202
  <main class="container">
203
203
  <h1>The Numbers</h1>
204
204
  <p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
205
- <div class="freshness">Updated: 2026-05-07 · Version 1.23.1</div>
205
+ <div class="freshness">Updated: 2026-05-07 · Version 1.25.0</div>
206
206
  <div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
207
207
 
208
208
  <h2>Gate enforcement</h2>
@@ -12,6 +12,7 @@ __GOOGLE_SITE_VERIFICATION_META__
12
12
  <meta property="og:url" content="__APP_ORIGIN__/pricing">
13
13
  <meta property="og:image" content="/og.png">
14
14
  <link rel="canonical" href="__APP_ORIGIN__/pricing">
15
+ <link rel="alternate" type="text/markdown" title="ThumbGate LLM context" href="__APP_ORIGIN__/llm-context.md">
15
16
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
16
17
  <link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
17
18
 
@@ -49,7 +50,7 @@ __GA_BOOTSTRAP__
49
50
  "@context": "https://schema.org",
50
51
  "@type": "FAQPage",
51
52
  "mainEntity": [
52
- { "@type": "Question", "name": "What does Pro add over the free CLI?", "acceptedAnswer": { "@type": "Answer", "text": "Free gives you unlimited captures and 5 active rules, running entirely on your machine. Pro is the hosted layer: lesson sync across machines, a dashboard without self-hosting, managed adapter updates, unlimited rules, and DPO export. You're paying for infrastructure we run, not features we hide." } },
53
+ { "@type": "Question", "name": "What does Pro add over the free CLI?", "acceptedAnswer": { "@type": "Answer", "text": "Free gives you 5 captures/day and 3 active rules, running entirely on your machine. Pro is the hosted layer: unlimited captures, unlimited rules, lesson sync across machines, a dashboard without self-hosting, managed adapter updates, and DPO export. You're paying for infrastructure we run, not features we hide." } },
53
54
  { "@type": "Question", "name": "Does ThumbGate send my code to the cloud?", "acceptedAnswer": { "@type": "Answer", "text": "No. The CLI is local-first — no data leaves your machine. Pro and Team add hosted sync for dashboards and shared lessons, but your source code stays local." } },
54
55
  { "@type": "Question", "name": "When should I pick Team over Pro?", "acceptedAnswer": { "@type": "Answer", "text": "When one engineer's correction should protect the whole team. Team shares the lesson database across seats so a fix in one repo prevents the same mistake in every repo." } },
55
56
  { "@type": "Question", "name": "Can I cancel anytime?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. Pro and Team are month-to-month with a 7-day refund window on the first charge. Cancel from the billing portal and your subscription ends at the period close." } }
@@ -237,8 +238,8 @@ __GA_BOOTSTRAP__
237
238
  <div class="price">$0</div>
238
239
  <div class="price-sub">Block repeated mistakes daily. Forever free for solo devs.</div>
239
240
  <ul>
240
- <li>Unlimited feedback captures — every thumbs-down, every session</li>
241
- <li>Up to 5 active prevention rules</li>
241
+ <li>5 feedback captures/day (25 total) enough to see the value</li>
242
+ <li>Up to 3 active prevention rules</li>
242
243
  <li>All MCP integrations (Claude Code, Cursor, Codex, Gemini, Amp)</li>
243
244
  <li>PreToolUse hook blocking with built-in safety checks</li>
244
245
  <li>Runs 100% local — no account, no signup, no data leaves your machine</li>
@@ -257,7 +258,7 @@ __GA_BOOTSTRAP__
257
258
  <li><strong>Hosted lesson sync</strong> — corrections follow you across machines, no manual export</li>
258
259
  <li><strong>Managed adapter matrix</strong> — we track runtime changes in Claude Code, Cursor, Codex, Gemini, Amp so you don't</li>
259
260
  <li><strong>Hosted dashboard</strong> — see every blocked action, every rule that fired, without running your own server</li>
260
- <li><strong>Unlimited prevention rules</strong> — free caps at 5 auto-promoted rules</li>
261
+ <li><strong>Unlimited prevention rules</strong> — free caps at 3 auto-promoted rules</li>
261
262
  <li><strong>DPO + HuggingFace export</strong> — training data from your real corrections</li>
262
263
  <li><strong>Auto-connect</strong> — new agent surfaces appear automatically after setup</li>
263
264
  <li>7-day refund window. Cancel anytime.</li>
@@ -296,7 +297,7 @@ __GA_BOOTSTRAP__
296
297
  <div class="faq-list">
297
298
  <div class="faq-item">
298
299
  <div class="faq-q">What does Pro add over the free CLI?</div>
299
- <div class="faq-a">Free gives you unlimited captures and 5 active rules, running entirely on your machine. Pro is the hosted layer: lesson sync across machines, a dashboard without self-hosting, managed adapter updates, unlimited rules, and DPO export. You're paying for infrastructure we run, not features we hide.</div>
300
+ <div class="faq-a">Free gives you 5 captures/day and 3 active rules, running entirely on your machine. Pro is the hosted layer: unlimited captures, unlimited rules, lesson sync across machines, a dashboard without self-hosting, managed adapter updates, and DPO export. You're paying for infrastructure we run, not features we hide.</div>
300
301
  </div>
301
302
  <div class="faq-item">
302
303
  <div class="faq-q">Does ThumbGate send my code to the cloud?</div>
package/public/pro.html CHANGED
@@ -11,6 +11,7 @@ __GOOGLE_SITE_VERIFICATION_META__
11
11
  <meta property="og:type" content="website">
12
12
  <meta property="og:url" content="__APP_ORIGIN__/pro">
13
13
  <link rel="canonical" href="__APP_ORIGIN__/pro">
14
+ <link rel="alternate" type="text/markdown" title="ThumbGate LLM context" href="__APP_ORIGIN__/llm-context.md">
14
15
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
15
16
  <link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
16
17
  <meta property="og:image" content="/og.png">
@@ -51,6 +51,7 @@ const {
51
51
  } = require('./analytics-window');
52
52
  const { ensureParentDir } = require('./fs-utils');
53
53
  const mailer = require('./mailer');
54
+ const { recordCheckoutFunnelEvent } = require('./plausible-server-events');
54
55
 
55
56
  function loadWorkflowSprintIntakeModule() {
56
57
  const modulePath = path.resolve(__dirname, 'workflow-sprint-intake.js');
@@ -3038,6 +3039,22 @@ async function handleWebhook(rawBody, signature) {
3038
3039
  attribution,
3039
3040
  });
3040
3041
  }
3042
+ // Fire Plausible purchase event so the funnel poller can measure
3043
+ // end-to-end conversion: visitor → CTA → checkout → email → Stripe → purchase.
3044
+ // Fire-and-forget (never blocks the webhook response).
3045
+ void recordCheckoutFunnelEvent('purchase', {
3046
+ page: '/success',
3047
+ props: {
3048
+ sessionId: session.id,
3049
+ customerId,
3050
+ traceId: traceId || '',
3051
+ packId: packId || '',
3052
+ amount: session.amount_total != null ? String(session.amount_total) : '',
3053
+ currency: session.currency || '',
3054
+ ...attribution,
3055
+ },
3056
+ });
3057
+
3041
3058
  return {
3042
3059
  handled: true,
3043
3060
  action: 'provisioned_api_key',
@@ -61,10 +61,12 @@ function buildCaptureReceipt({ signal, feedbackId, memoryId, actionType } = {})
61
61
  ` Stored proof : ${normalizedSignal} feedback${feedbackId ? ` (${feedbackId})` : ''}`,
62
62
  memoryId ? ` Local memory : ${memoryId}` : ' Local memory : saved locally',
63
63
  actionType ? ` Rule pressure : ${actionType}` : ' Rule pressure : available for promotion',
64
+ ' Free today : this proof protects this local machine',
65
+ ' Pro sync : keep this lesson, rule, and dashboard synced across machines and agent runtimes',
64
66
  ' Next proof : npx thumbgate stats',
65
67
  ' Cost proof : npx thumbgate cost',
66
68
  '',
67
- ` Solo Pro : ${PRO_PRICE_LABEL} for dashboard, search, exports, sync`,
69
+ ` Solo Pro : ${PRO_PRICE_LABEL} for hosted sync, search, dashboard, and exports`,
68
70
  ` Upgrade : ${trackedProUrl('cli_capture_receipt', actionType || normalizedSignal.toLowerCase())}`,
69
71
  ` Team path : ${TEAM_PRICE_LABEL}; start with one repeated workflow failure`,
70
72
  ' https://thumbgate.ai/#workflow-sprint-intake',
@@ -98,6 +100,7 @@ function buildStatsReceipt(stats = {}) {
98
100
  lines.push(` Failure pressure : ${negatives} negative ${pluralize(negatives, 'signal')}`);
99
101
  }
100
102
  lines.push(' Show the buyer : npx thumbgate cost');
103
+ lines.push(' Pro sync value : keep these lessons/rules visible across laptops, CI, containers, and agent runtimes');
101
104
  lines.push(` Solo Pro : ${trackedProUrl('cli_stats_receipt', 'proof_seen')}`);
102
105
  lines.push(' Team workflow : https://thumbgate.ai/#workflow-sprint-intake');
103
106
  lines.push('');
@@ -967,6 +967,20 @@ function computeAnalyticsSummary(feedbackDir, options = {}) {
967
967
  return {
968
968
  window: telemetry.window || analyticsWindow,
969
969
  telemetry,
970
+ firstPartyTrafficQuality: telemetry.trafficQuality || {
971
+ rawEvents: telemetry.totalEvents || 0,
972
+ externalEvents: 0,
973
+ excludedEvents: 0,
974
+ exclusionRate: 0,
975
+ byAudience: {},
976
+ byExclusionReason: {},
977
+ external: {
978
+ uniqueVisitors: 0,
979
+ pageViews: 0,
980
+ checkoutStarts: 0,
981
+ },
982
+ verdict: 'missing',
983
+ },
970
984
  funnel: {
971
985
  visitors: uniqueVisitors,
972
986
  sessions: telemetry.visitors ? telemetry.visitors.uniqueSessions || 0 : 0,
@@ -1149,16 +1163,42 @@ function computeInstrumentationReadiness(analytics, billing) {
1149
1163
  const coverage = billing && billing.coverage ? billing.coverage : {};
1150
1164
  const telemetry = analytics.telemetry || {};
1151
1165
  const visitors = telemetry.visitors || {};
1166
+ const quality = telemetry.trafficQuality || analytics.firstPartyTrafficQuality || {};
1167
+ const external = quality.external || {};
1152
1168
  const cli = telemetry.cli || {};
1169
+ const plausibleExportConfigured = Boolean(
1170
+ process.env.PLAUSIBLE_API_KEY && (process.env.PLAUSIBLE_SITE_ID || process.env.PLAUSIBLE_DOMAIN)
1171
+ );
1172
+ const posthogExportConfigured = Boolean(
1173
+ (process.env.POSTHOG_PERSONAL_API_KEY || process.env.POSTHOG_API_KEY) && process.env.POSTHOG_PROJECT_ID
1174
+ );
1175
+ const ga4ExportConfigured = Boolean(
1176
+ process.env.GA4_PROPERTY_ID && (process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_CLIENT_EMAIL)
1177
+ );
1178
+ const dashboardGradeExportReady = plausibleExportConfigured || posthogExportConfigured || ga4ExportConfigured;
1153
1179
 
1154
1180
  return {
1155
- plausibleConfigured: /plausible\.io\/js\/script\.js|\/js\/analytics\.js/.test(landingPage),
1181
+ plausibleConfigured: /plausible\.io\/js\/script(?:\.tagged-events)?\.js|\/js\/analytics\.js/.test(landingPage),
1156
1182
  ga4Configured: Boolean(runtimeConfig.gaMeasurementId),
1157
1183
  googleSearchConsoleConfigured: Boolean(runtimeConfig.googleSiteVerification),
1158
1184
  softwareApplicationSchemaPresent: /"@type": "SoftwareApplication"/.test(landingPage),
1159
1185
  faqSchemaPresent: /"@type": "FAQPage"/.test(landingPage),
1160
1186
  telemetryEventsPresent: (telemetry.totalEvents || 0) > 0,
1161
1187
  uniqueVisitorsTracked: visitors.uniqueVisitors || 0,
1188
+ rawTelemetryEvents: quality.rawEvents || telemetry.totalEvents || 0,
1189
+ externalTelemetryEvents: quality.externalEvents || 0,
1190
+ excludedTelemetryEvents: quality.excludedEvents || 0,
1191
+ externalVisitorsTracked: external.uniqueVisitors || 0,
1192
+ externalPageViewsTracked: external.pageViews || 0,
1193
+ externalCheckoutStartsTracked: external.checkoutStarts || 0,
1194
+ externalVisitorPathsTracked: Array.isArray(external.visitorPaths) ? external.visitorPaths.length : 0,
1195
+ internalTestPollutionRate: quality.exclusionRate || 0,
1196
+ trafficQualityVerdict: quality.verdict || 'missing',
1197
+ topExcludedTrafficReason: quality.topExclusionReason || null,
1198
+ plausibleExportConfigured,
1199
+ posthogExportConfigured,
1200
+ ga4ExportConfigured,
1201
+ dashboardGradeExportReady,
1162
1202
  cliInstallsTracked: cli.uniqueInstalls || 0,
1163
1203
  funnelEventsPresent: (analytics.reconciliation.telemetryCheckoutStarts || 0) > 0,
1164
1204
  seoSignalsPresent: (analytics.seo.landingViews || 0) > 0,
@@ -1865,8 +1905,10 @@ function printDashboard(data) {
1865
1905
  console.log('');
1866
1906
  console.log('\uD83D\uDCBC Growth Analytics');
1867
1907
  console.log(` Unique Visitors : ${analytics.trafficMetrics.visitors}`);
1908
+ console.log(` External Visitors: ${instrumentation.externalVisitorsTracked}`);
1868
1909
  console.log(` Sessions : ${analytics.trafficMetrics.sessions}`);
1869
1910
  console.log(` Page Views : ${analytics.trafficMetrics.pageViews}`);
1911
+ console.log(` External Views : ${instrumentation.externalPageViewsTracked}`);
1870
1912
  console.log(` CTA Clicks : ${analytics.trafficMetrics.ctaClicks}`);
1871
1913
  console.log(` Leads : ${analytics.funnel.acquisitionLeads}`);
1872
1914
  console.log(` Sprint Leads : ${analytics.pipeline.workflowSprintLeads.total}`);
@@ -1877,6 +1919,7 @@ function printDashboard(data) {
1877
1919
  console.log(` Booked Revenue : $${(analytics.revenue.bookedRevenueCents / 100).toFixed(2)}`);
1878
1920
  console.log(` Matched Journeys : ${analytics.reconciliation.matchedPaidOrders}/${analytics.reconciliation.telemetryCheckoutStarts}`);
1879
1921
  console.log(` Buyer Loss : ${analytics.buyerLoss.totalSignals}`);
1922
+ console.log(` Data Quality : ${analytics.firstPartyTrafficQuality.verdict} (${instrumentation.excludedTelemetryEvents}/${instrumentation.rawTelemetryEvents} excluded)`);
1880
1923
  if (analytics.telemetry.visitors.topSource) {
1881
1924
  console.log(` Top Source : ${analytics.telemetry.visitors.topSource.key} (${analytics.telemetry.visitors.topSource.count}\u00D7)`);
1882
1925
  }
@@ -1896,6 +1939,15 @@ function printDashboard(data) {
1896
1939
  console.log(` GA4 : ${instrumentation.ga4Configured ? 'configured' : 'missing'}`);
1897
1940
  console.log(` Search Console : ${instrumentation.googleSearchConsoleConfigured ? 'configured' : 'missing'}`);
1898
1941
  console.log(` Telemetry Events : ${instrumentation.telemetryEventsPresent ? instrumentation.uniqueVisitorsTracked : 0} visitors`);
1942
+ console.log(` Clean Visitors : ${instrumentation.externalVisitorsTracked} external (${Math.round((instrumentation.internalTestPollutionRate || 0) * 100)}% internal/test/bot events)`);
1943
+ console.log(` Clean Paths : ${instrumentation.externalVisitorPathsTracked} first-party paths`);
1944
+ if (instrumentation.topExcludedTrafficReason) {
1945
+ console.log(` Top Exclusion : ${instrumentation.topExcludedTrafficReason.key} (${instrumentation.topExcludedTrafficReason.count}\u00D7)`);
1946
+ }
1947
+ console.log(` Plausible Export : ${instrumentation.plausibleExportConfigured ? 'configured' : 'missing API credentials'}`);
1948
+ console.log(` PostHog Export : ${instrumentation.posthogExportConfigured ? 'configured' : 'missing API credentials'}`);
1949
+ console.log(` GA4 Export : ${instrumentation.ga4ExportConfigured ? 'configured' : 'missing API credentials'}`);
1950
+ console.log(` Visitor Paths : ${instrumentation.dashboardGradeExportReady ? 'export-ready' : 'not dashboard-grade in repo'}`);
1899
1951
  console.log(` SEO Signals : ${instrumentation.seoSignalsPresent ? analytics.seo.landingViews : 0}`);
1900
1952
  console.log(` Buyer Loss : ${instrumentation.buyerLossSignalsPresent ? analytics.buyerLoss.totalSignals : 0}`);
1901
1953
  console.log(` Attribution : ${Math.round((instrumentation.trafficAttributionCoverage || 0) * 100)}% page-view coverage`);
@@ -55,6 +55,9 @@ const {
55
55
  const {
56
56
  evaluateSecurityScan,
57
57
  } = require('./security-scanner');
58
+ const { evaluateSequenceState } = loadOptionalModule('./sequence-guard', () => ({
59
+ evaluateSequenceState: () => null,
60
+ }));
58
61
  const { getAutoGatesPath } = require('./auto-promote-gates');
59
62
  const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
60
63
 
@@ -1953,12 +1956,12 @@ function buildBlockActionProCta() {
1953
1956
  if (totalBlocks < 5) return null; // Too early — let them experience the product
1954
1957
 
1955
1958
  if (totalBlocks < 25) {
1956
- return '\n\n💡 Pro: sync rules across machines + dashboard analytics → thumbgate.ai/go/pro';
1959
+ return '\n\n💡 Pro: keep this rule synced across laptops, CI, containers, and agent runtimes → thumbgate.ai/go/pro';
1957
1960
  }
1958
1961
  if (totalBlocks < 100) {
1959
- return `\n\n💡 ${totalBlocks} actions blocked. Pro keeps rules in sync everywhere → thumbgate.ai/go/pro ($19/mo)`;
1962
+ return `\n\n💡 ${totalBlocks} actions blocked. Pro keeps these lessons/rules synced everywhere → thumbgate.ai/go/pro ($19/mo)`;
1960
1963
  }
1961
- return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use this → thumbgate.ai/go/pro`;
1964
+ return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use shared hosted enforcement → thumbgate.ai/go/pro`;
1962
1965
  } catch (_) {
1963
1966
  return null;
1964
1967
  }
@@ -2132,8 +2135,9 @@ function buildRecentCorrectiveActionsContext(options = {}) {
2132
2135
  function buildRelevantLessonContext(toolName, toolInput) {
2133
2136
  if (!toolName) return null;
2134
2137
 
2135
- const { retrieveRelevantLessons } = loadOptionalModule('./lesson-retrieval', () => ({
2138
+ const { retrieveRelevantLessons, calculateRetrievalEntropy } = loadOptionalModule('./lesson-retrieval', () => ({
2136
2139
  retrieveRelevantLessons: () => [],
2140
+ calculateRetrievalEntropy: () => 0,
2137
2141
  }));
2138
2142
 
2139
2143
  // Extract a searchable action context from the tool input
@@ -2142,23 +2146,77 @@ function buildRelevantLessonContext(toolName, toolInput) {
2142
2146
 
2143
2147
  try {
2144
2148
  const lessons = retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
2145
- // retrieveRelevantLessons already filters at relevanceScore > 0.1 internally;
2146
- // any negative lesson that survives retrieval is relevant enough to surface.
2147
- const negative = lessons.filter((l) => l.signal === 'negative');
2148
- if (negative.length === 0) return null;
2149
-
2150
- const formatted = negative.map((l) => {
2151
- const title = (l.title || '').replace(/^MISTAKE:\s*/, '').slice(0, 140);
2152
- const advice = extractAvoidanceAdvice(l.content);
2153
- return advice ? ` • ${title}\n → ${advice}` : ` • ${title}`;
2154
- });
2155
2149
 
2156
- return `[ThumbGate] Past mistakes relevant to this action — read before proceeding:\n${formatted.join('\n')}`;
2150
+ const entropy = calculateRetrievalEntropy(lessons);
2151
+ if (entropy > 0.7) {
2152
+ recordStat("retrieval_entropy_high", "block");
2153
+ return { decision: "deny", gate: "knowledge-conflict-gate", message: "✗ THUMBGATE: Action blocked due to high Knowledge Entropy (conflicting past lessons).", severity: "high" };
2154
+ }
2155
+ return formatNegativeLessonContext(lessons);
2157
2156
  } catch {
2158
2157
  return null;
2159
2158
  }
2160
2159
  }
2161
2160
 
2161
+ /**
2162
+ * Async counterpart of buildRelevantLessonContext: uses HYBRID (dense embeddings +
2163
+ * lexical) retrieval so the agent is warned about semantically-related past mistakes
2164
+ * even when they share no keywords with the current action. Wired into runAsync.
2165
+ * Degrades to the lexical result automatically when no embedder is available.
2166
+ */
2167
+ async function buildRelevantLessonContextAsync(toolName, toolInput) {
2168
+ if (!toolName) return null;
2169
+
2170
+ const { retrieveRelevantLessonsAsync, retrieveRelevantLessons, calculateRetrievalEntropy } = loadOptionalModule(
2171
+ './lesson-retrieval',
2172
+ () => ({ retrieveRelevantLessonsAsync: null, retrieveRelevantLessons: () => [], calculateRetrievalEntropy: () => 0 }),
2173
+ );
2174
+
2175
+ const actionContext = extractActionContext(toolName, toolInput);
2176
+ if (!actionContext) return null;
2177
+
2178
+ try {
2179
+ const lessons = retrieveRelevantLessonsAsync
2180
+ ? await retrieveRelevantLessonsAsync(toolName, actionContext, { maxResults: 3 })
2181
+ : retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
2182
+
2183
+ // Knowledge Conflict Detection: if retrieved lessons have high sentiment entropy,
2184
+ // it indicates conflicting past evidence. Block and require human disambiguation.
2185
+ const entropy = calculateRetrievalEntropy(lessons);
2186
+ if (entropy > 0.7) {
2187
+ recordStat('retrieval_entropy_high', 'block');
2188
+ return {
2189
+ decision: 'deny',
2190
+ gate: 'knowledge-conflict-gate',
2191
+ message: '✗ THUMBGATE: Action blocked due to high Knowledge Entropy (conflicting past lessons). Please disambiguate your instructions or verify the intended behavior manually.',
2192
+ severity: 'high',
2193
+ };
2194
+ }
2195
+
2196
+ return formatNegativeLessonContext(lessons);
2197
+ } catch {
2198
+ return null;
2199
+ }
2200
+ }
2201
+
2202
+ /**
2203
+ * Shared formatter: render the negative (mistake) lessons that survived retrieval
2204
+ * into the PreToolUse warning block. Retrieval already filters by relevance, so any
2205
+ * negative lesson present is relevant enough to surface.
2206
+ */
2207
+ function formatNegativeLessonContext(lessons) {
2208
+ const negative = (lessons || []).filter((l) => l.signal === 'negative');
2209
+ if (negative.length === 0) return null;
2210
+
2211
+ const formatted = negative.map((l) => {
2212
+ const title = (l.title || '').replace(/^MISTAKE:\s*/, '').slice(0, 140);
2213
+ const advice = extractAvoidanceAdvice(l.content);
2214
+ return advice ? ` • ${title}\n → ${advice}` : ` • ${title}`;
2215
+ });
2216
+
2217
+ return `[ThumbGate] Past mistakes relevant to this action — read before proceeding:\n${formatted.join('\n')}`;
2218
+ }
2219
+
2162
2220
  function extractActionContext(toolName, toolInput) {
2163
2221
  if (!toolInput) return toolName;
2164
2222
  const parts = [toolName];
@@ -2196,6 +2254,12 @@ async function runAsync(input) {
2196
2254
 
2197
2255
  const toolName = input.tool_name || '';
2198
2256
  const toolInput = input.tool_input || {};
2257
+
2258
+ const sequenceGuard = evaluateSequenceState(toolName, toolInput);
2259
+ if (sequenceGuard && sequenceGuard.decision === 'deny') {
2260
+ return formatOutput(sequenceGuard);
2261
+ }
2262
+
2199
2263
  const result = await evaluateGatesAsync(toolName, toolInput);
2200
2264
 
2201
2265
  // Attach security warnings to allow/warn results
@@ -2208,11 +2272,18 @@ async function runAsync(input) {
2208
2272
  }
2209
2273
  }
2210
2274
 
2275
+
2211
2276
  const behavioralContext = buildBehavioralContext();
2212
- const lessonContext = buildRelevantLessonContext(toolName, toolInput);
2277
+ const lessonContext = await buildRelevantLessonContextAsync(toolName, toolInput);
2278
+
2279
+ if (lessonContext && lessonContext.decision === "deny") {
2280
+ return formatOutput(lessonContext);
2281
+ }
2282
+
2213
2283
  const recentContext = buildRecentCorrectiveActionsContext();
2214
2284
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
2215
2285
  return formatOutput(result, combinedContext);
2286
+
2216
2287
  }
2217
2288
 
2218
2289
  function run(input) {
@@ -2229,6 +2300,12 @@ function run(input) {
2229
2300
 
2230
2301
  const toolName = input.tool_name || '';
2231
2302
  const toolInput = input.tool_input || {};
2303
+
2304
+ const sequenceGuard = evaluateSequenceState(toolName, toolInput);
2305
+ if (sequenceGuard && sequenceGuard.decision === 'deny') {
2306
+ return formatOutput(sequenceGuard);
2307
+ }
2308
+
2232
2309
  const result = evaluateGates(toolName, toolInput);
2233
2310
 
2234
2311
  // Attach security warnings to allow/warn results
@@ -2241,11 +2318,18 @@ function run(input) {
2241
2318
  }
2242
2319
  }
2243
2320
 
2321
+
2244
2322
  const behavioralContext = buildBehavioralContext();
2245
2323
  const lessonContext = buildRelevantLessonContext(toolName, toolInput);
2324
+
2325
+ if (lessonContext && lessonContext.decision === "deny") {
2326
+ return formatOutput(lessonContext);
2327
+ }
2328
+
2246
2329
  const recentContext = buildRecentCorrectiveActionsContext();
2247
2330
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
2248
2331
  return formatOutput(result, combinedContext);
2332
+
2249
2333
  }
2250
2334
 
2251
2335
  // ---------------------------------------------------------------------------
@@ -2562,6 +2646,7 @@ module.exports = {
2562
2646
  buildBehavioralContext,
2563
2647
  buildRecentCorrectiveActionsContext,
2564
2648
  buildRelevantLessonContext,
2649
+ buildRelevantLessonContextAsync,
2565
2650
  extractActionContext,
2566
2651
  extractAvoidanceAdvice,
2567
2652
  mergeContextStrings,