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
@@ -111,7 +111,8 @@ function isSparseAttentionFamily(modelFamily) {
111
111
 
112
112
  function resolveProviderMode(env = process.env) {
113
113
  const explicit = normalizeSlug(env.THUMBGATE_PROVIDER_MODE || env.THUMBGATE_MODEL_PROVIDER_MODE);
114
- if (explicit === 'local' || explicit === 'managed') return explicit;
114
+ if (explicit === 'local' || explicit === 'managed' || explicit === 'vertex') return explicit;
115
+ if (env.VERTEX_PROJECT_ID || env.VERTEX_API_ENDPOINT) return 'vertex';
115
116
  if (env.THUMBGATE_LOCAL_MODEL_FAMILY || env.THUMBGATE_LOCAL_MODEL_SERVER) return 'local';
116
117
  return 'managed';
117
118
  }
@@ -133,6 +134,7 @@ function resolveModelFamily(env = process.env) {
133
134
  }
134
135
 
135
136
  function buildBackendLabel(providerMode, modelFamily) {
137
+ if (providerMode === 'vertex') return 'Vertex AI secure cloud backend';
136
138
  if (providerMode === 'managed') return 'Managed API backend';
137
139
  if (modelFamily.startsWith('deepseek')) return 'Local DeepSeek sparse backend';
138
140
  if (modelFamily.startsWith('glm')) return 'Local GLM sparse backend';
@@ -148,14 +150,18 @@ function detectInferenceBackend(env = process.env) {
148
150
  && supportsSparseAttention
149
151
  && INDEXCACHE_SERVER_ENGINES.has(serverEngine);
150
152
  const indexCacheEnabled = indexCacheEligible && parseBoolean(env.THUMBGATE_INDEXCACHE_ENABLED, false);
151
- const id = providerMode === 'managed'
152
- ? 'managed-api'
153
- : supportsSparseAttention
154
- ? `local-${modelFamily}-sparse`
155
- : 'local-dense';
153
+ const id = providerMode === 'vertex'
154
+ ? 'vertex-api'
155
+ : providerMode === 'managed'
156
+ ? 'managed-api'
157
+ : supportsSparseAttention
158
+ ? `local-${modelFamily}-sparse`
159
+ : 'local-dense';
156
160
 
157
161
  let rationale = 'Baseline backend with no sparse-attention acceleration.';
158
- if (providerMode === 'managed') {
162
+ if (providerMode === 'vertex') {
163
+ rationale = 'Vertex AI secure cloud backend providing compliant enterprise Gemini models inside VPC boundary.';
164
+ } else if (providerMode === 'managed') {
159
165
  rationale = 'Managed API path does not expose sparse-attention kernel controls like IndexCache.';
160
166
  } else if (indexCacheEnabled) {
161
167
  rationale = `Local ${modelFamily} backend is sparse-attention capable and IndexCache-ready on ${serverEngine}.`;
@@ -336,7 +342,8 @@ function resolveModelRole(role, env) {
336
342
  const envKey = `THUMBGATE_MODEL_ROLE_${normalized.toUpperCase()}`;
337
343
  const modelFamily = resolveModelFamily(e);
338
344
  const isLocalGlm = modelFamily.startsWith('glm');
339
- const provider = isLocalGlm ? 'local' : 'gemini';
345
+ const providerMode = resolveProviderMode(e);
346
+ const provider = isLocalGlm ? 'local' : (providerMode === 'vertex' ? 'vertex' : 'gemini');
340
347
  const defaultModel = isLocalGlm ? (GLM_MODEL_ROLES[normalized] || MODEL_ROLES[normalized]) : MODEL_ROLES[normalized];
341
348
  const model = (e[envKey] && String(e[envKey]).trim()) || defaultModel;
342
349
  return { role: normalized, model, provider, envKey };
@@ -107,9 +107,15 @@ function portableMcpEntry(pkgVersion) {
107
107
  }
108
108
 
109
109
  function codexAutoUpdateCliEntry(commandArgs = []) {
110
+ // Fast-start from the installed runtime binary; only resolve @latest via npx
111
+ // when the runtime is absent (e.g. first launch). This matches the hook
112
+ // commands and must never block MCP server startup on a per-launch reinstall
113
+ // (a blocking `npm install thumbgate@latest` on every launch caused
114
+ // capture/serve timeouts on slow or offline networks — a failed install left
115
+ // the chained `exec` unreached, so the server never started).
110
116
  return {
111
117
  command: 'sh',
112
- args: ['-lc', publishedCliShellCommand('latest', commandArgs, { preferInstalled: false })],
118
+ args: ['-lc', publishedCliShellCommand('latest', commandArgs)],
113
119
  };
114
120
  }
115
121
 
@@ -2,6 +2,38 @@
2
2
  'use strict';
3
3
 
4
4
  const REQUIRED_SCOPE_FIELDS = ['entityId', 'projectId', 'processId', 'sessionId'];
5
+ const MEMORY_OS_LAYERS = Object.freeze([
6
+ {
7
+ id: 'file_layer',
8
+ name: 'File Layer',
9
+ purpose: 'Raw feedback, tool receipts, sessions, and memory rows are durably stored before interpretation.',
10
+ },
11
+ {
12
+ id: 'vector_db_layer',
13
+ name: 'Vector DB Layer',
14
+ purpose: 'Semantic retrieval can find related lessons without stuffing every raw memory into context.',
15
+ },
16
+ {
17
+ id: 'structured_facts_layer',
18
+ name: 'Structured Facts Layer',
19
+ purpose: 'Confirmed account, project, policy, and budget facts are typed separately from fuzzy memories.',
20
+ },
21
+ {
22
+ id: 'auto_curation_layer',
23
+ name: 'Auto Curation Layer',
24
+ purpose: 'Duplicate, stale, contradictory, and unscoped memories are consolidated before retrieval quality decays.',
25
+ },
26
+ {
27
+ id: 'context_layer',
28
+ name: 'Context Layer',
29
+ purpose: 'Only relevant scoped memories enter a given tool call, PR, deployment, or support session.',
30
+ },
31
+ {
32
+ id: 'interface_layer',
33
+ name: 'Interface Layer',
34
+ purpose: 'The memory contract is exposed through CLI, MCP, hooks, dashboards, and agent adapters without model lock-in.',
35
+ },
36
+ ]);
5
37
 
6
38
  const FIELD_ALIASES = {
7
39
  entityId: [
@@ -228,6 +260,128 @@ function buildRecommendations({ unscopedRecords, crossScopeDuplicates }) {
228
260
  return recommendations;
229
261
  }
230
262
 
263
+ function hasEmbeddingEvidence(record = {}) {
264
+ return Boolean(
265
+ record.embedding
266
+ || record.vector
267
+ || record.embeddingId
268
+ || record.metadata?.embedding
269
+ || record.metadata?.embeddingId
270
+ || record.metadata?.vectorId
271
+ || record.semanticKey
272
+ || record.metadata?.semanticKey
273
+ );
274
+ }
275
+
276
+ function hasStructuredFactEvidence(record = {}) {
277
+ const type = String(record.type || record.kind || record.memoryType || record.metadata?.type || '').toLowerCase();
278
+ return type === 'fact'
279
+ || type === 'structured_fact'
280
+ || Boolean(record.factKey || record.fact || record.metadata?.factKey || record.metadata?.fact);
281
+ }
282
+
283
+ function hasContextEvidence(record = {}) {
284
+ return Boolean(
285
+ record.contextPackId
286
+ || record.contextPack
287
+ || record.metadata?.contextPackId
288
+ || record.metadata?.contextPack
289
+ || record.retrievalQuery
290
+ || record.metadata?.retrievalQuery
291
+ );
292
+ }
293
+
294
+ function boolCapability(capabilities = {}, ...keys) {
295
+ return keys.some((key) => capabilities[key] === true);
296
+ }
297
+
298
+ function buildMemoryOsLayerReport(records = [], capabilities = {}) {
299
+ const scopeReport = buildMemoryScopeReadinessReport(records);
300
+ const semanticRecords = records.filter(hasEmbeddingEvidence);
301
+ const structuredFactRecords = records.filter(hasStructuredFactEvidence);
302
+ const contextRecords = records.filter(hasContextEvidence);
303
+ const curationReady = scopeReport.unscopedRecords === 0 && scopeReport.crossScopeDuplicates.length === 0;
304
+
305
+ const checks = [
306
+ {
307
+ id: 'file_layer',
308
+ ok: records.length > 0 || boolCapability(capabilities, 'rawStorage', 'fileLayer'),
309
+ evidence: {
310
+ records: records.length,
311
+ durableStore: Boolean(records.length > 0 || capabilities.rawStorage || capabilities.fileLayer),
312
+ },
313
+ recommendation: 'Capture raw feedback, action receipts, and tool outcomes before promoting memories.',
314
+ },
315
+ {
316
+ id: 'vector_db_layer',
317
+ ok: semanticRecords.length > 0 || boolCapability(capabilities, 'semanticSearch', 'vectorDbLayer'),
318
+ evidence: {
319
+ semanticRecords: semanticRecords.length,
320
+ semanticSearch: Boolean(capabilities.semanticSearch || capabilities.vectorDbLayer),
321
+ },
322
+ recommendation: 'Index lessons with semantic keys or embeddings so related failures are retrieved before action.',
323
+ },
324
+ {
325
+ id: 'structured_facts_layer',
326
+ ok: structuredFactRecords.length > 0 || boolCapability(capabilities, 'structuredFacts', 'structuredFactsLayer'),
327
+ evidence: {
328
+ structuredFactRecords: structuredFactRecords.length,
329
+ structuredFacts: Boolean(capabilities.structuredFacts || capabilities.structuredFactsLayer),
330
+ },
331
+ recommendation: 'Store confirmed customer, project, policy, and budget facts as typed records, not just prose.',
332
+ },
333
+ {
334
+ id: 'auto_curation_layer',
335
+ ok: curationReady && boolCapability(capabilities, 'autoCuration', 'dedupe', 'autoCurationLayer'),
336
+ evidence: {
337
+ unscopedRecords: scopeReport.unscopedRecords,
338
+ crossScopeDuplicates: scopeReport.crossScopeDuplicates.length,
339
+ autoCuration: Boolean(capabilities.autoCuration || capabilities.dedupe || capabilities.autoCurationLayer),
340
+ },
341
+ recommendation: 'Run dedupe, contradiction, stale-memory, and scope-isolation checks before memories can become gates.',
342
+ },
343
+ {
344
+ id: 'context_layer',
345
+ ok: contextRecords.length > 0 || boolCapability(capabilities, 'contextPacks', 'contextLayer', 'scopedRetrieval'),
346
+ evidence: {
347
+ contextRecords: contextRecords.length,
348
+ scopedRetrieval: Boolean(capabilities.contextPacks || capabilities.contextLayer || capabilities.scopedRetrieval),
349
+ },
350
+ recommendation: 'Inject scoped context packs per task instead of loading every memory into the model window.',
351
+ },
352
+ {
353
+ id: 'interface_layer',
354
+ ok: boolCapability(capabilities, 'mcp', 'cli', 'hooks', 'dashboard', 'interfaceLayer'),
355
+ evidence: {
356
+ cli: Boolean(capabilities.cli),
357
+ mcp: Boolean(capabilities.mcp),
358
+ hooks: Boolean(capabilities.hooks),
359
+ dashboard: Boolean(capabilities.dashboard),
360
+ },
361
+ recommendation: 'Expose the same memory contract through CLI, MCP, hooks, dashboard, and agent adapters.',
362
+ },
363
+ ].map((check) => {
364
+ const layer = MEMORY_OS_LAYERS.find((candidate) => candidate.id === check.id);
365
+ return {
366
+ ...layer,
367
+ ...check,
368
+ };
369
+ });
370
+
371
+ const missingLayers = checks.filter((check) => !check.ok).map((check) => check.id);
372
+
373
+ return {
374
+ ready: missingLayers.length === 0,
375
+ riskLevel: missingLayers.length === 0 ? 'low' : missingLayers.length <= 2 ? 'medium' : 'high',
376
+ layers: checks,
377
+ missingLayers,
378
+ scopeReport,
379
+ recommendations: checks
380
+ .filter((check) => !check.ok)
381
+ .map((check) => check.recommendation),
382
+ };
383
+ }
384
+
231
385
  function selectRecordsForScope(records = [], requestedScope = {}, options = {}) {
232
386
  const requested = normalizeScope(requestedScope);
233
387
  const requestedKey = memoryScopeKey(requested);
@@ -265,6 +419,7 @@ function buildMemoriStyleBenchmarkRecords() {
265
419
  projectId: 'thumbgate',
266
420
  processId: 'agent-a',
267
421
  sessionId: 'session-1',
422
+ metadata: { semanticKey: 'checkout-readiness', contextPackId: 'checkout-pro' },
268
423
  content: 'Use the paid sprint checklist before changing checkout code.',
269
424
  },
270
425
  {
@@ -298,14 +453,18 @@ function buildMemoriStyleBenchmarkRecords() {
298
453
  processId: 'agent-a',
299
454
  sessionId: 'session-1',
300
455
  visibility: 'shared',
456
+ type: 'fact',
457
+ factKey: 'checkout.mutation_policy',
301
458
  content: 'Shared rule: checkout mutations require audit evidence.',
302
459
  },
303
460
  ];
304
461
  }
305
462
 
306
463
  module.exports = {
464
+ MEMORY_OS_LAYERS,
307
465
  REQUIRED_SCOPE_FIELDS,
308
466
  buildMemoriStyleBenchmarkRecords,
467
+ buildMemoryOsLayerReport,
309
468
  buildMemoryScopeReadinessReport,
310
469
  isSharedMemory,
311
470
  memoryScopeKey,
@@ -282,6 +282,8 @@ function buildPromotedGate(candidate, metrics, runId) {
282
282
  occurrences: metrics.hits,
283
283
  promotedAt: new Date().toISOString(),
284
284
  source: 'meta-agent',
285
+ // origin distinguishes silent-failure-clustered candidates from feedback-derived ones
286
+ origin: candidate.origin || 'user-feedback',
285
287
  runId,
286
288
  score: parseFloat(metrics.score.toFixed(3)),
287
289
  hitRate: parseFloat(metrics.hitRate.toFixed(3)),
@@ -371,6 +373,38 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
371
373
  candidates = generateCandidatesHeuristic(failures, blockPatterns);
372
374
  }
373
375
 
376
+ // Tag existing-pipeline candidates with their origin so downstream precision
377
+ // measurement (silentFailureDerivedGates vs user-feedback-derived) is possible.
378
+ candidates = candidates.map((c) => (c.origin ? c : { ...c, origin: 'user-feedback' }));
379
+
380
+ // Step 3b: Silent-failure clustering — DEFAULT-ON as of 2026-05-21
381
+ // (flipped from opt-in by PR #2289). Opt out via
382
+ // THUMBGATE_SILENT_FAILURE_CLUSTERING=0 (or NODE_ENV=test). Candidates flow
383
+ // through the SAME scoring / fp-rate eval below; we do not bypass any
384
+ // guardrail. The point of this clustering is to cover the case where users
385
+ // are lazy and never give thumbs-down — keeping it opt-in meant the users
386
+ // who need it most never got the benefit.
387
+ let silentFailureStats = null;
388
+ const { isSilentFailureClusteringEnabled, generateSilentFailureCandidates } = require('./silent-failure-cluster');
389
+ if (isSilentFailureClusteringEnabled()) {
390
+ try {
391
+ const sfResult = generateSilentFailureCandidates({ feedbackLogPath });
392
+ silentFailureStats = sfResult.stats;
393
+ if (sfResult.candidates && sfResult.candidates.length > 0) {
394
+ candidates = candidates.concat(sfResult.candidates);
395
+ }
396
+ if (verbose) {
397
+ process.stdout.write(
398
+ `[meta-agent] silent-failure-cluster: candidates=${sfResult.candidates.length} `
399
+ + `failed=${sfResult.stats.failedCalls} clusters=${sfResult.stats.clusters} `
400
+ + `skipped=${sfResult.stats.skippedReason || 'none'}\n`
401
+ );
402
+ }
403
+ } catch (err) {
404
+ if (verbose) process.stdout.write(`[meta-agent] silent-failure-cluster failed (non-fatal): ${err.message}\n`);
405
+ }
406
+ }
407
+
374
408
  if (verbose) {
375
409
  process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
376
410
  }
@@ -507,6 +541,8 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
507
541
  skipped: evolutionResult.skipped || false,
508
542
  }
509
543
  : null,
544
+ silentFailureCluster: silentFailureStats,
545
+ silentFailureDerivedGates: promotedGates.filter((g) => g.origin === 'silent-failure-cluster').length,
510
546
  };
511
547
 
512
548
  if (!dryRun) {
@@ -580,17 +580,51 @@ function resolveGitHubRepository(env = process.env) {
580
580
  function findOpenPrForBranch({ branchName, runner = runGh, env = process.env } = {}) {
581
581
  const normalizedBranch = String(branchName || '').trim();
582
582
  if (!normalizedBranch) return null;
583
- if (!env.GH_TOKEN && !env.GITHUB_TOKEN) {
583
+ const token = env.GH_TOKEN || env.GITHUB_TOKEN;
584
+ if (!token) {
584
585
  return null;
585
586
  }
586
- const args = ['pr', 'list'];
587
587
  const repository = resolveGitHubRepository(env);
588
- if (repository) {
589
- args.push('--repo', repository);
588
+ if (!repository) return null;
589
+
590
+ // Try REST API first as it is much more robust and doesn't rely on gh CLI's GraphQL auth
591
+ try {
592
+ const owner = repository.split('/')[0];
593
+ const url = `https://api.github.com/repos/${repository}/pulls?head=${encodeURIComponent(owner + ':' + normalizedBranch)}&state=open`;
594
+ const curlConfig = `header = "Authorization: token ${token}"\nheader = "User-Agent: ThumbGate-CI"`;
595
+ const curlResult = spawnSync('curl', [
596
+ '-s',
597
+ '-K', '-',
598
+ url
599
+ ], {
600
+ encoding: 'utf8',
601
+ input: curlConfig
602
+ });
603
+
604
+ if (curlResult && curlResult.status === 0) {
605
+ const prs = JSON.parse(curlResult.stdout || '[]');
606
+ if (Array.isArray(prs) && prs.length > 0) {
607
+ return {
608
+ number: prs[0].number,
609
+ state: prs[0].state,
610
+ isDraft: prs[0].draft || false,
611
+ url: prs[0].html_url
612
+ };
613
+ }
614
+ }
615
+ } catch (err) {
616
+ console.warn(`[findOpenPrForBranch] REST fallback failed: ${err.message}`);
590
617
  }
591
- args.push('--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url');
618
+
619
+ // Fall back to gh CLI
620
+ const args = ['pr', 'list', '--repo', repository, '--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url'];
592
621
  const result = runner(args);
593
622
  if (!result || result.status !== 0) {
623
+ if (result) {
624
+ console.warn(`[findOpenPrForBranch] gh command failed with status ${result.status}. Stderr: ${result.stderr || ''}`);
625
+ } else {
626
+ console.warn(`[findOpenPrForBranch] gh command failed to spawn.`);
627
+ }
594
628
  return null;
595
629
  }
596
630
  try {
@@ -8,6 +8,7 @@ const DEFAULT_PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
8
8
  const DEFAULT_OUTPUT_DIR = path.join(__dirname, '..', 'docs', 'marketing');
9
9
  const KNOWN_REPOS = Object.freeze({
10
10
  '@anthropic-ai/sdk': 'anthropics/anthropic-sdk-typescript',
11
+ '@modelcontextprotocol/sdk': 'modelcontextprotocol/typescript-sdk',
11
12
  '@google/genai': 'googleapis/js-genai',
12
13
  '@huggingface/transformers': 'huggingface/transformers.js',
13
14
  '@lancedb/lancedb': 'lancedb/lancedb',
@@ -23,6 +24,24 @@ const KNOWN_REPOS = Object.freeze({
23
24
  undici: 'nodejs/undici',
24
25
  });
25
26
 
27
+ // Communities where ThumbGate's buyers live even though they are not npm
28
+ // dependencies. ThumbGate ships an MCP server, so the Model Context Protocol
29
+ // repos are the single highest-ROI ecosystem to contribute to — but the
30
+ // dependency-scan above would never surface them. These are always scouted on
31
+ // the default (no explicit --dependencies) path, de-duped against package.json.
32
+ const STRATEGIC_DEPENDENCIES = Object.freeze([
33
+ '@modelcontextprotocol/sdk', // MCP TypeScript SDK — the protocol ThumbGate implements
34
+ 'modelcontextprotocol/servers', // MCP servers ecosystem — where MCP authors (our buyers) collaborate
35
+ ]);
36
+
37
+ // Honest, repo-accurate framing for the outreach draft. ThumbGate does not
38
+ // `import` these — it implements the protocol — so the generic "while using X"
39
+ // line would be a false claim. Keep drafts truthful (CEO honesty rule).
40
+ const RELATIONSHIP_OVERRIDES = Object.freeze({
41
+ '@modelcontextprotocol/sdk': 'building ThumbGate as an MCP server against the Model Context Protocol spec',
42
+ 'modelcontextprotocol/servers': 'building and testing ThumbGate as an MCP server alongside the reference MCP servers',
43
+ });
44
+
26
45
  const BOUNTY_KEYWORDS = [
27
46
  'bug bounty',
28
47
  'bounty',
@@ -70,7 +89,10 @@ function dependencyNames(pkg = {}) {
70
89
  }
71
90
 
72
91
  function repoFromDependency(name) {
73
- return KNOWN_REPOS[name] || '';
92
+ if (KNOWN_REPOS[name]) return KNOWN_REPOS[name];
93
+ // A strategic identifier may itself be an "owner/repo" slug (not an npm name).
94
+ if (!name.startsWith('@') && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(name)) return name;
95
+ return '';
74
96
  }
75
97
 
76
98
  function buildIssueSearchQueries(repo) {
@@ -89,14 +111,18 @@ function scoreOpportunity(depName, repo, options = {}) {
89
111
  score += 20;
90
112
  reasons.push('known upstream repository');
91
113
  }
92
- if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici/i.test(depName)) {
114
+ if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici|mcp|modelcontext/i.test(depName)) {
93
115
  score += 20;
94
116
  reasons.push('high product adjacency for agent tooling');
95
117
  }
96
- if (/anthropic|google|huggingface|stripe|microsoft|nodejs/i.test(repo)) {
118
+ if (/anthropic|google|huggingface|stripe|microsoft|nodejs|modelcontextprotocol/i.test(repo)) {
97
119
  score += 15;
98
120
  reasons.push('large ecosystem visibility');
99
121
  }
122
+ if (/modelcontext|mcp/i.test(depName) || /modelcontextprotocol/i.test(repo)) {
123
+ score += 12;
124
+ reasons.push("ThumbGate's own protocol surface — buyers are MCP authors");
125
+ }
100
126
  if (options.includeBounties) {
101
127
  score += 10;
102
128
  reasons.push('bounty search enabled');
@@ -138,7 +164,7 @@ function buildOpportunity(depName, options = {}) {
138
164
  'no bounty, security, or maintainer-policy claim without source link',
139
165
  ],
140
166
  outreachDraft: repo
141
- ? `I found this while using ${depName} in ThumbGate. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
167
+ ? `I found this while ${RELATIONSHIP_OVERRIDES[depName] || `using ${depName} in ThumbGate`}. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
142
168
  : '',
143
169
  };
144
170
  }
@@ -149,7 +175,9 @@ function buildOssPrOpportunityScoutPlan(rawOptions = {}) {
149
175
  const explicitDeps = splitList(rawOptions.dependencies || rawOptions.deps);
150
176
  const includeBounties = rawOptions.includeBounties !== false && rawOptions['include-bounties'] !== false;
151
177
  const maxRepos = Math.max(1, Number.parseInt(String(rawOptions.maxRepos || rawOptions['max-repos'] || 12), 10) || 12);
152
- const deps = explicitDeps.length ? explicitDeps : dependencyNames(pkg);
178
+ const deps = explicitDeps.length
179
+ ? explicitDeps
180
+ : [...new Set([...dependencyNames(pkg), ...STRATEGIC_DEPENDENCIES])];
153
181
  const opportunities = deps
154
182
  .map((dep) => buildOpportunity(dep, { includeBounties }))
155
183
  .filter((opportunity) => opportunity.repo)
@@ -223,6 +251,8 @@ function writeOssPrOpportunityScoutPack(outputDir = DEFAULT_OUTPUT_DIR, options
223
251
 
224
252
  module.exports = {
225
253
  KNOWN_REPOS,
254
+ STRATEGIC_DEPENDENCIES,
255
+ RELATIONSHIP_OVERRIDES,
226
256
  buildIssueSearchQueries,
227
257
  buildOpportunity,
228
258
  buildOssPrOpportunityScoutPlan,
@@ -24,23 +24,25 @@
24
24
  */
25
25
 
26
26
  const https = require('node:https');
27
+ const {
28
+ resolvePlausibleDataDomain,
29
+ } = require('./plausible-domain-config');
27
30
 
28
- const DEFAULT_PLAUSIBLE_DOMAIN = 'thumbgate-production.up.railway.app';
29
31
  const PLAUSIBLE_ENDPOINT = 'https://plausible.io/api/event';
30
32
  const REQUEST_TIMEOUT_MS = 2_000;
31
33
 
32
34
  function isPlausibleDisabled() {
33
35
  if (process.env.THUMBGATE_PLAUSIBLE_DISABLE === '1') return true;
34
36
  if (process.env.DO_NOT_TRACK === '1') return true;
35
- // NODE_ENV-based detection was tried and dropped: `node --test` does not
36
- // set NODE_ENV automatically, so the check produced surprising behavior
37
- // in test vs. production. Tests must opt out explicitly via the dedicated
38
- // THUMBGATE_PLAUSIBLE_DISABLE flag.
37
+ // Automatically disable Plausible events in local development unless explicitly overridden in tests
38
+ if (process.env.THUMBGATE_PLAUSIBLE_DISABLE !== '0' && process.env.NODE_ENV !== 'production') {
39
+ return true;
40
+ }
39
41
  return false;
40
42
  }
41
43
 
42
44
  function getPlausibleDomain() {
43
- return process.env.THUMBGATE_PLAUSIBLE_DOMAIN || DEFAULT_PLAUSIBLE_DOMAIN;
45
+ return resolvePlausibleDataDomain();
44
46
  }
45
47
 
46
48
  /**
@@ -142,6 +144,7 @@ const CHECKOUT_EVENT_NAMES = Object.freeze({
142
144
  view: 'Checkout Pro Viewed',
143
145
  emailSubmitted: 'Checkout Pro Email Submitted',
144
146
  stripeRedirect: 'Checkout Pro Stripe Redirect Started',
147
+ purchase: 'Checkout Pro Purchase Completed',
145
148
  });
146
149
 
147
150
  function recordCheckoutFunnelEvent(stage, options = {}) {
@@ -16,7 +16,7 @@ const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
16
16
  * 2. Env var: THUMBGATE_DEV_BYPASS=[set via THUMBGATE_DEV_SECRET env var]
17
17
  * Requires a specific non-obvious value (not boolean) to prevent accidental activation.
18
18
  */
19
- function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
19
+ function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE || os.homedir() } = {}) {
20
20
  // Layer 1: env var with specific value
21
21
  if (CREATOR_BYPASS_VALUE && String(env[CREATOR_BYPASS_ENV] || '') === CREATOR_BYPASS_VALUE) {
22
22
  return true;
@@ -37,7 +37,7 @@ function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
37
37
  * with any non-empty bypass value. No env var needed — just the config file.
38
38
  * Used by the server to skip auth on localhost during local development.
39
39
  */
40
- function hasDevOverride(homeDir = os.homedir()) {
40
+ function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
41
41
  // Disabled during test runs to avoid interfering with auth assertions
42
42
  if (process.env.NODE_TEST_CONTEXT || process.env.THUMBGATE_TESTING) return false;
43
43
  try {
@@ -47,11 +47,11 @@ function hasDevOverride(homeDir = os.homedir()) {
47
47
  } catch { return false; }
48
48
  }
49
49
 
50
- function getLicenseDir(homeDir = os.homedir()) {
50
+ function getLicenseDir(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
51
51
  return path.join(homeDir, '.thumbgate');
52
52
  }
53
53
 
54
- function getLicensePath(homeDir = os.homedir()) {
54
+ function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
55
55
  return path.join(getLicenseDir(homeDir), 'license.json');
56
56
  }
57
57
 
@@ -39,9 +39,23 @@ function normalizeOptions(options = {}) {
39
39
  ...splitCsv(options.documents),
40
40
  ...splitCsv(options['document-ids']),
41
41
  ]);
42
+ const sourcePointers = unique([
43
+ ...splitCsv(options['source-pointers']),
44
+ ...splitCsv(options.pointers),
45
+ ...splitCsv(options.sources),
46
+ ]);
42
47
  const candidateImages = Number.isFinite(Number(options['candidate-images']))
43
48
  ? Number(options['candidate-images'])
44
49
  : null;
50
+ const extractedEntities = Number.isFinite(Number(options['extracted-entities']))
51
+ ? Number(options['extracted-entities'])
52
+ : 0;
53
+ const extractedRelations = Number.isFinite(Number(options['extracted-relations']))
54
+ ? Number(options['extracted-relations'])
55
+ : 0;
56
+ const promotionThreshold = Number.isFinite(Number(options['promotion-threshold']))
57
+ ? Number(options['promotion-threshold'])
58
+ : 3;
45
59
 
46
60
  return {
47
61
  ragTool: String(options['rag-tool'] || options.tool || 'proxy-pointer-rag').trim() || 'proxy-pointer-rag',
@@ -49,10 +63,15 @@ function normalizeOptions(options = {}) {
49
63
  sectionIds,
50
64
  imagePointers,
51
65
  documentIds,
66
+ sourcePointers,
52
67
  candidateImages,
68
+ extractedEntities,
69
+ extractedRelations,
70
+ promotionThreshold,
53
71
  crossDocumentPolicy: String(options['cross-doc-policy'] || options['cross-document-policy'] || '').trim().toLowerCase(),
54
72
  visionFilter: normalizeBoolean(options['vision-filter']),
55
73
  visualClaims: normalizeBoolean(options['visual-claims']),
74
+ pointerFirst: normalizeBoolean(options['pointer-first']) || normalizeBoolean(options['proxy-pointer']),
56
75
  };
57
76
  }
58
77
 
@@ -72,6 +91,14 @@ function gateApplicability(template, options) {
72
91
  return false;
73
92
  }
74
93
 
94
+ function hasExtractionSprawl(options) {
95
+ const extractedFacts = options.extractedEntities + options.extractedRelations;
96
+ if (extractedFacts === 0) return false;
97
+ if (options.pointerFirst) return true;
98
+ if (options.sourcePointers.length === 0) return true;
99
+ return extractedFacts > options.sourcePointers.length * Math.max(2, options.promotionThreshold);
100
+ }
101
+
75
102
  function buildSignalSummary(options) {
76
103
  const signals = [];
77
104
  if (options.treePath || options.sectionIds.length > 0) {
@@ -110,6 +137,19 @@ function buildSignalSummary(options) {
110
137
  risk: 'answers that describe image content may need a vision-model sanity check',
111
138
  });
112
139
  }
140
+ if (hasExtractionSprawl(options)) {
141
+ signals.push({
142
+ id: 'entity_relation_sprawl',
143
+ label: 'Entity/relation extraction sprawl',
144
+ values: unique([
145
+ `${options.extractedEntities} extracted entities`,
146
+ `${options.extractedRelations} extracted relations`,
147
+ `${options.sourcePointers.length} source pointers`,
148
+ `promotion threshold ${options.promotionThreshold}`,
149
+ ]),
150
+ risk: 'eager graph extraction can create stale aliases, weak edges, and unauditable memory; keep source pointers first and promote relations only after repeated retrieval value',
151
+ });
152
+ }
113
153
  return signals;
114
154
  }
115
155
 
@@ -139,11 +179,12 @@ function buildProxyPointerRagGuardrailsPlan(rawOptions = {}, templatesPath) {
139
179
  templates: recommendedTemplates,
140
180
  nextActions: [
141
181
  'Preserve document hierarchy, section IDs, and image file paths during ingestion.',
182
+ 'Store source pointers before extracting entities or relations; promote a relation only after repeated retrieval value and source verification.',
142
183
  'Pass section-tree and image-pointer metadata into the agent before it answers with visuals.',
143
184
  'Enable the recommended Document RAG Safety templates as pre-action gates.',
144
185
  'Use a vision filter only for high-impact answers that make claims about visual content.',
145
186
  ],
146
- exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --image-pointers=paper-1/figures/fig2.png --documents=paper-1 --visual-claims --json',
187
+ exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --source-pointers=lesson/fb_123,tool/run_456 --extracted-entities=120 --extracted-relations=80 --pointer-first --json',
147
188
  };
148
189
  }
149
190
 
@@ -37,14 +37,6 @@ function publishedCliShellCommand(pkgVersion, commandArgs = [], options = {}) {
37
37
  const escapedArgs = commandArgs.map(shellQuote).join(' ');
38
38
  const fastPath = `[ -x ${shellQuote(runtimeBin)} ] && exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`;
39
39
  const installPath = `mkdir -p ${shellQuote(prefixDir)} && exec npm ${publishedCliArgs(pkgVersion, commandArgs, { prefixDir }).map(shellQuote).join(' ')}`;
40
- if (options.preferInstalled === false) {
41
- const packageSpec = `thumbgate@${pkgVersion}`;
42
- return [
43
- `mkdir -p ${shellQuote(prefixDir)}`,
44
- `npm "install" "--prefix" ${shellQuote(prefixDir)} "--no-save" "--omit=dev" ${shellQuote(packageSpec)} >/dev/null 2>&1`,
45
- `exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`,
46
- ].join(' && ');
47
- }
48
40
  return `${fastPath} || ${installPath}`;
49
41
  }
50
42