thumbgate 1.30.0 → 1.31.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +54 -16
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +66 -8
- package/adapters/opencode/opencode.json +1 -1
- package/bench/observability-eval-suite.json +2 -2
- package/bin/cli.js +147 -31
- package/config/evals/generation-quality-golden.json +95 -0
- package/config/evals/rag-answer-quality-golden.json +91 -0
- package/config/evals/retrieval-hybrid-ablation.json +66 -0
- package/config/evals/retrieval-ranking-golden.json +522 -0
- package/config/gates/default.json +217 -50
- package/config/mcp-allowlists.json +212 -206
- package/config/model-tiers.json +7 -2
- package/glama.json +6 -0
- package/package.json +60 -8
- package/public/assets/diagrams/before-after.svg +17 -16
- package/public/assets/diagrams/hero-thumbs.svg +68 -0
- package/public/assets/diagrams/loop.svg +19 -13
- package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
- package/public/compare.html +1 -0
- package/public/dashboard.html +126 -28
- package/public/evaluations.html +1 -1
- package/public/index.html +142 -13
- package/public/numbers.html +3 -2
- package/public/pricing.html +143 -30
- package/scripts/a-plus-evidence-scorecard.js +303 -0
- package/scripts/async-eval-observability.js +36 -11
- package/scripts/audit-trail.js +37 -1
- package/scripts/auto-promote-gates.js +149 -34
- package/scripts/colbert-style-maxsim.js +236 -0
- package/scripts/cross-encoder-reranker.js +356 -126
- package/scripts/dashboard-chat.js +350 -17
- package/scripts/document-intake.js +283 -7
- package/scripts/eval-quality-suite.js +204 -0
- package/scripts/feedback-loop.js +115 -7
- package/scripts/feedback-paths.js +32 -13
- package/scripts/feedback-quality.js +53 -0
- package/scripts/filesystem-search.js +17 -7
- package/scripts/gates-engine.js +17 -0
- package/scripts/harness-tool-names.js +70 -0
- package/scripts/hook-runtime.js +10 -3
- package/scripts/lesson-db.js +16 -5
- package/scripts/lesson-embedding-index.js +67 -20
- package/scripts/lesson-embedding-maintenance.js +177 -0
- package/scripts/lesson-reranker.js +55 -9
- package/scripts/lesson-retrieval.js +305 -29
- package/scripts/lesson-search.js +22 -8
- package/scripts/llm-client.js +304 -15
- package/scripts/model-tier-router.js +593 -0
- package/scripts/pragmatic-hybrid-search.js +379 -0
- package/scripts/rag-document-pipeline.js +461 -0
- package/scripts/rag-structured-output.js +441 -0
- package/scripts/ragas-style-metrics.js +351 -0
- package/scripts/request-envelope.js +178 -0
- package/scripts/rerank-pipeline.js +370 -0
- package/scripts/rerank-quality-eval.js +155 -0
- package/scripts/retrieval-hybrid-ablation.js +120 -0
- package/scripts/retrieval-quality-tier.js +118 -0
- package/scripts/secret-scanner.js +395 -4
- package/scripts/self-distill-agent.js +7 -1
- package/scripts/self-healing-check.js +25 -0
- package/scripts/skill-packs.js +183 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/statusline-links.js +1 -1
- package/scripts/statusline.sh +8 -1
- package/scripts/telemetry-analytics.js +13 -1
- package/scripts/thumbgate-search.js +98 -6
- package/scripts/tier-budget-guard.js +186 -0
- package/scripts/tool-registry.js +46 -0
- package/scripts/vector-store.js +108 -4
- package/scripts/verify-marketing-pages-deployed.js +85 -3
- package/server.json +44 -0
- package/smithery.yaml +17 -0
- package/src/api/server.js +194 -13
package/scripts/tool-registry.js
CHANGED
|
@@ -248,6 +248,20 @@ const TOOLS = [
|
|
|
248
248
|
scope: MEMORY_SCOPE_SCHEMA,
|
|
249
249
|
requireScope: { type: 'boolean', description: 'Fail closed unless a complete four-field scope is supplied.' },
|
|
250
250
|
includeShared: { type: 'boolean', description: 'Include explicitly shared memories with scoped results. Defaults true.' },
|
|
251
|
+
filters: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
description: 'Pre-filter the candidate corpus before lexical or vector ranking.',
|
|
254
|
+
properties: {
|
|
255
|
+
domain: { type: ['string', 'array'], items: { type: 'string' } },
|
|
256
|
+
signal: { type: ['string', 'array'], items: { type: 'string' } },
|
|
257
|
+
source: { type: ['string', 'array'], items: { type: 'string' } },
|
|
258
|
+
toolsUsed: { type: ['string', 'array'], items: { type: 'string' } },
|
|
259
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
260
|
+
requireAllTags: { type: 'boolean' },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
queryRewrite: { type: 'boolean', description: 'Enable bounded deterministic synonym expansion when the original lexical match is weak. Defaults true.' },
|
|
264
|
+
includeRetrievalMeta: { type: 'boolean', description: 'Include path provenance such as lexical/dense pools and query variants.' },
|
|
251
265
|
},
|
|
252
266
|
required: ['toolName'],
|
|
253
267
|
},
|
|
@@ -276,6 +290,17 @@ const TOOLS = [
|
|
|
276
290
|
limit: { type: 'number', description: 'Maximum results to return (default 10)' },
|
|
277
291
|
source: { type: 'string', enum: ['all', 'feedback', 'context', 'rules', 'documents'], description: 'Restrict search to a single ThumbGate source.' },
|
|
278
292
|
signal: { type: 'string', enum: ['up', 'down', 'positive', 'negative'], description: 'Optional feedback-signal filter when searching feedback data.' },
|
|
293
|
+
filters: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
description: 'Pre-filter imported documents before chunk ranking.',
|
|
296
|
+
properties: {
|
|
297
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
298
|
+
requireAllTags: { type: 'boolean' },
|
|
299
|
+
sourceFormat: { type: 'string' },
|
|
300
|
+
sourceType: { type: 'string' },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
queryRewrite: { type: 'boolean', description: 'Enable bounded deterministic synonym expansion. Defaults true.' },
|
|
279
304
|
},
|
|
280
305
|
},
|
|
281
306
|
}),
|
|
@@ -1285,6 +1310,27 @@ const TOOLS = [
|
|
|
1285
1310
|
},
|
|
1286
1311
|
},
|
|
1287
1312
|
}),
|
|
1313
|
+
readOnlyTool({
|
|
1314
|
+
name: 'gate_check',
|
|
1315
|
+
// Harnesses with a native pre-tool hook (Claude Code, Codex, Gemini, Forge) get hard
|
|
1316
|
+
// enforcement and never need this. MCP-only harnesses (Cline, Cursor, OpenCode) have no
|
|
1317
|
+
// interception point, so this exposes the SAME gates engine as a tool the agent calls
|
|
1318
|
+
// before acting. adapters/cline/.clinerules has instructed agents to call
|
|
1319
|
+
// `thumbgate.gate_check` since the adapter shipped — but the tool did not exist, so
|
|
1320
|
+
// that enforcement was inert. This makes the documented contract real.
|
|
1321
|
+
description: 'Evaluate a proposed tool call against ThumbGate policy BEFORE executing it. Returns decision "block" (do not run the action; surface the reason) or "allow". Use this when about to run a shell command, write/edit a file, or take any irreversible action.',
|
|
1322
|
+
inputSchema: {
|
|
1323
|
+
type: 'object',
|
|
1324
|
+
required: ['tool_name'],
|
|
1325
|
+
properties: {
|
|
1326
|
+
tool_name: { type: 'string', description: 'The tool about to be invoked (e.g. Bash, Write, Edit)' },
|
|
1327
|
+
tool_input: {
|
|
1328
|
+
type: 'object',
|
|
1329
|
+
description: 'The proposed arguments, e.g. { "command": "rm -rf /" } for Bash or { "file_path": "..." } for Write',
|
|
1330
|
+
},
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
}),
|
|
1288
1334
|
readOnlyTool({
|
|
1289
1335
|
name: 'gate_stats',
|
|
1290
1336
|
description: 'Get gate enforcement statistics -- blocked count, warned count, top gates',
|
package/scripts/vector-store.js
CHANGED
|
@@ -80,6 +80,42 @@ function hasLocalTransformerProvider() {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function getOllamaEmbeddingConfig() {
|
|
84
|
+
const model = String(process.env.THUMBGATE_OLLAMA_EMBED_MODEL || '').trim();
|
|
85
|
+
let endpoint = String(
|
|
86
|
+
process.env.THUMBGATE_OLLAMA_ENDPOINT
|
|
87
|
+
|| process.env.OLLAMA_HOST
|
|
88
|
+
|| 'http://127.0.0.1:11434',
|
|
89
|
+
).trim();
|
|
90
|
+
if (endpoint && !/^https?:\/\//i.test(endpoint)) endpoint = `http://${endpoint}`;
|
|
91
|
+
while (endpoint.endsWith('/')) endpoint = endpoint.slice(0, -1);
|
|
92
|
+
return {
|
|
93
|
+
enabled: Boolean(model),
|
|
94
|
+
model,
|
|
95
|
+
endpoint,
|
|
96
|
+
timeoutMs: Math.max(
|
|
97
|
+
250,
|
|
98
|
+
Math.min(30_000, Number(process.env.THUMBGATE_OLLAMA_TIMEOUT_MS) || 10_000),
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Synchronous capability check used by hot-path routing. This reports only
|
|
105
|
+
* semantic providers, never the deterministic feature-hash degradation.
|
|
106
|
+
*/
|
|
107
|
+
function hasSemanticEmbeddingProvider() {
|
|
108
|
+
if (process.env.THUMBGATE_VECTOR_STUB_EMBED === 'true') return true;
|
|
109
|
+
if (getOllamaEmbeddingConfig().enabled) return true;
|
|
110
|
+
if (hasLocalTransformerProvider()) return true;
|
|
111
|
+
try {
|
|
112
|
+
const config = resolveGeminiEmbeddingConfig();
|
|
113
|
+
return config.provider === 'coreai' || Boolean(config.enabled && config.apiKey);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
function fnv1a32(value) {
|
|
84
120
|
let hash = 0x811c9dc5;
|
|
85
121
|
const bytes = Buffer.from(String(value), 'utf8');
|
|
@@ -98,7 +134,7 @@ function addHashedFeature(vector, feature, weight) {
|
|
|
98
134
|
}
|
|
99
135
|
|
|
100
136
|
function embedWithFeatureHash(text) {
|
|
101
|
-
const vector = Array(FEATURE_HASH_DIMENSIONS).fill(0);
|
|
137
|
+
const vector = new Array(FEATURE_HASH_DIMENSIONS).fill(0);
|
|
102
138
|
const tokens = String(text || '').toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [];
|
|
103
139
|
|
|
104
140
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
@@ -249,6 +285,43 @@ async function embedWithCoreAI(text, options = {}) {
|
|
|
249
285
|
throw new Error('Core AI local service did not return a valid embedding');
|
|
250
286
|
}
|
|
251
287
|
|
|
288
|
+
async function embedWithOllama(text, options = {}) {
|
|
289
|
+
const config = getOllamaEmbeddingConfig();
|
|
290
|
+
if (!config.enabled) {
|
|
291
|
+
throw new Error('Ollama embeddings require THUMBGATE_OLLAMA_EMBED_MODEL');
|
|
292
|
+
}
|
|
293
|
+
if (typeof fetch !== 'function') {
|
|
294
|
+
throw new Error('Ollama embeddings require global fetch. Use Node 18.18+.');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let response;
|
|
298
|
+
try {
|
|
299
|
+
response = await fetch(`${config.endpoint}/api/embed`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
302
|
+
body: JSON.stringify({
|
|
303
|
+
model: config.model,
|
|
304
|
+
input: String(text || ''),
|
|
305
|
+
truncate: true,
|
|
306
|
+
dimensions: options.outputDimensionality || undefined,
|
|
307
|
+
}),
|
|
308
|
+
signal: AbortSignal.timeout(config.timeoutMs),
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
throw new Error(`Ollama embedding service unavailable: ${error.message}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
throw new Error(`Ollama embedding request failed: ${response.status} ${response.statusText}`);
|
|
316
|
+
}
|
|
317
|
+
const payload = await response.json();
|
|
318
|
+
const vector = Array.isArray(payload.embeddings) ? payload.embeddings[0] : null;
|
|
319
|
+
if (!Array.isArray(vector) || vector.length === 0) {
|
|
320
|
+
throw new Error('Ollama embedding response did not include vector values');
|
|
321
|
+
}
|
|
322
|
+
return vector.map(Number);
|
|
323
|
+
}
|
|
324
|
+
|
|
252
325
|
async function embed(text, options = {}) {
|
|
253
326
|
if (process.env.THUMBGATE_VECTOR_STUB_EMBED === 'true') {
|
|
254
327
|
// Deterministic 384-dim unit vector: first element = 1.0, rest = 0.0
|
|
@@ -277,6 +350,28 @@ async function embed(text, options = {}) {
|
|
|
277
350
|
console.warn(`Core AI embedding failed, falling back to local: ${coreaiError.message}`);
|
|
278
351
|
}
|
|
279
352
|
}
|
|
353
|
+
const ollamaConfig = getOllamaEmbeddingConfig();
|
|
354
|
+
if (ollamaConfig.enabled) {
|
|
355
|
+
try {
|
|
356
|
+
const vector = await embedWithOllama(text, options);
|
|
357
|
+
_lastEmbeddingProfile = {
|
|
358
|
+
generatedAt: new Date().toISOString(),
|
|
359
|
+
source: 'local-ollama',
|
|
360
|
+
activeProfile: {
|
|
361
|
+
id: 'ollama',
|
|
362
|
+
model: ollamaConfig.model,
|
|
363
|
+
outputDimensionality: vector.length,
|
|
364
|
+
task: options.task || 'code retrieval',
|
|
365
|
+
rationale: 'Explicit local Ollama semantic embedding provider.',
|
|
366
|
+
qualityTier: 'production',
|
|
367
|
+
},
|
|
368
|
+
fallbackUsed: false,
|
|
369
|
+
};
|
|
370
|
+
return vector;
|
|
371
|
+
} catch (ollamaError) {
|
|
372
|
+
console.warn(`Ollama embedding failed, falling back: ${ollamaError.message}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
280
375
|
if (geminiConfig.enabled) {
|
|
281
376
|
try {
|
|
282
377
|
const vector = await embedWithGemini(text, options);
|
|
@@ -297,7 +392,9 @@ async function embed(text, options = {}) {
|
|
|
297
392
|
if (!geminiConfig.fallbackToLocal) {
|
|
298
393
|
throw geminiError;
|
|
299
394
|
}
|
|
300
|
-
|
|
395
|
+
// Do not log raw provider/user-controlled error text (Sonar jssecurity:S5145).
|
|
396
|
+
const code = geminiError && (geminiError.code || geminiError.name || 'Error');
|
|
397
|
+
console.warn(`Gemini embedding fallback: ${code}`);
|
|
301
398
|
}
|
|
302
399
|
}
|
|
303
400
|
if (hasLocalTransformerProvider()) {
|
|
@@ -314,6 +411,8 @@ async function embed(text, options = {}) {
|
|
|
314
411
|
}
|
|
315
412
|
|
|
316
413
|
const vector = embedWithFeatureHash(text);
|
|
414
|
+
// Feature-hash is a last-resort degrade, not production semantic quality.
|
|
415
|
+
// Callers (prove/eval/chat health) must treat quality_tier=degraded.
|
|
317
416
|
_lastEmbeddingProfile = {
|
|
318
417
|
generatedAt: new Date().toISOString(),
|
|
319
418
|
source: 'built-in',
|
|
@@ -322,9 +421,11 @@ async function embed(text, options = {}) {
|
|
|
322
421
|
model: 'ThumbGate feature hashing',
|
|
323
422
|
outputDimensionality: FEATURE_HASH_DIMENSIONS,
|
|
324
423
|
task: options.task || 'code retrieval',
|
|
325
|
-
rationale: '
|
|
424
|
+
rationale: 'DEGRADED: deterministic zero-dependency hash embedding — not semantic. Configure Gemini or local transformers for production retrieval quality.',
|
|
425
|
+
qualityTier: 'degraded',
|
|
326
426
|
},
|
|
327
|
-
fallbackUsed:
|
|
427
|
+
fallbackUsed: true,
|
|
428
|
+
fallbackReason: 'no_managed_or_transformer_embedder',
|
|
328
429
|
};
|
|
329
430
|
return vector;
|
|
330
431
|
}
|
|
@@ -440,4 +541,7 @@ module.exports = {
|
|
|
440
541
|
setGeminiEmbedderForTests,
|
|
441
542
|
truncateForEmbedding,
|
|
442
543
|
embedWithFeatureHash,
|
|
544
|
+
embedWithOllama,
|
|
545
|
+
getOllamaEmbeddingConfig,
|
|
546
|
+
hasSemanticEmbeddingProvider,
|
|
443
547
|
};
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* --json machine-readable report on stdout, suitable for piping
|
|
23
23
|
* into the GitHub Actions PR comment step.
|
|
24
24
|
* --quiet suppress per-route lines; only print the final summary.
|
|
25
|
+
* --max-attempts=N retry only transient transport/5xx failures.
|
|
26
|
+
* --retry-delay-ms=N bounded delay between transient retries.
|
|
25
27
|
*
|
|
26
28
|
* Exit code is 0 when every page passes, 1 if any sentinel is missing
|
|
27
29
|
* or any route returns non-200.
|
|
@@ -32,6 +34,8 @@ const path = require('node:path');
|
|
|
32
34
|
|
|
33
35
|
const DEFAULT_PROD_URL = 'https://thumbgate-production.up.railway.app';
|
|
34
36
|
const DEFAULT_TIMEOUT_MS = 12000;
|
|
37
|
+
const DEFAULT_MAX_ATTEMPTS = 2;
|
|
38
|
+
const DEFAULT_RETRY_DELAY_MS = 500;
|
|
35
39
|
const DEFAULT_MANIFEST_PATH = path.resolve(__dirname, '..', 'config', 'post-deploy-marketing-pages.json');
|
|
36
40
|
|
|
37
41
|
function parseArgs(argv = []) {
|
|
@@ -41,6 +45,8 @@ function parseArgs(argv = []) {
|
|
|
41
45
|
json: false,
|
|
42
46
|
quiet: false,
|
|
43
47
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
48
|
+
maxAttempts: DEFAULT_MAX_ATTEMPTS,
|
|
49
|
+
retryDelayMs: DEFAULT_RETRY_DELAY_MS,
|
|
44
50
|
};
|
|
45
51
|
for (const arg of argv) {
|
|
46
52
|
if (arg === '--json') out.json = true;
|
|
@@ -50,6 +56,12 @@ function parseArgs(argv = []) {
|
|
|
50
56
|
else if (arg.startsWith('--timeout-ms=')) {
|
|
51
57
|
const n = Number(arg.slice('--timeout-ms='.length));
|
|
52
58
|
if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
|
|
59
|
+
} else if (arg.startsWith('--max-attempts=')) {
|
|
60
|
+
const n = Number(arg.slice('--max-attempts='.length));
|
|
61
|
+
if (Number.isInteger(n) && n > 0 && n <= 5) out.maxAttempts = n;
|
|
62
|
+
} else if (arg.startsWith('--retry-delay-ms=')) {
|
|
63
|
+
const n = Number(arg.slice('--retry-delay-ms='.length));
|
|
64
|
+
if (Number.isInteger(n) && n >= 0 && n <= 5000) out.retryDelayMs = n;
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
return out;
|
|
@@ -80,7 +92,7 @@ function loadManifest(manifestPath) {
|
|
|
80
92
|
return parsed;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
async function
|
|
95
|
+
async function probePageOnce({ prodUrl, route, sentinel, mustNotContain = [], userAgent, fetchImpl, timeoutMs }) {
|
|
84
96
|
if (typeof fetchImpl !== 'function') {
|
|
85
97
|
return { route, ok: false, error: 'fetch_unavailable' };
|
|
86
98
|
}
|
|
@@ -97,7 +109,10 @@ async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAg
|
|
|
97
109
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
98
110
|
},
|
|
99
111
|
});
|
|
100
|
-
|
|
112
|
+
// Keep body-read transport failures visible to the outer retry classifier.
|
|
113
|
+
// Treating a disconnected 2xx stream as an empty body would incorrectly
|
|
114
|
+
// turn an operational failure into a deterministic sentinel mismatch.
|
|
115
|
+
const body = await res.text();
|
|
101
116
|
const sentinelPresent = body.includes(sentinel);
|
|
102
117
|
const forbiddenPresent = Array.isArray(mustNotContain)
|
|
103
118
|
? mustNotContain.filter((value) => body.includes(value))
|
|
@@ -122,7 +137,67 @@ async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAg
|
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
|
|
140
|
+
function isTransientProbeFailure(result = {}) {
|
|
141
|
+
if (result.ok) return false;
|
|
142
|
+
if (result.error && result.error !== 'fetch_unavailable') return true;
|
|
143
|
+
return result.status === 429 || result.status >= 500;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function waitForRetry(delayMs) {
|
|
147
|
+
if (!delayMs) return Promise.resolve();
|
|
148
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function probePage({
|
|
152
|
+
prodUrl,
|
|
153
|
+
route,
|
|
154
|
+
sentinel,
|
|
155
|
+
mustNotContain = [],
|
|
156
|
+
userAgent,
|
|
157
|
+
fetchImpl = globalThis.fetch,
|
|
158
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
159
|
+
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
160
|
+
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
|
161
|
+
}) {
|
|
162
|
+
const boundedAttempts = Number.isInteger(maxAttempts)
|
|
163
|
+
? Math.min(Math.max(maxAttempts, 1), 5)
|
|
164
|
+
: DEFAULT_MAX_ATTEMPTS;
|
|
165
|
+
let result;
|
|
166
|
+
for (let attempt = 1; attempt <= boundedAttempts; attempt += 1) {
|
|
167
|
+
// eslint-disable-next-line no-await-in-loop
|
|
168
|
+
result = await probePageOnce({
|
|
169
|
+
prodUrl,
|
|
170
|
+
route,
|
|
171
|
+
sentinel,
|
|
172
|
+
mustNotContain,
|
|
173
|
+
userAgent,
|
|
174
|
+
fetchImpl,
|
|
175
|
+
timeoutMs,
|
|
176
|
+
});
|
|
177
|
+
if (!isTransientProbeFailure(result) || attempt === boundedAttempts) {
|
|
178
|
+
return {
|
|
179
|
+
...result,
|
|
180
|
+
attempts: attempt,
|
|
181
|
+
recoveredAfterRetry: Boolean(result.ok && attempt > 1),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// A deploy can pass /health while the first buyer-page request still
|
|
185
|
+
// wakes a cold instance. Retry only transient failures; content contract
|
|
186
|
+
// mismatches remain immediate, deterministic failures.
|
|
187
|
+
// eslint-disable-next-line no-await-in-loop
|
|
188
|
+
await waitForRetry(retryDelayMs);
|
|
189
|
+
}
|
|
190
|
+
return { ...result, attempts: boundedAttempts, recoveredAfterRetry: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function runVerification({
|
|
194
|
+
prodUrl,
|
|
195
|
+
manifestPath,
|
|
196
|
+
fetchImpl = globalThis.fetch,
|
|
197
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
198
|
+
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
199
|
+
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
|
200
|
+
} = {}) {
|
|
126
201
|
const manifest = loadManifest(manifestPath);
|
|
127
202
|
const results = [];
|
|
128
203
|
// Sequential is fine — manifest has <20 entries; parallelism would
|
|
@@ -137,6 +212,8 @@ async function runVerification({ prodUrl, manifestPath, fetchImpl = globalThis.f
|
|
|
137
212
|
userAgent: entry.userAgent,
|
|
138
213
|
fetchImpl,
|
|
139
214
|
timeoutMs,
|
|
215
|
+
maxAttempts,
|
|
216
|
+
retryDelayMs,
|
|
140
217
|
});
|
|
141
218
|
results.push({ ...result, sentinel: entry.sentinel, description: entry.description });
|
|
142
219
|
}
|
|
@@ -183,6 +260,8 @@ async function main(argv) {
|
|
|
183
260
|
prodUrl: args.prodUrl,
|
|
184
261
|
manifestPath: args.manifestPath,
|
|
185
262
|
timeoutMs: args.timeoutMs,
|
|
263
|
+
maxAttempts: args.maxAttempts,
|
|
264
|
+
retryDelayMs: args.retryDelayMs,
|
|
186
265
|
});
|
|
187
266
|
} catch (error) {
|
|
188
267
|
process.stderr.write(`verify-marketing-pages-deployed FAILED: ${error.message}\n`);
|
|
@@ -199,9 +278,12 @@ async function main(argv) {
|
|
|
199
278
|
module.exports = {
|
|
200
279
|
DEFAULT_PROD_URL,
|
|
201
280
|
DEFAULT_TIMEOUT_MS,
|
|
281
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
282
|
+
DEFAULT_RETRY_DELAY_MS,
|
|
202
283
|
DEFAULT_MANIFEST_PATH,
|
|
203
284
|
parseArgs,
|
|
204
285
|
loadManifest,
|
|
286
|
+
isTransientProbeFailure,
|
|
205
287
|
probePage,
|
|
206
288
|
runVerification,
|
|
207
289
|
renderHuman,
|
package/server.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.IgorGanapolsky/thumbgate",
|
|
4
|
+
"description": "ThumbGate pre-action gates for AI coding agents. Captures thumbs-up/down feedback as lessons, promotes repeated failures into prevention rules, and evaluates tool calls over stdio MCP before they run.",
|
|
5
|
+
"title": "ThumbGate",
|
|
6
|
+
"websiteUrl": "https://thumbgate.ai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"source": "github",
|
|
9
|
+
"url": "https://github.com/IgorGanapolsky/ThumbGate"
|
|
10
|
+
},
|
|
11
|
+
"version": "1.31.0",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "npm",
|
|
15
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
|
+
"identifier": "thumbgate",
|
|
17
|
+
"version": "1.31.0",
|
|
18
|
+
"runtimeHint": "npx",
|
|
19
|
+
"runtimeArguments": [
|
|
20
|
+
{
|
|
21
|
+
"type": "positional",
|
|
22
|
+
"value": "-y"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"packageArguments": [
|
|
26
|
+
{
|
|
27
|
+
"type": "positional",
|
|
28
|
+
"value": "serve"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"transport": {
|
|
32
|
+
"type": "stdio"
|
|
33
|
+
},
|
|
34
|
+
"environmentVariables": [
|
|
35
|
+
{
|
|
36
|
+
"name": "THUMBGATE_MCP_PROFILE",
|
|
37
|
+
"description": "Optional MCP tool profile override (essential | default | readonly | locked). Omit to keep the factory least-privilege essential profile; only set when expanded tools are intentional.",
|
|
38
|
+
"isRequired": false,
|
|
39
|
+
"format": "string"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
startCommand:
|
|
2
|
+
type: "stdio"
|
|
3
|
+
configSchema:
|
|
4
|
+
type: "object"
|
|
5
|
+
properties:
|
|
6
|
+
mcpProfile:
|
|
7
|
+
type: "string"
|
|
8
|
+
description: "MCP profile to use (default, readonly, locked)"
|
|
9
|
+
default: "default"
|
|
10
|
+
required: []
|
|
11
|
+
commandFunction:
|
|
12
|
+
command: "npx"
|
|
13
|
+
args:
|
|
14
|
+
- "-y"
|
|
15
|
+
- "thumbgate"
|
|
16
|
+
- "serve"
|
|
17
|
+
description: "ThumbGate — pre-action gates for AI coding agents. Turns thumbs-down feedback into prevention rules and evaluates tool calls over stdio MCP (npx -y thumbgate serve) before they run. Not the HTTP API (npm start). Works with Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode, and any MCP-compatible agent."
|