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
@@ -53,10 +53,31 @@ function listJsonFiles(dirPath) {
53
53
  return results;
54
54
  }
55
55
 
56
+ const STOPWORDS = new Set([
57
+ 'a', 'about', 'above', 'after', 'again', 'against', 'all', 'am', 'an', 'and', 'any', 'are', 'arent', 'as', 'at',
58
+ 'be', 'because', 'been', 'before', 'being', 'below', 'between', 'both', 'but', 'by',
59
+ 'cant', 'cannot', 'could', 'couldnt', 'did', 'didnt', 'do', 'does', 'doesnt', 'doing', 'dont', 'down', 'during',
60
+ 'each', 'few', 'for', 'from', 'further', 'had', 'hadnt', 'has', 'hasnt', 'have', 'havent', 'having',
61
+ 'he', 'hed', 'hell', 'hes', 'her', 'here', 'heres', 'hers', 'herself', 'him', 'himself', 'his',
62
+ 'how', 'hows', 'i', 'id', 'ill', 'im', 'ive', 'if', 'in', 'into', 'is', 'isnt', 'it', 'its', 'itself',
63
+ 'lets', 'me', 'more', 'most', 'mustnt', 'my', 'myself',
64
+ 'no', 'nor', 'not', 'of', 'off', 'on', 'once', 'only', 'or', 'other', 'ought', 'our', 'ours', 'ourselves', 'out', 'over', 'own',
65
+ 'same', 'shant', 'she', 'shed', 'shell', 'shes', 'should', 'shouldnt', 'so', 'some', 'such',
66
+ 'than', 'that', 'thats', 'the', 'their', 'theirs', 'them', 'themselves', 'then', 'there', 'theres', 'these', 'they', 'theyd', 'theyll', 'theyre', 'theyve', 'this', 'those', 'through', 'to', 'too', 'under', 'until', 'up', 'very',
67
+ 'was', 'wasnt', 'we', 'wed', 'well', 'were', 'weve', 'werent', 'what', 'whats', 'when', 'whens', 'where', 'wheres', 'which', 'while', 'who', 'whos', 'whom', 'why', 'whys',
68
+ 'with', 'wont', 'would', 'wouldnt', 'you', 'youd', 'youll', 'youre', 'youve', 'your', 'yours', 'yourself', 'yourselves'
69
+ ]);
70
+
56
71
  function tokenize(text) {
57
72
  return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
58
73
  }
59
74
 
75
+ function getSearchTokens(queryText) {
76
+ const allTokens = tokenize(queryText);
77
+ const contentTokens = allTokens.filter(t => !STOPWORDS.has(t));
78
+ return contentTokens.length > 0 ? contentTokens : allTokens;
79
+ }
80
+
60
81
  function unique(arr) {
61
82
  return [...new Set(arr)];
62
83
  }
@@ -82,7 +103,7 @@ function substringBoost(query, text) {
82
103
  const q = query.toLowerCase();
83
104
  const t = text.toLowerCase();
84
105
  if (t.includes(q)) return 0.3;
85
- const words = q.split(/\s+/).filter((w) => w.length > 2);
106
+ const words = q.split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
86
107
  const matched = words.filter((w) => t.includes(w)).length;
87
108
  return words.length > 0 ? (matched / words.length) * 0.2 : 0;
88
109
  }
@@ -117,8 +138,12 @@ function scoreRecord(queryTokens, queryText, record) {
117
138
  const recency = recencyScore(record.timestamp);
118
139
  const signalBoost = record.signal === 'down' ? 0.05 : 0;
119
140
 
141
+ const matchScore = jaccard + substr;
142
+ const isWildcard = !queryText || queryText === '*';
143
+ const score = isWildcard ? (recency + signalBoost || 0.01) : (matchScore > 0 ? matchScore + recency + signalBoost : 0);
144
+
120
145
  return {
121
- score: jaccard + substr + recency + signalBoost,
146
+ score: Number(score.toFixed(4)),
122
147
  record,
123
148
  matchedTokens: unique(queryTokens).filter((t) => new Set(recordTokens).has(t)),
124
149
  };
@@ -129,7 +154,7 @@ function scoreRecord(queryTokens, queryText, record) {
129
154
  // ---------------------------------------------------------------------------
130
155
 
131
156
  function searchFeedbackLog(queryText, limit = 5, options = {}) {
132
- const logPath = path.join(getFeedbackDir(), 'feedback-log.jsonl');
157
+ const logPath = path.join(options.feedbackDir || getFeedbackDir(), 'feedback-log.jsonl');
133
158
  let records = readJsonl(logPath);
134
159
 
135
160
  // SQLite fallback: if JSONL is empty/tiny, pull records from the lesson DB
@@ -156,7 +181,7 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
156
181
 
157
182
  // Wildcard query: return all records sorted by recency
158
183
  const isWildcard = queryText === '*' || queryText === '';
159
- const queryTokens = isWildcard ? [] : tokenize(queryText);
184
+ const queryTokens = isWildcard ? [] : getSearchTokens(queryText);
160
185
 
161
186
  let scored = isWildcard
162
187
  ? records.map((r) => ({ score: recencyScore(r.timestamp) || 0.01, record: r, matchedTokens: [] }))
@@ -186,9 +211,9 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
186
211
  }
187
212
 
188
213
  function searchContextFs(queryText, limit = 5, options = {}) {
189
- const contextDir = getContextFsDir();
214
+ const contextDir = options.contextDir || (options.feedbackDir ? path.join(options.feedbackDir, 'contextfs') : getContextFsDir());
190
215
  const namespaces = options.namespaces || ['memory/error', 'memory/learning', 'rules', 'raw_history'];
191
- const queryTokens = tokenize(queryText);
216
+ const queryTokens = getSearchTokens(queryText);
192
217
  const scored = [];
193
218
 
194
219
  for (const ns of namespaces) {
@@ -221,12 +246,12 @@ function searchContextFs(queryText, limit = 5, options = {}) {
221
246
  }));
222
247
  }
223
248
 
224
- function searchPreventionRules(queryText, limit = 5) {
225
- const rulesPath = path.join(getFeedbackDir(), 'prevention-rules.md');
249
+ function searchPreventionRules(queryText, limit = 5, options = {}) {
250
+ const rulesPath = path.join(options.feedbackDir || getFeedbackDir(), 'prevention-rules.md');
226
251
  if (!fs.existsSync(rulesPath)) return [];
227
252
 
228
253
  const content = fs.readFileSync(rulesPath, 'utf-8');
229
- const queryTokens = tokenize(queryText);
254
+ const queryTokens = getSearchTokens(queryText);
230
255
  const blocks = content.split(/^#{1,3}\s+/m).filter(Boolean);
231
256
 
232
257
  return blocks
@@ -252,7 +277,7 @@ function searchPreventionRules(queryText, limit = 5) {
252
277
  function searchAll(queryText, limit = 10, options = {}) {
253
278
  const feedbackResults = searchFeedbackLog(queryText, limit, options);
254
279
  const contextResults = searchContextFs(queryText, limit, options);
255
- const ruleResults = searchPreventionRules(queryText, limit);
280
+ const ruleResults = searchPreventionRules(queryText, limit, options);
256
281
 
257
282
  const merged = [
258
283
  ...feedbackResults.map((r) => ({ ...r, _source_type: 'feedback' })),
@@ -9,6 +9,7 @@ const { sequencePathFor } = require('./risk-scorer');
9
9
 
10
10
  const PROJECT_ROOT = path.join(__dirname, '..');
11
11
  const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
12
+ const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
12
13
 
13
14
  function loadGatesFile(filePath) {
14
15
  if (!fs.existsSync(filePath)) return [];
@@ -65,6 +66,15 @@ function calculateStats() {
65
66
  // sample can produce a misleading 0.0% floor.
66
67
  const bayesErrorRate = tryComputeBayesErrorRate();
67
68
 
69
+ // Calibration: per-gate assessment of whether block actions are well-supported
70
+ // by negative feedback, or potentially over/under-blocking without confirmation.
71
+ const calibration = computeCalibration(allGates);
72
+
73
+ // First-time fix rate: 1 - (recurringBlocks / totalBlocksAndWarns)
74
+ // Measures how often a single gate fire resolves the issue vs the agent retrying.
75
+ // Returns null when there is no recorded block/warn data yet.
76
+ const firstTimeFixRate = computeFirstTimeFixRate();
77
+
68
78
  return {
69
79
  totalGates: allGates.length,
70
80
  manualGates: manualGates.length,
@@ -77,10 +87,70 @@ function calculateStats() {
77
87
  lastPromotion,
78
88
  estimatedHoursSaved,
79
89
  bayesErrorRate,
90
+ calibration,
91
+ firstTimeFixRate,
80
92
  gates: allGates,
81
93
  };
82
94
  }
83
95
 
96
+ /**
97
+ * Assess each gate's calibration by comparing block occurrences to confirmed
98
+ * negative feedback counts. A gate with many blocks but no confirming negative
99
+ * feedback may be over-blocking; one with matching feedback is well-calibrated.
100
+ *
101
+ * @param {Array} gates - Combined array of manual + auto-promoted gate objects
102
+ * @returns {Array<{gateId: string, occurrences: number, action: string, calibrationNote: string}>}
103
+ */
104
+ function computeCalibration(gates) {
105
+ const calibration = [];
106
+ for (const gate of gates || []) {
107
+ if (!gate || !gate.id) continue;
108
+ const occurrences = Number(gate.occurrences || 0);
109
+ const action = gate.action || 'unknown';
110
+ // Only annotate gates with recorded occurrence data
111
+ if (occurrences === 0) continue;
112
+
113
+ if (action === 'block') {
114
+ const confirmedNegative = Number(gate.confirmedNegative || gate.negativeCount || 0);
115
+ let calibrationNote;
116
+ if (occurrences > 10 && confirmedNegative === 0) {
117
+ calibrationNote = `over-blocking (${occurrences} blocks, 0 confirmed)`;
118
+ } else if (confirmedNegative > 0) {
119
+ calibrationNote = `well-calibrated (${occurrences} blocks, ${confirmedNegative} confirmed)`;
120
+ } else {
121
+ // Low occurrence count with no feedback — not enough data yet
122
+ calibrationNote = `insufficient data (${occurrences} blocks, 0 confirmed)`;
123
+ }
124
+ calibration.push({ gateId: gate.id, occurrences, action, calibrationNote });
125
+ }
126
+ }
127
+ return calibration;
128
+ }
129
+
130
+ /**
131
+ * Compute the first-time fix rate from the persisted gate-stats.json file.
132
+ *
133
+ * firstTimeFixRate = 1 - (recurringBlocks / totalBlocksAndWarns)
134
+ *
135
+ * Returns null when there are no recorded block/warn events yet.
136
+ * Returns a number in [0, 1] otherwise, where 1.0 means every gate fire
137
+ * was a first-time occurrence and 0.0 means every gate fired at least twice.
138
+ */
139
+ function computeFirstTimeFixRate() {
140
+ try {
141
+ if (!fs.existsSync(STATS_PATH)) return null;
142
+ const raw = fs.readFileSync(STATS_PATH, 'utf8');
143
+ const data = JSON.parse(raw);
144
+ const totalBlocksAndWarns = (data.blocked || 0) + (data.warned || 0);
145
+ if (totalBlocksAndWarns === 0) return null;
146
+ const recurring = data.recurringBlocks || 0;
147
+ const rate = 1 - (recurring / totalBlocksAndWarns);
148
+ return Math.max(0, Math.min(1, rate));
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
84
154
  function tryComputeBayesErrorRate() {
85
155
  try {
86
156
  const seqPath = sequencePathFor();
@@ -142,6 +212,13 @@ function formatStats(stats) {
142
212
  lines.push(` Last promotion: ${formatLastPromotion(stats.lastPromotion)}`);
143
213
  lines.push(` Estimated time saved: ~${stats.estimatedHoursSaved} hours`);
144
214
  lines.push(` Bayes error rate: ${formatBayesErrorRate(stats.bayesErrorRate)}`);
215
+ lines.push(` First-time fix rate: ${formatFirstTimeFixRate(stats.firstTimeFixRate)}`);
216
+ if (Array.isArray(stats.calibration) && stats.calibration.length > 0) {
217
+ lines.push('Calibration:');
218
+ for (const entry of stats.calibration) {
219
+ lines.push(` - ${entry.gateId}: ${entry.calibrationNote}`);
220
+ }
221
+ }
145
222
  return lines.join('\n');
146
223
  }
147
224
 
@@ -153,6 +230,14 @@ function formatBayesErrorRate(rate) {
153
230
  return `${pct}% — high irreducible error; the feature set can't discriminate`;
154
231
  }
155
232
 
233
+ function formatFirstTimeFixRate(rate) {
234
+ if (rate === null || rate === undefined) return 'n/a (no blocks or warns recorded yet)';
235
+ const pct = (rate * 100).toFixed(1);
236
+ if (rate >= 0.95) return `${pct}% — agents fix issues on first block (excellent)`;
237
+ if (rate >= 0.80) return `${pct}% — most blocks resolved first time (good)`;
238
+ return `${pct}% — recurring blocks detected; agents may be ignoring gate feedback`;
239
+ }
240
+
156
241
  if (require.main === module) {
157
242
  try {
158
243
  const stats = calculateStats();
@@ -168,7 +253,11 @@ module.exports = {
168
253
  formatStats,
169
254
  formatLastPromotion,
170
255
  formatBayesErrorRate,
256
+ formatFirstTimeFixRate,
257
+ computeFirstTimeFixRate,
171
258
  loadGatesFile,
172
259
  tryComputeBayesErrorRate,
260
+ computeCalibration,
173
261
  MANUAL_GATES_PATH,
262
+ STATS_PATH,
174
263
  };