thumbgate 1.30.0 → 1.34.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 (92) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +54 -16
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/forge/forge.yaml +3 -3
  6. package/adapters/mcp/server-stdio.js +105 -10
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +168 -31
  10. package/config/evals/generation-quality-golden.json +95 -0
  11. package/config/evals/rag-answer-quality-golden.json +91 -0
  12. package/config/evals/retrieval-hybrid-ablation.json +66 -0
  13. package/config/evals/retrieval-ranking-golden.json +522 -0
  14. package/config/gates/claim-verifiers.example.json +42 -0
  15. package/config/gates/claim-verifiers.json +25 -0
  16. package/config/gates/default.json +217 -50
  17. package/config/mcp-allowlists.json +233 -206
  18. package/config/model-tiers.json +7 -2
  19. package/glama.json +6 -0
  20. package/hooks/hooks.json +1 -1
  21. package/package.json +69 -12
  22. package/public/assets/diagrams/before-after.svg +17 -16
  23. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  24. package/public/assets/diagrams/loop.svg +19 -13
  25. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  26. package/public/compare.html +1 -0
  27. package/public/dashboard.html +126 -28
  28. package/public/evaluations.html +1 -1
  29. package/public/index.html +142 -13
  30. package/public/numbers.html +3 -2
  31. package/public/pricing.html +143 -30
  32. package/scripts/a-plus-evidence-scorecard.js +303 -0
  33. package/scripts/agent-readiness.js +110 -0
  34. package/scripts/async-eval-observability.js +36 -11
  35. package/scripts/audit-trail.js +37 -1
  36. package/scripts/auto-promote-gates.js +149 -34
  37. package/scripts/auto-wire-hooks.js +20 -8
  38. package/scripts/cli-schema.js +14 -0
  39. package/scripts/colbert-style-maxsim.js +236 -0
  40. package/scripts/cross-encoder-reranker.js +356 -126
  41. package/scripts/dashboard-chat.js +350 -17
  42. package/scripts/document-intake.js +283 -7
  43. package/scripts/eval-quality-suite.js +204 -0
  44. package/scripts/feedback-loop.js +115 -7
  45. package/scripts/feedback-paths.js +32 -13
  46. package/scripts/feedback-quality.js +53 -0
  47. package/scripts/feedback-schema.js +3 -0
  48. package/scripts/file-ledger-lock.js +130 -0
  49. package/scripts/filesystem-search.js +17 -7
  50. package/scripts/financial-control-plane.js +1514 -0
  51. package/scripts/gates-engine.js +202 -7
  52. package/scripts/gemini-embedding-policy.js +1 -0
  53. package/scripts/harness-tool-names.js +70 -0
  54. package/scripts/hook-runtime.js +15 -3
  55. package/scripts/hook-stop-anti-claim.js +63 -3
  56. package/scripts/human-escalation.js +353 -41
  57. package/scripts/lesson-db.js +16 -5
  58. package/scripts/lesson-embedding-index.js +67 -20
  59. package/scripts/lesson-embedding-maintenance.js +177 -0
  60. package/scripts/lesson-reranker.js +55 -9
  61. package/scripts/lesson-retrieval.js +305 -29
  62. package/scripts/lesson-search.js +22 -8
  63. package/scripts/llm-client.js +304 -15
  64. package/scripts/model-tier-router.js +593 -0
  65. package/scripts/pragmatic-hybrid-search.js +379 -0
  66. package/scripts/provider-action-normalizer.js +11 -4
  67. package/scripts/rag-document-pipeline.js +461 -0
  68. package/scripts/rag-structured-output.js +441 -0
  69. package/scripts/ragas-style-metrics.js +351 -0
  70. package/scripts/request-envelope.js +178 -0
  71. package/scripts/rerank-pipeline.js +370 -0
  72. package/scripts/rerank-quality-eval.js +155 -0
  73. package/scripts/retrieval-hybrid-ablation.js +120 -0
  74. package/scripts/retrieval-quality-tier.js +118 -0
  75. package/scripts/secret-scanner.js +395 -4
  76. package/scripts/self-distill-agent.js +7 -1
  77. package/scripts/self-healing-check.js +25 -0
  78. package/scripts/skill-packs.js +183 -0
  79. package/scripts/slow-loop.js +72 -0
  80. package/scripts/statusline-links.js +1 -1
  81. package/scripts/statusline.sh +8 -1
  82. package/scripts/telemetry-analytics.js +13 -1
  83. package/scripts/thumbgate-search.js +98 -6
  84. package/scripts/tier-budget-guard.js +186 -0
  85. package/scripts/tool-registry.js +141 -5
  86. package/scripts/universal-claim-evaluator.js +767 -0
  87. package/scripts/vector-store.js +154 -17
  88. package/scripts/verify-marketing-pages-deployed.js +85 -3
  89. package/scripts/workflow-sentinel.js +77 -11
  90. package/server.json +44 -0
  91. package/smithery.yaml +17 -0
  92. package/src/api/server.js +196 -13
@@ -17,6 +17,7 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const {
19
19
  cacheUpdateHookCommand,
20
+ claimStopHookCommand,
20
21
  codexCacheUpdateHookCommand,
21
22
  codexPreToolHookCommand,
22
23
  codexSessionStartHookCommand,
@@ -36,7 +37,7 @@ function getHome() {
36
37
  // --- Hook definitions ---
37
38
  const CLAUDE_HOOKS = {
38
39
  PreToolUse: {
39
- matcher: 'Bash|Edit|Write|MultiEdit',
40
+ matcher: '.*',
40
41
  hooks: [{ type: 'command', command: preToolHookCommand() }],
41
42
  },
42
43
  UserPromptSubmit: {
@@ -49,11 +50,14 @@ const CLAUDE_HOOKS = {
49
50
  SessionStart: {
50
51
  hooks: [{ type: 'command', command: sessionStartHookCommand() }],
51
52
  },
53
+ Stop: {
54
+ hooks: [{ type: 'command', command: claimStopHookCommand() }],
55
+ },
52
56
  };
53
57
 
54
58
  const CODEX_HOOKS = {
55
59
  PreToolUse: {
56
- matcher: 'Bash',
60
+ matcher: '.*',
57
61
  hooks: [{ type: 'command', command: codexPreToolHookCommand() }],
58
62
  },
59
63
  UserPromptSubmit: {
@@ -384,6 +388,7 @@ function wireClaudeHooks(options) {
384
388
  UserPromptSubmit: /(hook-auto-capture\.sh|hook-auto-capture\b)/,
385
389
  PostToolUse: /(hook-thumbgate-cache-updater|cache-update\b)/,
386
390
  SessionStart: /(thumbgate_session_start\.sh|session-start\b)/,
391
+ Stop: /(hook-stop-anti-claim\.js|claim-stop-check\b)/,
387
392
  };
388
393
 
389
394
  for (const [lifecycle, hookDef] of Object.entries(CLAUDE_HOOKS)) {
@@ -456,8 +461,13 @@ function codexTomlConfigPath(configPath = codexConfigPath()) {
456
461
  return path.join(path.dirname(configPath), 'config.toml');
457
462
  }
458
463
 
464
+ function escapeRegexLiteral(value) {
465
+ return String(value).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
466
+ }
467
+
459
468
  function tomlSectionRegex(name) {
460
- return new RegExp(`^\\[${String(name).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\]\\n(?:^(?!\\[).*(?:\\n|$))*`, 'm');
469
+ const sectionName = escapeRegexLiteral(name);
470
+ return new RegExp(String.raw`^\[${sectionName}\]\n(?:^(?!\[).*(?:\n|$))*`, 'm');
461
471
  }
462
472
 
463
473
  function codexUserPromptTomlBlock() {
@@ -470,10 +480,11 @@ function upsertCodexUserPromptToml(configPath, dryRun = false) {
470
480
  const current = fs.existsSync(tomlPath) ? fs.readFileSync(tomlPath, 'utf8') : '';
471
481
  const canonicalBlock = codexUserPromptTomlBlock();
472
482
  const sectionRegex = tomlSectionRegex('hooks.user_prompt_submit');
473
- let next = current;
483
+ const existingMatch = sectionRegex.exec(current);
484
+ let next;
474
485
 
475
- if (sectionRegex.test(current)) {
476
- const existingBlock = current.match(sectionRegex)[0];
486
+ if (existingMatch) {
487
+ const existingBlock = existingMatch[0];
477
488
  if (existingBlock === canonicalBlock) {
478
489
  return { changed: false, settingsPath: tomlPath };
479
490
  }
@@ -610,7 +621,7 @@ function wireGeminiHooks(options) {
610
621
  if (!hookAlreadyPresent(settings.hooks.PreToolUse, preToolCmd)) {
611
622
  settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
612
623
  settings.hooks.PreToolUse.push({
613
- matcher: 'Bash',
624
+ matcher: '.*',
614
625
  hooks: [{ type: 'command', command: preToolCmd }],
615
626
  });
616
627
  added.push({ lifecycle: 'PreToolUse', command: preToolCmd });
@@ -661,7 +672,7 @@ function wireForgeHooks(options) {
661
672
  if (!hookAlreadyPresent(existing.hooks.PreToolUse, preToolCmd)) {
662
673
  existing.hooks.PreToolUse = existing.hooks.PreToolUse || [];
663
674
  existing.hooks.PreToolUse.push({
664
- matcher: 'Bash',
675
+ matcher: '.*',
665
676
  hooks: [{ type: 'command', command: preToolCmd }],
666
677
  });
667
678
  added.push({ lifecycle: 'PreToolUse', command: preToolCmd });
@@ -765,6 +776,7 @@ module.exports = {
765
776
  pruneStaleHooksInFile,
766
777
  CLAUDE_HOOKS,
767
778
  preToolHookCommand,
779
+ claimStopHookCommand,
768
780
  userPromptHookCommand,
769
781
  sessionStartHookCommand,
770
782
  };
@@ -505,6 +505,20 @@ const CLI_COMMANDS = [
505
505
  group: 'gates',
506
506
  flags: [],
507
507
  },
508
+ {
509
+ name: 'verify-claims',
510
+ aliases: ['verify-claim'],
511
+ description: 'Recheck factual claims against configured SQLite, filesystem, and JSON sources',
512
+ group: 'gates',
513
+ mcpTool: 'verify_claim',
514
+ flags: [
515
+ { name: 'claim', type: 'string', required: true, description: 'Factual claim text to verify' },
516
+ { name: 'config', type: 'string', description: 'Verifier config path (default .thumbgate/claim-verifiers.json)' },
517
+ { name: 'cwd', type: 'string', description: 'Root directory that contains configured sources' },
518
+ { name: 'advisory', type: 'boolean', description: 'Do not fail an otherwise parseable claim only because no verifier is configured' },
519
+ { name: 'json', type: 'boolean', description: 'Output a machine-readable verdict' },
520
+ ],
521
+ },
508
522
  {
509
523
  name: 'hermes-gate',
510
524
  description: 'Hermes Agent pre_tool_call hook: gate runtime tool calls (incl. skill_manage) before they run',
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ColBERT-style late interaction (MaxSim) for ThumbGate lesson reranking.
5
+ *
6
+ * This is NOT a pretrained ColBERT neural model. It implements the *interaction
7
+ * pattern* ColBERT made famous:
8
+ * - encode query and document as multi-vector bags (one vector per token)
9
+ * - score with MaxSim: sum over query tokens of max cosine vs any doc token
10
+ *
11
+ * Token vectors are deterministic hashed character-n-gram projections (local-only,
12
+ * no GPU, no network). That gives late interaction without shipping a 100MB model
13
+ * in the npm package — while remaining honest about model provenance.
14
+ *
15
+ * For true neural ColBERT, operators can plug vectors via `tokenEmbedder`.
16
+ *
17
+ * @see https://arxiv.org/abs/2004.12832 (ColBERT MaxSim)
18
+ */
19
+
20
+ const DEFAULT_DIM = 32;
21
+ const DEFAULT_NGRAM = 3;
22
+
23
+ /**
24
+ * Deterministic 32-bit hash (FNV-1a style) for seedable projections.
25
+ * @param {string} str
26
+ * @returns {number}
27
+ */
28
+ function hash32(str) {
29
+ let h = 0x811c9dc5;
30
+ for (let i = 0; i < str.length; i += 1) {
31
+ h ^= str.charCodeAt(i);
32
+ h = Math.imul(h, 0x01000193);
33
+ }
34
+ return h >>> 0;
35
+ }
36
+
37
+ /**
38
+ * Tokenize into lowercase word tokens (length >= 2).
39
+ * @param {string} text
40
+ * @returns {string[]}
41
+ */
42
+ function tokenize(text) {
43
+ if (!text) return [];
44
+ return String(text)
45
+ .toLowerCase()
46
+ .replace(/[^\w\s]/g, ' ')
47
+ .split(/[\s_]+/)
48
+ .filter((t) => t.length >= 2);
49
+ }
50
+
51
+ /**
52
+ * Character n-grams for a token (with edge markers).
53
+ * @param {string} token
54
+ * @param {number} n
55
+ * @returns {string[]}
56
+ */
57
+ function charNgrams(token, n = DEFAULT_NGRAM) {
58
+ const padded = `#${token}#`;
59
+ if (padded.length < n) return [padded];
60
+ const grams = [];
61
+ for (let i = 0; i <= padded.length - n; i += 1) {
62
+ grams.push(padded.slice(i, i + n));
63
+ }
64
+ return grams;
65
+ }
66
+
67
+ /**
68
+ * Build a unit-length multi-dim embedding for one token via hashed n-grams.
69
+ * @param {string} token
70
+ * @param {{ dim?: number, ngram?: number }} [opts]
71
+ * @returns {Float64Array}
72
+ */
73
+ function embedToken(token, opts = {}) {
74
+ const dim = opts.dim ?? DEFAULT_DIM;
75
+ const ngram = opts.ngram ?? DEFAULT_NGRAM;
76
+ const vec = new Float64Array(dim);
77
+ for (const gram of charNgrams(token, ngram)) {
78
+ const h = hash32(gram);
79
+ const idx = h % dim;
80
+ const sign = (h & 1) === 0 ? 1 : -1;
81
+ vec[idx] += sign;
82
+ // Second hash for denser projection (locality-sensitive bag)
83
+ const h2 = hash32(`${gram}:2`);
84
+ vec[h2 % dim] += ((h2 >> 1) & 1) === 0 ? 0.5 : -0.5;
85
+ }
86
+ // L2 normalize
87
+ let norm = 0;
88
+ for (let i = 0; i < dim; i += 1) norm += vec[i] * vec[i];
89
+ norm = Math.sqrt(norm) || 1;
90
+ for (let i = 0; i < dim; i += 1) vec[i] /= norm;
91
+ return vec;
92
+ }
93
+
94
+ /**
95
+ * Cosine similarity for unit vectors (dot product).
96
+ * @param {Float64Array|number[]} a
97
+ * @param {Float64Array|number[]} b
98
+ * @returns {number}
99
+ */
100
+ function cosine(a, b) {
101
+ const n = Math.min(a.length, b.length);
102
+ let s = 0;
103
+ for (let i = 0; i < n; i += 1) s += a[i] * b[i];
104
+ return s;
105
+ }
106
+
107
+ /**
108
+ * Encode text as a bag of token vectors (ColBERT multi-vector representation).
109
+ * @param {string} text
110
+ * @param {{ dim?: number, ngram?: number, maxTokens?: number, tokenEmbedder?: (t: string) => Float64Array|number[] }} [opts]
111
+ * @returns {{ tokens: string[], vectors: Array<Float64Array|number[]> }}
112
+ */
113
+ function encodeMultiVector(text, opts = {}) {
114
+ const maxTokens = opts.maxTokens ?? 64;
115
+ const tokens = tokenize(text).slice(0, maxTokens);
116
+ const embedder = opts.tokenEmbedder || ((t) => embedToken(t, opts));
117
+ const vectors = tokens.map((t) => embedder(t));
118
+ return { tokens, vectors };
119
+ }
120
+
121
+ /**
122
+ * ColBERT MaxSim: Σ_i max_j cos(q_i, d_j), normalized by |Q|.
123
+ * @param {Array<Float64Array|number[]>} queryVectors
124
+ * @param {Array<Float64Array|number[]>} docVectors
125
+ * @returns {number} score in [0, 1] approximately
126
+ */
127
+ function maxSim(queryVectors, docVectors) {
128
+ if (!queryVectors.length || !docVectors.length) return 0;
129
+ let total = 0;
130
+ for (const q of queryVectors) {
131
+ let best = -1;
132
+ for (const d of docVectors) {
133
+ const c = cosine(q, d);
134
+ if (c > best) best = c;
135
+ }
136
+ total += Math.max(0, best);
137
+ }
138
+ // Normalize by query length so longer queries don't dominate
139
+ return Math.min(1, total / queryVectors.length);
140
+ }
141
+
142
+ /**
143
+ * Score a (query, document) pair with ColBERT-style late interaction.
144
+ * @param {string} query
145
+ * @param {string} document
146
+ * @param {object} [opts]
147
+ * @returns {{ score: number, queryTokens: string[], docTokens: string[], mode: string }}
148
+ */
149
+ function scoreLateInteraction(query, document, opts = {}) {
150
+ const q = encodeMultiVector(query, opts);
151
+ const d = encodeMultiVector(document, opts);
152
+ const score = maxSim(q.vectors, d.vectors);
153
+ return {
154
+ score: Number(score.toFixed(6)),
155
+ queryTokens: q.tokens,
156
+ docTokens: d.tokens,
157
+ mode: opts.tokenEmbedder ? 'colbert-style-external' : 'colbert-style-hash',
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Rerank candidates by MaxSim late interaction.
163
+ * @param {string} query
164
+ * @param {Array<object>} candidates
165
+ * @param {{ topK?: number, textOf?: (c: object) => string, blendWeight?: number }} [options]
166
+ * @returns {Array<object>} candidates with maxSimScore + optional blend into rerankedScore
167
+ */
168
+ function rerankWithMaxSim(query, candidates, options = {}) {
169
+ const {
170
+ topK = 5,
171
+ textOf = defaultTextOf,
172
+ blendWeight = 0.55,
173
+ dim,
174
+ ngram,
175
+ maxTokens,
176
+ tokenEmbedder,
177
+ } = options;
178
+
179
+ if (!candidates || candidates.length === 0) return [];
180
+ if (candidates.length === 1) {
181
+ const only = candidates[0];
182
+ return [{
183
+ ...only,
184
+ maxSimScore: 1,
185
+ rerankedScore: only.rerankedScore ?? only.relevanceScore ?? 1,
186
+ lateInteractionMode: 'trivial',
187
+ }].slice(0, topK);
188
+ }
189
+
190
+ const qEnc = encodeMultiVector(query, { dim, ngram, maxTokens, tokenEmbedder });
191
+ const scored = candidates.map((c) => {
192
+ const docText = textOf(c);
193
+ const dEnc = encodeMultiVector(docText, { dim, ngram, maxTokens, tokenEmbedder });
194
+ const ms = maxSim(qEnc.vectors, dEnc.vectors);
195
+ const orig = Number(c.rerankedScore ?? c.relevanceScore ?? c.score ?? 0);
196
+ const blended = blendWeight * ms + (1 - blendWeight) * Math.max(0, Math.min(1, orig));
197
+ return {
198
+ ...c,
199
+ maxSimScore: Number(ms.toFixed(6)),
200
+ rerankedScore: Number(blended.toFixed(6)),
201
+ lateInteractionMode: tokenEmbedder ? 'colbert-style-external' : 'colbert-style-hash',
202
+ };
203
+ });
204
+
205
+ return scored
206
+ .sort((a, b) => (b.rerankedScore || 0) - (a.rerankedScore || 0))
207
+ .slice(0, topK);
208
+ }
209
+
210
+ function defaultTextOf(candidate) {
211
+ if (!candidate || typeof candidate !== 'object') return String(candidate || '');
212
+ return [
213
+ candidate.title,
214
+ candidate.whatWentWrong,
215
+ candidate.whatToChange,
216
+ candidate.howToAvoid,
217
+ candidate.summary,
218
+ candidate.content,
219
+ candidate.context,
220
+ Array.isArray(candidate.tags) ? candidate.tags.join(' ') : '',
221
+ ].filter(Boolean).join(' ');
222
+ }
223
+
224
+ module.exports = {
225
+ hash32,
226
+ tokenize,
227
+ charNgrams,
228
+ embedToken,
229
+ cosine,
230
+ encodeMultiVector,
231
+ maxSim,
232
+ scoreLateInteraction,
233
+ rerankWithMaxSim,
234
+ DEFAULT_DIM,
235
+ DEFAULT_NGRAM,
236
+ };