thumbgate 1.26.0 → 1.26.2

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.
package/bin/cli.js CHANGED
@@ -29,6 +29,7 @@
29
29
  'use strict';
30
30
 
31
31
  const fs = require('fs');
32
+ const os = require('os');
32
33
  const path = require('path');
33
34
  const crypto = require('crypto');
34
35
  const { execSync, execFileSync } = require('child_process');
@@ -610,6 +611,91 @@ function detectAgent(projectDir) {
610
611
  return null;
611
612
  }
612
613
 
614
+ async function setupVertex() {
615
+ const { execSync } = require('child_process');
616
+ console.log(`\nthumbgate setup-vertex v${pkgVersion()}`);
617
+ console.log(' Zero-friction Google Cloud & Vertex AI onboarding...');
618
+ console.log('');
619
+
620
+ // 1. Detect gcloud CLI
621
+ let activeAccount = '';
622
+ let activeProject = '';
623
+ try {
624
+ activeAccount = execSync('gcloud config get-value account', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
625
+ activeProject = execSync('gcloud config get-value project', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
626
+ } catch (_) {
627
+ console.log(' ⚠️ Google Cloud SDK (gcloud CLI) not detected or not logged in.');
628
+ console.log(' To automate setup, install the Google Cloud CLI and run: gcloud auth login');
629
+ console.log(' Otherwise, manually set the following variables in your .env file:');
630
+ console.log(' THUMBGATE_PROVIDER_MODE=vertex');
631
+ console.log(' VERTEX_PROJECT_ID=<your-gcp-project-id>');
632
+ console.log('');
633
+ return;
634
+ }
635
+
636
+ if (!activeAccount) {
637
+ console.log(' ⚠️ No active Google Cloud account set. Run: gcloud auth login');
638
+ return;
639
+ }
640
+
641
+ console.log(` Active Account : \x1b[36m${activeAccount}\x1b[0m`);
642
+ if (!activeProject) {
643
+ console.log(' ⚠️ No active Google Cloud project set. Run: gcloud config set project <PROJECT_ID>');
644
+ return;
645
+ }
646
+ console.log(` Active Project : \x1b[36m${activeProject}\x1b[0m`);
647
+
648
+ // Validate project ID matches GCP format before use in shell
649
+ if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(activeProject)) {
650
+ console.log(' ⚠️ Invalid GCP project ID format. Aborting.');
651
+ return;
652
+ }
653
+
654
+ // 2. Auto-enable Vertex AI API
655
+ console.log(' ⚙️ Enabling Vertex AI API in your project (this can take a few seconds)...');
656
+ try {
657
+ execSync(`gcloud services enable aiplatform.googleapis.com --project=${activeProject}`, { stdio: 'inherit' });
658
+ console.log(' ✅ Vertex AI API successfully enabled.');
659
+ } catch (err) {
660
+ console.log(' ⚠️ Could not programmatically enable Vertex AI API. Please make sure your billing is open.');
661
+ }
662
+
663
+ // 3. Write env config to .env
664
+ const envPath = path.join(process.cwd(), '.env');
665
+ let envContent = '';
666
+ if (fs.existsSync(envPath)) {
667
+ envContent = fs.readFileSync(envPath, 'utf8');
668
+ }
669
+
670
+ // Helper to update or append env values
671
+ function updateEnvKey(content, key, value) {
672
+ const regex = new RegExp(`^${key}=.*$`, 'm');
673
+ if (regex.test(content)) {
674
+ return content.replace(regex, `${key}=${value}`);
675
+ }
676
+ return content.trim() + `\n${key}=${value}\n`;
677
+ }
678
+
679
+ let updatedContent = updateEnvKey(envContent, 'THUMBGATE_PROVIDER_MODE', 'vertex');
680
+ updatedContent = updateEnvKey(updatedContent, 'VERTEX_PROJECT_ID', activeProject);
681
+
682
+ fs.writeFileSync(envPath, updatedContent, 'utf8');
683
+ console.log(' ✅ Wrote configuration to local .env file.');
684
+
685
+ // 4. Print gorgeous success activation box
686
+ console.log('');
687
+ console.log(' ╭──────────────────────────────────────────────────────────╮');
688
+ console.log(' │ 🎉 Vertex AI Setup Complete — ZERO FRICTION! │');
689
+ console.log(' │ │');
690
+ console.log(' │ ThumbGate is now fully wired to your GCP environment. │');
691
+ console.log(' │ All agent checks will route securely via Vertex AI. │');
692
+ console.log(' │ │');
693
+ console.log(' │ Try a test run: │');
694
+ console.log(' │ npx thumbgate feedback-self-test │');
695
+ console.log(' ╰──────────────────────────────────────────────────────────╯');
696
+ console.log('');
697
+ }
698
+
613
699
  function quickStart() {
614
700
  const qsArgs = parseArgs(process.argv.slice(3));
615
701
  const projectDir = process.cwd();
@@ -845,17 +931,12 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
845
931
  // ---------------------------------------------------------------------------
846
932
  console.log('');
847
933
  console.log(' ╭──────────────────────────────────────────────────────────╮');
848
- console.log(' │ NEXT: Create your first prevention rule (30 seconds) │');
934
+ console.log(' │ NEXT: Prove feedback capture works │');
849
935
  console.log(' │ │');
850
- console.log(' │ When your AI agent makes a mistake, capture it: │');
936
+ console.log(' │ npx thumbgate feedback-self-test │');
851
937
  console.log(' │ │');
852
- console.log(' │ npx thumbgate capture --feedback=down \\ │');
853
- console.log(' │ --context="agent deleted prod config" \\ │');
854
- console.log(' │ --what-went-wrong="ran rm on .env" \\ │');
855
- console.log(' │ --what-to-change="never delete .env files" │');
856
- console.log(' │ │');
857
- console.log(' │ ThumbGate auto-promotes this to a prevention rule │');
858
- console.log(' │ that blocks the mistake from happening again. │');
938
+ console.log(' │ Then dogfood it in chat: │');
939
+ console.log(' │ thumbs down: agent skipped verification │');
859
940
  console.log(' ╰──────────────────────────────────────────────────────────╯');
860
941
  console.log('');
861
942
  printInitConversionPrompt(onboardingEmail);
@@ -906,13 +987,35 @@ function capture() {
906
987
  return;
907
988
  }
908
989
 
909
- const signal = (args.feedback || '').toLowerCase();
990
+ // Parse pure positional arguments
991
+ const rawArgv = process.argv.slice(3);
992
+ const positionalArgs = [];
993
+ const BOOLEAN_FLAGS = new Set(['--json', '--verbose', '--quiet', '--dry-run', '--stats', '--summary', '--no-nudge', '--help']);
994
+ for (let i = 0; i < rawArgv.length; i++) {
995
+ const arg = rawArgv[i];
996
+ if (arg.startsWith('--')) {
997
+ if (!arg.includes('=') && !BOOLEAN_FLAGS.has(arg)) {
998
+ i++;
999
+ }
1000
+ } else {
1001
+ positionalArgs.push(arg);
1002
+ }
1003
+ }
1004
+
1005
+ let signal = (args.feedback || '').toLowerCase();
1006
+ if (!signal && positionalArgs[0]) {
1007
+ const firstPos = positionalArgs[0].toLowerCase();
1008
+ if (['up', 'down', 'thumbsup', 'thumbsdown', 'thumbs_up', 'thumbs_down', 'positive', 'negative'].some(v => firstPos.includes(v))) {
1009
+ signal = firstPos;
1010
+ }
1011
+ }
1012
+
910
1013
  const normalized = ['up', 'thumbsup', 'thumbs_up', 'positive'].some(v => signal.includes(v)) ? 'up'
911
1014
  : ['down', 'thumbsdown', 'thumbs_down', 'negative'].some(v => signal.includes(v)) ? 'down'
912
1015
  : signal;
913
1016
 
914
1017
  if (normalized !== 'up' && normalized !== 'down') {
915
- console.error('Missing or unrecognized --feedback=up|down');
1018
+ console.error('Missing or unrecognized --feedback=up|down (or positional argument)');
916
1019
  process.exit(1);
917
1020
  }
918
1021
 
@@ -922,11 +1025,30 @@ function capture() {
922
1025
  process.exit(1);
923
1026
  }
924
1027
 
1028
+ let context = args.context || '';
1029
+ if (!context && positionalArgs[1]) {
1030
+ context = positionalArgs[1];
1031
+ }
1032
+
1033
+ let whatWentWrong = args['what-went-wrong'];
1034
+ if (!whatWentWrong && positionalArgs[2]) {
1035
+ whatWentWrong = positionalArgs[2];
1036
+ } else if (!whatWentWrong && normalized === 'down' && context) {
1037
+ whatWentWrong = context;
1038
+ }
1039
+
1040
+ let whatToChange = args['what-to-change'];
1041
+ if (!whatToChange && positionalArgs[3]) {
1042
+ whatToChange = positionalArgs[3];
1043
+ } else if (!whatToChange && normalized === 'down' && context) {
1044
+ whatToChange = `avoid: ${context}`;
1045
+ }
1046
+
925
1047
  const result = captureFeedback({
926
1048
  signal: normalized,
927
- context: args.context || '',
928
- whatWentWrong: args['what-went-wrong'],
929
- whatToChange: args['what-to-change'],
1049
+ context: context,
1050
+ whatWentWrong: whatWentWrong,
1051
+ whatToChange: whatToChange,
930
1052
  whatWorked: args['what-worked'],
931
1053
  tags: args.tags,
932
1054
  gateAction: gateAction || undefined,
@@ -989,6 +1111,116 @@ function capture() {
989
1111
  }
990
1112
  }
991
1113
 
1114
+ function readJsonlEntries(filePath) {
1115
+ try {
1116
+ return fs.readFileSync(filePath, 'utf8')
1117
+ .split(/\r?\n/)
1118
+ .map((line) => line.trim())
1119
+ .filter(Boolean)
1120
+ .map((line) => JSON.parse(line));
1121
+ } catch (_) {
1122
+ return [];
1123
+ }
1124
+ }
1125
+
1126
+ function shellSingleQuote(value) {
1127
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
1128
+ }
1129
+
1130
+ function feedbackSelfTest() {
1131
+ const args = parseArgs(process.argv.slice(3));
1132
+ const signalArg = String(args.feedback || args.signal || 'down').toLowerCase();
1133
+ const normalized = ['up', 'thumbsup', 'thumbs_up', 'positive'].some((v) => signalArg.includes(v)) ? 'up'
1134
+ : ['down', 'thumbsdown', 'thumbs_down', 'negative'].some((v) => signalArg.includes(v)) ? 'down'
1135
+ : signalArg;
1136
+
1137
+ if (normalized !== 'up' && normalized !== 'down') {
1138
+ console.error('feedback-self-test needs --feedback=up|down when overriding the default.');
1139
+ process.exit(1);
1140
+ }
1141
+
1142
+ const previousFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
1143
+ const previousNoNudge = process.env.THUMBGATE_NO_NUDGE;
1144
+ const feedbackDir = args['feedback-dir']
1145
+ ? path.resolve(CWD, args['feedback-dir'])
1146
+ : args.persist
1147
+ ? null
1148
+ : fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-feedback-self-test-'));
1149
+ const isolated = Boolean(feedbackDir);
1150
+
1151
+ if (feedbackDir) process.env.THUMBGATE_FEEDBACK_DIR = feedbackDir;
1152
+ process.env.THUMBGATE_NO_NUDGE = '1';
1153
+
1154
+ try {
1155
+ const { captureFeedback, getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
1156
+ const context = args.context || `feedback self-test: typed thumbs ${normalized} reaches ThumbGate capture`;
1157
+ const result = captureFeedback({
1158
+ signal: normalized,
1159
+ context,
1160
+ whatWentWrong: normalized === 'down'
1161
+ ? (args['what-went-wrong'] || 'Need proof that feedback capture is wired in this runtime')
1162
+ : undefined,
1163
+ whatToChange: normalized === 'down'
1164
+ ? (args['what-to-change'] || 'Run a one-command self-test before claiming thumbs feedback is captured')
1165
+ : undefined,
1166
+ whatWorked: normalized === 'up'
1167
+ ? (args['what-worked'] || 'Feedback capture persisted and was verified by a self-test')
1168
+ : undefined,
1169
+ tags: args.tags || 'self-test,dogfood,feedback-capture',
1170
+ });
1171
+
1172
+ const paths = getFeedbackPaths();
1173
+ const feedbackRows = readJsonlEntries(paths.FEEDBACK_LOG_PATH);
1174
+ const memoryRows = readJsonlEntries(paths.MEMORY_LOG_PATH);
1175
+ const feedbackId = result.feedbackEvent && result.feedbackEvent.id;
1176
+ const memoryId = result.memoryRecord && result.memoryRecord.id;
1177
+ const feedbackStored = Boolean(feedbackId && feedbackRows.some((row) => row.id === feedbackId));
1178
+ const memoryStored = Boolean(memoryId && memoryRows.some((row) => row.id === memoryId));
1179
+ const ok = Boolean(result.accepted && feedbackStored && memoryStored);
1180
+
1181
+ const payload = {
1182
+ ok,
1183
+ command: 'feedback-self-test',
1184
+ signal: normalized,
1185
+ accepted: Boolean(result.accepted),
1186
+ feedbackId: feedbackId || null,
1187
+ memoryId: memoryId || null,
1188
+ feedbackStored,
1189
+ memoryStored,
1190
+ isolated,
1191
+ feedbackDir: paths.FEEDBACK_DIR,
1192
+ nextDogfoodCommand: `npx thumbgate capture --feedback=${normalized} --context=${shellSingleQuote(context)}`,
1193
+ };
1194
+
1195
+ if (args.json) {
1196
+ console.log(JSON.stringify(payload, null, 2));
1197
+ } else if (ok) {
1198
+ console.log('\nThumbGate feedback self-test: PASS');
1199
+ console.log('─'.repeat(50));
1200
+ console.log(` Captured : ${normalized} (${feedbackId})`);
1201
+ console.log(` Stored lesson: ${memoryId}`);
1202
+ console.log(` Storage : ${paths.FEEDBACK_DIR}`);
1203
+ console.log(` Mode : ${isolated ? 'isolated test store' : 'active ThumbGate store'}`);
1204
+ console.log('\nDogfood in chat with:');
1205
+ console.log(` thumbs ${normalized}: ${context}`);
1206
+ } else {
1207
+ console.log('\nThumbGate feedback self-test: FAIL');
1208
+ console.log('─'.repeat(50));
1209
+ console.log(` Accepted : ${payload.accepted}`);
1210
+ console.log(` Feedback stored: ${feedbackStored}`);
1211
+ console.log(` Memory stored : ${memoryStored}`);
1212
+ console.log(` Storage : ${paths.FEEDBACK_DIR}`);
1213
+ }
1214
+
1215
+ if (!ok) process.exit(2);
1216
+ } finally {
1217
+ if (previousFeedbackDir === undefined) delete process.env.THUMBGATE_FEEDBACK_DIR;
1218
+ else process.env.THUMBGATE_FEEDBACK_DIR = previousFeedbackDir;
1219
+ if (previousNoNudge === undefined) delete process.env.THUMBGATE_NO_NUDGE;
1220
+ else process.env.THUMBGATE_NO_NUDGE = previousNoNudge;
1221
+ }
1222
+ }
1223
+
992
1224
  function stats() {
993
1225
  trackEvent('cli_stats', { command: 'stats' });
994
1226
  const args = parseArgs(process.argv.slice(3));
@@ -2456,12 +2688,14 @@ function help() {
2456
2688
  console.log('');
2457
2689
  console.log('Common commands:');
2458
2690
  console.log(' init Detect agent and wire ThumbGate hooks');
2691
+ console.log(' feedback-self-test Prove thumbs capture works locally');
2459
2692
  console.log(' capture --feedback=up|down --context="<text>" Capture a thumbs signal as a stored lesson');
2460
2693
  console.log(' stats Approval rate, recent trend, blocked-pattern count');
2461
2694
  console.log(' lessons [query] Search promoted lessons');
2462
2695
  console.log(' explore Interactive TUI for lessons, gates, stats');
2463
2696
  console.log(' dashboard Open the local ThumbGate dashboard');
2464
2697
  console.log(' doctor Audit runtime isolation + bootstrap context');
2698
+ console.log(' brain [--write] Build the agent-readable context brain (lessons + rules + gates)');
2465
2699
  console.log(' pro ThumbGate Pro (dashboard, exports, sync)');
2466
2700
  console.log(' subscribe <email> Get the 5-min setup guide + weekly tips by email');
2467
2701
  console.log('');
@@ -2557,6 +2791,7 @@ function help() {
2557
2791
 
2558
2792
  console.log('Examples:');
2559
2793
  console.log(' npx thumbgate init');
2794
+ console.log(' npx thumbgate feedback-self-test');
2560
2795
  console.log(' npx thumbgate status --json');
2561
2796
  console.log(' npx thumbgate explore lessons --json');
2562
2797
  console.log(' npx thumbgate explore gates --json');
@@ -2598,6 +2833,8 @@ const _wantsHelp = _cliSubArgs.includes('--help') || _cliSubArgs.includes('-h');
2598
2833
  const SUBCOMMAND_HELP = {
2599
2834
  capture: 'Usage: npx thumbgate capture --feedback=up|down --context="..." [--what-worked="..."] [--what-went-wrong="..."] [--what-to-change="..."] [--tags=a,b]',
2600
2835
  feedback: 'Usage: npx thumbgate feedback --feedback=up|down --context="..." [--what-worked="..."] [--what-went-wrong="..."] [--what-to-change="..."] [--tags=a,b]',
2836
+ 'feedback-self-test': 'Usage: npx thumbgate feedback-self-test [--json] [--persist] [--feedback=up|down]\n\nCapture a synthetic thumbs signal and verify feedback-log + memory-log writes. Defaults to an isolated test store; use --persist to dogfood the active ThumbGate store.',
2837
+ dogfood: 'Usage: npx thumbgate dogfood [--json] [--persist] [--feedback=up|down]\n\nAlias for feedback-self-test.',
2601
2838
  stats: 'Usage: npx thumbgate stats\n\nShow gate enforcement statistics: blocked/warned counts, active gates, time saved.',
2602
2839
  trial: 'Usage: npx thumbgate trial\n\nShow Pro trial status, remaining days, and upgrade path.',
2603
2840
  pro: 'Usage: npx thumbgate pro [--activate <key>]\n\nLaunch the local Pro dashboard or activate a Pro license key.',
@@ -2613,6 +2850,8 @@ const SUBCOMMAND_HELP = {
2613
2850
  suggest: 'Usage: npx thumbgate suggest <gate-id>\n\nSuggest fixes for a specific gate based on lesson history.',
2614
2851
  cost: 'Usage: npx thumbgate cost [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nShow cumulative $ and tokens saved by PreToolUse gate blocks. Reads ~/.thumbgate/gate-stats.json.',
2615
2852
  savings: 'Usage: npx thumbgate savings [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nAlias for `thumbgate cost`.',
2853
+ 'setup-vertex': 'Usage: npx thumbgate setup-vertex\n\nAuto-enable Vertex AI API on GCP and write secure credentials to local .env.',
2854
+ brain: 'Usage: npx thumbgate brain [--write] [--json] [--limit=N]\n\nBuild the agent-readable "context brain" — a single artifact consolidating this\nrepo\'s lessons, prevention rules, active gates, and project context for a coding\nagent to read BEFORE acting. --write saves it to .thumbgate/BRAIN.md (versioned,\ndeterministic). --json emits the structured model. --limit caps lessons (default 15).',
2616
2855
  };
2617
2856
 
2618
2857
  if (_wantsHelp && COMMAND && SUBCOMMAND_HELP[COMMAND]) {
@@ -2620,6 +2859,128 @@ if (_wantsHelp && COMMAND && SUBCOMMAND_HELP[COMMAND]) {
2620
2859
  process.exit(0);
2621
2860
  }
2622
2861
 
2862
+ // -----------------------------------------------------------------------------
2863
+ // brain — consolidate ThumbGate's institutional memory (lessons + prevention
2864
+ // rules + active gates + project context) into a single, versioned,
2865
+ // agent-readable "context brain". The persistent context a coding agent should
2866
+ // read BEFORE acting, so it stops repeating mistakes. Composes the existing
2867
+ // explore-subcommands primitives; output is deterministic (no volatile
2868
+ // timestamp) so `.thumbgate/BRAIN.md` diffs only when the underlying memory
2869
+ // changes.
2870
+ // -----------------------------------------------------------------------------
2871
+ function buildBrainModel(opts = {}) {
2872
+ const { exploreLessons, exploreRules, exploreGates } = require(path.join(PKG_ROOT, 'scripts', 'explore-subcommands'));
2873
+ let feedbackDir;
2874
+ try {
2875
+ const { getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
2876
+ feedbackDir = getFeedbackPaths().FEEDBACK_DIR;
2877
+ } catch (_) { feedbackDir = path.join(CWD, '.thumbgate'); }
2878
+ const limit = Math.max(1, Number(opts.limit) || 15);
2879
+ const safe = (fn, fallback) => { try { return fn(); } catch (_) { return fallback; } };
2880
+ const lessons = safe(() => exploreLessons({ feedbackDir, pkgRoot: PKG_ROOT, limit, json: true }), { lessons: [], total: 0 });
2881
+ const rules = safe(() => exploreRules({ feedbackDir, pkgRoot: PKG_ROOT, json: true }), { rules: [], total: 0 });
2882
+ const gates = safe(() => exploreGates({ feedbackDir, pkgRoot: PKG_ROOT, json: true }), { gates: [], total: 0 });
2883
+ const instructionFiles = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules', '.thumbgate/config.json']
2884
+ .filter((f) => { try { return fs.existsSync(path.join(CWD, f)); } catch (_) { return false; } });
2885
+ return { lessons, rules, gates, project: { root: CWD, instructionFiles } };
2886
+ }
2887
+
2888
+ function renderBrainMarkdown(model) {
2889
+ const out = [];
2890
+ out.push('# ThumbGate Context Brain');
2891
+ out.push('');
2892
+ out.push('<!-- The persistent context a coding agent should read BEFORE acting in this repo.');
2893
+ out.push(' Generated by `npx thumbgate brain`. Rebuild after capturing feedback. -->');
2894
+ out.push('');
2895
+ out.push('> Institutional memory for AI coding agents working here: what has been');
2896
+ out.push('> learned, what to avoid, and what is enforced. Read this first.');
2897
+ out.push('');
2898
+
2899
+ out.push('## What this codebase taught its agents (lessons)');
2900
+ out.push('');
2901
+ const lessons = (model.lessons && model.lessons.lessons) || [];
2902
+ if (!lessons.length) {
2903
+ out.push('_No lessons captured yet. Run `npx thumbgate capture --feedback=down --context="what failed"`._');
2904
+ } else {
2905
+ for (const l of lessons) {
2906
+ const sig = String(l.signal || '').toLowerCase();
2907
+ const mark = (sig.includes('positive') || sig === 'up') ? '✅' : ((sig.includes('negative') || sig === 'down') ? '⛔' : '•');
2908
+ const enforced = l.confidence === 'active' ? ' _(enforced)_' : '';
2909
+ const ctx = String(l.context || '').replace(/\s+/g, ' ').trim().slice(0, 220);
2910
+ if (ctx) out.push(`- ${mark} ${ctx}${enforced}`);
2911
+ }
2912
+ }
2913
+ out.push('');
2914
+
2915
+ out.push('## Guardrails — do NOT repeat these (prevention rules)');
2916
+ out.push('');
2917
+ const rules = (model.rules && model.rules.rules) || [];
2918
+ if (!rules.length) {
2919
+ out.push('_No prevention rules yet._');
2920
+ } else {
2921
+ for (const r of rules) {
2922
+ out.push(`- **${String(r.title || '').trim()}**`);
2923
+ const body = String(r.body || '').split('\n')[0].trim();
2924
+ if (body && body !== r.title) out.push(` - ${body.slice(0, 200)}`);
2925
+ }
2926
+ }
2927
+ out.push('');
2928
+
2929
+ out.push('## Active enforcement (gates)');
2930
+ out.push('');
2931
+ const gates = (model.gates && model.gates.gates) || [];
2932
+ if (!gates.length) {
2933
+ out.push('_No active gates._');
2934
+ } else {
2935
+ for (const g of gates) {
2936
+ const occ = g.occurrences ? ` (${g.occurrences}×)` : '';
2937
+ out.push(`- \`${g.pattern || g.id}\` → **${g.action}**${occ}`);
2938
+ }
2939
+ }
2940
+ out.push('');
2941
+
2942
+ out.push('## Project context');
2943
+ out.push('');
2944
+ out.push(`- Repo root: \`${model.project.root}\``);
2945
+ const files = model.project.instructionFiles;
2946
+ out.push(`- Agent instruction files: ${files.length ? files.map((f) => '`' + f + '`').join(', ') : '_none detected_'}`);
2947
+ out.push('');
2948
+ out.push('---');
2949
+ const lt = (model.lessons && model.lessons.total) || 0;
2950
+ const rt = (model.rules && model.rules.total) || 0;
2951
+ const gt = (model.gates && model.gates.total) || 0;
2952
+ out.push(`_${lt} lessons · ${rt} rules · ${gt} gates. Rebuild with \`npx thumbgate brain --write\`._`);
2953
+ out.push('');
2954
+ return out.join('\n');
2955
+ }
2956
+
2957
+ function cmdBrain(args = {}) {
2958
+ const model = buildBrainModel({ limit: args.limit });
2959
+ if (args.json) { console.log(JSON.stringify(model, null, 2)); return 0; }
2960
+ const md = renderBrainMarkdown(model);
2961
+ if (args.write) {
2962
+ const dir = path.join(CWD, '.thumbgate');
2963
+ const target = path.join(dir, 'BRAIN.md');
2964
+ try {
2965
+ fs.mkdirSync(dir, { recursive: true });
2966
+ fs.writeFileSync(target, md);
2967
+ } catch (err) {
2968
+ // Surface a clean, actionable error instead of an uncaught stack trace
2969
+ // (e.g. permission denied, read-only filesystem).
2970
+ console.error(`Could not write ${target}: ${err && err.message ? err.message : err}`);
2971
+ return 1;
2972
+ }
2973
+ const lt = (model.lessons && model.lessons.total) || 0;
2974
+ const rt = (model.rules && model.rules.total) || 0;
2975
+ const gt = (model.gates && model.gates.total) || 0;
2976
+ console.log(`\u{1f9e0} Wrote context brain to .thumbgate/BRAIN.md (${lt} lessons · ${rt} rules · ${gt} gates).`);
2977
+ console.log(' Point your agent at it: add "Read .thumbgate/BRAIN.md first" to CLAUDE.md / AGENTS.md.');
2978
+ return 0;
2979
+ }
2980
+ process.stdout.write(md);
2981
+ return 0;
2982
+ }
2983
+
2623
2984
  switch (COMMAND) {
2624
2985
  case '--version':
2625
2986
  case '-v':
@@ -2666,6 +3027,16 @@ switch (COMMAND) {
2666
3027
  capture();
2667
3028
  upgradeNudge();
2668
3029
  break;
3030
+ case 'feedback-self-test':
3031
+ case 'dogfood':
3032
+ feedbackSelfTest();
3033
+ break;
3034
+ case 'setup-vertex':
3035
+ setupVertex().catch((err) => {
3036
+ console.error(err && err.message ? err.message : err);
3037
+ process.exit(1);
3038
+ });
3039
+ break;
2669
3040
  case 'stats':
2670
3041
  stats();
2671
3042
  upgradeNudge();
@@ -2685,6 +3056,11 @@ switch (COMMAND) {
2685
3056
  // a test runner stubs process.exit (flagged by gitar-bot on PR #2281).
2686
3057
  break;
2687
3058
  }
3059
+ case 'brain': {
3060
+ const brainArgs = parseArgs(process.argv.slice(3));
3061
+ process.exit(cmdBrain(brainArgs));
3062
+ break;
3063
+ }
2688
3064
  case 'billing:setup':
2689
3065
  require(path.join(PKG_ROOT, 'scripts', 'billing-setup'));
2690
3066
  break;
@@ -43,6 +43,9 @@
43
43
  "get_branch_governance",
44
44
  "approve_protected_action",
45
45
  "track_action",
46
+ "detect_noop",
47
+ "record_action_receipt",
48
+ "get_action_receipts",
46
49
  "verify_claim",
47
50
  "check_operational_integrity",
48
51
  "workflow_sentinel",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.26.0",
3
+ "version": "1.26.2",
4
4
  "description": "ThumbGate self-improving agent governance: thumbs-up/down turns every mistake into a prevention rule and blocks repeat patterns. 36 pre-action checks, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "repository": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "scripts/access-anomaly-detector.js",
19
+ "scripts/action-receipts.js",
19
20
  "scripts/activation-tracker.js",
20
21
  "scripts/agent-audit-trace.js",
21
22
  "scripts/agent-design-governance.js",
@@ -132,7 +133,10 @@
132
133
  "scripts/multimodal-retrieval-plan.js",
133
134
  "scripts/native-messaging-audit.js",
134
135
  "scripts/natural-language-harness.js",
136
+ "scripts/noop-detect.js",
135
137
  "scripts/obsidian-export.js",
138
+ "scripts/operational-dashboard.js",
139
+ "scripts/operational-summary.js",
136
140
  "scripts/operational-integrity.js",
137
141
  "scripts/oss-pr-opportunity-scout.js",
138
142
  "scripts/otel-declarative-config.js",
@@ -156,6 +160,7 @@
156
160
  "scripts/rag-precision-guardrails.js",
157
161
  "scripts/rate-limiter.js",
158
162
  "scripts/reasoning-efficiency-guardrails.js",
163
+ "scripts/repeat-metric.js",
159
164
  "scripts/reward-hacking-guardrails.js",
160
165
  "scripts/risk-scorer.js",
161
166
  "scripts/rlaif-self-audit.js",
@@ -343,7 +348,7 @@
343
348
  "social:prospect:bluesky:dry": "node scripts/social-bluesky-prospecting.js --dry-run",
344
349
  "social:reply-publish:bluesky:dry": "node scripts/social-reply-monitor-bluesky.js --publish-approved --dry-run",
345
350
  "test:python": "python3 -m pytest tests/*.py",
346
- "test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:mcp-tool-annotations && npm run test:mcp-oauth && npm run test:mcp-oauth-flow && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:lesson-semantic-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:predictive-credible-range && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:numbers-page && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:competitive-positioning-marketing && npm run test:medium-weekly && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:agent-reasoning-traces && npm run test:judge-reward && npm run test:llm-behavior-monitor && npm run test:prompting-os && npm run test:single-use-credential-gate && npm run test:structured-prompt-driven && npm run test:require-evidence-gate && npm run test:rule-validator && npm run test:bluesky-atproto && npm run test:social-reply-monitor-bluesky && npm run test:bluesky-delete-replies && npm run test:architect-kit-memory-bridge && npm run test:sonar-review-hotspots && npm run test:actionable-remediations && npm run test:gemini-embedding-policy && npm run test:agent-design-governance && npm run test:public-core-boundary && npm run test:hook-stop-verify-deploy && npm run test:hook-stop-anti-claim && npm run test:plausible-server-events && npm run test:activation-tracker && npm run test:unified-revenue-rollup && npm run test:conversion-rate-stats && npm run test:external-customer-audit && npm run test:telemetry-export && npm run test:stripe-checkout-diagnostic && npm run test:stripe-business-identity-probe && npm run test:revenue-observability-doctor && npm run test:public-bundle-ratchet && npm run test:stripe-payment-link-update && npm run test:ci-cd-hygiene-audit && npm run test:verify-marketing-pages-deployed && npm run test:install-email-capture && npm run test:install-shim && npm run test:hook-runtime-subcommands && npm run test:implementation-notes && npm run test:daily-block-cap && npm run test:free-to-paid-conversion-units && npm run test:metrics-real-endpoint && npm run test:cli-trial-and-help && npm run test:cost-cli && npm run test:silent-failure-cluster && npm run test:proof:truth && node --test tests/adaptive-reliability.test.js && npm run test:mcp-oauth-reviewer",
351
+ "test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:repeat-metric && npm run test:noop-detect && npm run test:action-receipts && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:mcp-tool-annotations && npm run test:mcp-oauth && npm run test:mcp-oauth-flow && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:lesson-semantic-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:predictive-credible-range && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:numbers-page && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:competitive-positioning-marketing && npm run test:medium-weekly && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:agent-reasoning-traces && npm run test:judge-reward && npm run test:llm-behavior-monitor && npm run test:prompting-os && npm run test:single-use-credential-gate && npm run test:structured-prompt-driven && npm run test:require-evidence-gate && npm run test:rule-validator && npm run test:bluesky-atproto && npm run test:social-reply-monitor-bluesky && npm run test:bluesky-delete-replies && npm run test:architect-kit-memory-bridge && npm run test:sonar-review-hotspots && npm run test:actionable-remediations && npm run test:gemini-embedding-policy && npm run test:agent-design-governance && npm run test:public-core-boundary && npm run test:hook-stop-verify-deploy && npm run test:hook-stop-anti-claim && npm run test:plausible-server-events && npm run test:activation-tracker && npm run test:unified-revenue-rollup && npm run test:conversion-rate-stats && npm run test:external-customer-audit && npm run test:telemetry-export && npm run test:stripe-checkout-diagnostic && npm run test:stripe-business-identity-probe && npm run test:revenue-observability-doctor && npm run test:public-bundle-ratchet && npm run test:stripe-payment-link-update && npm run test:ci-cd-hygiene-audit && npm run test:verify-marketing-pages-deployed && npm run test:install-email-capture && npm run test:install-shim && npm run test:hook-runtime-subcommands && npm run test:implementation-notes && npm run test:daily-block-cap && npm run test:free-to-paid-conversion-units && npm run test:metrics-real-endpoint && npm run test:cli-trial-and-help && npm run test:cost-cli && npm run test:silent-failure-cluster && npm run test:proof:truth && node --test tests/adaptive-reliability.test.js && npm run test:mcp-oauth-reviewer && npm run test:dfcx-gate && npm run test:dfcx-gate-server && npm run test:vertex-scorer",
347
352
  "test:hook-stop-verify-deploy": "node --test tests/hook-stop-verify-deploy.test.js",
348
353
  "test:hook-stop-anti-claim": "node --test tests/hook-stop-anti-claim.test.js",
349
354
  "test:plausible-server-events": "node --test tests/plausible-server-events.test.js tests/plausible-poller.test.js",
@@ -396,6 +401,12 @@
396
401
  "test:sync-version": "node --test tests/sync-version.test.js",
397
402
  "test:check-congruence": "node --test tests/check-congruence.test.js",
398
403
  "test:tool-registry": "node --test tests/tool-registry.test.js",
404
+ "test:dfcx-gate": "node --test tests/dfcx-webhook-gate.test.js",
405
+ "test:dfcx-gate-server": "node --test tests/dfcx-gate-server.test.js",
406
+ "test:vertex-scorer": "node --test tests/vertex-scorer.test.js",
407
+ "test:repeat-metric": "node --test tests/repeat-metric.test.js",
408
+ "test:noop-detect": "node --test tests/noop-detect.test.js",
409
+ "test:action-receipts": "node --test tests/action-receipts.test.js",
399
410
  "test:learn-hub": "node --test tests/learn-hub.test.js",
400
411
  "test:feedback-to-rules": "node --test tests/feedback-to-rules.test.js",
401
412
  "test:memory-firewall": "node --test tests/memory-firewall.test.js",
@@ -106,6 +106,7 @@
106
106
  <tr><td>Allocate spend to teams / features</td><td>✅</td><td>Per-gate breakdown via <code>byGate</code></td></tr>
107
107
  <tr><td>Stop a known-bad tool call before it hits the model</td><td>❌</td><td>✅ — PreToolUse gate fires, no API call made</td></tr>
108
108
  <tr><td>Promote a one-off failure into a permanent gate</td><td>❌</td><td>✅ — feedback loop + lesson DB</td></tr>
109
+ <tr><td>Surface gate candidates from <em>silent</em> failures (where no human ever gave thumbs-down)</td><td>❌</td><td>✅ — unsupervised silent-failure clustering, on by default</td></tr>
109
110
  <tr><td>Print conservative $ saved per day</td><td>❌</td><td>✅ — <code>thumbgate cost</code></td></tr>
110
111
  <tr><td>K8s pod-level allocation, finance-grade reporting</td><td>✅ (that's their core)</td><td>❌ (not our layer)</td></tr>
111
112
  </tbody>
@@ -117,6 +118,7 @@
117
118
  <li><strong>Every block is one fewer round trip.</strong> A blocked tool call doesn't reach the model. There's no "ThumbGate intercepted but the request still cost you" — the agent's tool-call execution is replaced with the gate's verdict, and the agent's next reasoning step takes the verdict as context instead of the failed result.</li>
118
119
  <li><strong>The avoided retry loop is the bulk of the saving.</strong> Failed tool calls don't just cost the call — they cost the model's next reasoning turn (which sees the failure and tries again), and often a third turn (which tries a different approach). Conservative 2k input + 600 output assumes one retry; in practice it's often more.</li>
119
120
  <li><strong>The numbers come from your local <code>gate-stats.json</code>.</strong> Not from a marketing model, not from "what enterprises like you saved." Your machine, your gates, your blocks.</li>
121
+ <li><strong>You don't have to give thumbs-down for the system to learn.</strong> ThumbGate's unsupervised <em>silent-failure clustering</em> runs by default — it mines your conversation logs for tool calls that failed (non-zero exit code or matched error patterns) but never got an explicit thumbs-down, clusters them by tool + argument shape, and surfaces them as gate candidates. The same false-positive-rate guardrail that vets human-feedback candidates vets these. Lazy users still get the savings. Opt out with <code>THUMBGATE_SILENT_FAILURE_CLUSTERING=0</code> if you want HITL-only.</li>
120
122
  </ol>
121
123
 
122
124
  <h2>Get the number on your machine</h2>