thumbgate 1.29.2 → 1.30.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 (55) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/adapters/claude/.mcp.json +2 -2
  4. package/adapters/forge/forge.yaml +3 -3
  5. package/adapters/mcp/server-stdio.js +78 -7
  6. package/adapters/opencode/opencode.json +1 -1
  7. package/bin/cli.js +7 -5
  8. package/config/mcp-allowlists.json +26 -2
  9. package/config/post-deploy-marketing-pages.json +26 -1
  10. package/package.json +38 -7
  11. package/public/architecture.html +130 -0
  12. package/public/assets/diagrams/agent-integration.png +0 -0
  13. package/public/assets/diagrams/before-after.svg +21 -0
  14. package/public/assets/diagrams/decision.svg +36 -0
  15. package/public/assets/diagrams/feedback-pipeline.png +0 -0
  16. package/public/assets/diagrams/loop.svg +34 -0
  17. package/public/assets/diagrams/plugin-topology.png +0 -0
  18. package/public/assets/diagrams/pre-action-gate-loop.svg +59 -0
  19. package/public/assets/diagrams/stack.svg +18 -0
  20. package/public/assets/diagrams/thumbgate-architecture.png +0 -0
  21. package/public/case-studies.html +151 -0
  22. package/public/eval-scorecard.html +195 -0
  23. package/public/eval-scorecard.json +18 -0
  24. package/public/evaluations.html +168 -0
  25. package/public/index.html +4 -3
  26. package/public/numbers.html +2 -2
  27. package/public/whitepaper.html +189 -0
  28. package/scripts/activation-quickstart.js +1 -0
  29. package/scripts/agent-outcome-monitor.js +71 -1
  30. package/scripts/billing.js +3 -1
  31. package/scripts/claude-feedback-sync.js +3 -2
  32. package/scripts/cli-feedback.js +13 -7
  33. package/scripts/cross-encoder-reranker.js +3 -0
  34. package/scripts/feedback-aggregate.js +5 -2
  35. package/scripts/feedback-loop.js +244 -182
  36. package/scripts/gates-engine.js +81 -4
  37. package/scripts/generate-case-study-outreach.js +253 -0
  38. package/scripts/generate-eval-scorecard.js +276 -0
  39. package/scripts/growth-campaigns.js +183 -0
  40. package/scripts/jsonl-watcher.js +1 -0
  41. package/scripts/lesson-inference.js +23 -4
  42. package/scripts/lesson-retrieval.js +71 -4
  43. package/scripts/lesson-search.js +26 -3
  44. package/scripts/mcp-config.js +26 -5
  45. package/scripts/mcp-oauth.js +37 -2
  46. package/scripts/model-eval.js +308 -0
  47. package/scripts/parallel-workflow-orchestrator.js +86 -22
  48. package/scripts/published-cli.js +11 -1
  49. package/scripts/refresh-proof-pack.js +261 -0
  50. package/scripts/risk-scorer.js +144 -15
  51. package/scripts/statusline-local-stats.js +1 -1
  52. package/scripts/thumbgate-bench.js +13 -0
  53. package/scripts/tool-kpi-tracker.js +124 -0
  54. package/scripts/tool-registry.js +49 -1
  55. package/src/api/server.js +230 -86
@@ -93,6 +93,7 @@ async function runActivationFlow({ ask, out, isTTY, deps = {} }) {
93
93
  whatToChange: `Block this action: ${mistake}`,
94
94
  tags: 'quickstart,activation,first-rule',
95
95
  gateAction: 'block',
96
+ reviewOrigin: 'human',
96
97
  });
97
98
  } catch {
98
99
  // Capture failure should not abort the activation aha.
@@ -5,11 +5,13 @@ const fs = require('node:fs');
5
5
  const os = require('node:os');
6
6
  const path = require('node:path');
7
7
  const { calculateTaskOutcomeMetrics, readTaskOutcomes } = require('./task-outcomes');
8
+ const { computeToolKpis } = require('./tool-kpi-tracker');
8
9
 
9
10
  const DEFAULT_THRESHOLDS = path.join(__dirname, '..', 'config', 'agent-outcome-monitor-thresholds.json');
10
11
  const DEFAULT_HOSTED_ORIGIN = 'https://thumbgate-production.up.railway.app';
11
12
  const DEFAULT_MONITOR_PATH = '/v1/task-outcomes/monitor';
12
13
  const DEFAULT_SCHEDULE_ID = 'thumbgate-agent-outcome-monitor';
14
+ const DEFAULT_MINIMUM_TOOL_CALLS = 20;
13
15
 
14
16
  function monitorTaskOutcomes(outcomes = [], options = {}) {
15
17
  const metrics = calculateTaskOutcomeMetrics(outcomes);
@@ -72,6 +74,71 @@ function monitorTaskOutcomes(outcomes = [], options = {}) {
72
74
  };
73
75
  }
74
76
 
77
+ function monitorProductionSignals(outcomes = [], toolKpis = {}, options = {}) {
78
+ const taskReport = monitorTaskOutcomes(outcomes, options);
79
+ const requestedMinimumToolCalls = Number(options.minimumToolCalls);
80
+ const minimumToolCalls = Number.isFinite(requestedMinimumToolCalls) && requestedMinimumToolCalls >= 1
81
+ ? Math.floor(requestedMinimumToolCalls)
82
+ : DEFAULT_MINIMUM_TOOL_CALLS;
83
+ const measuredToolCalls = Number(toolKpis.totalCalls);
84
+ const totalToolCalls = Number.isFinite(measuredToolCalls) && measuredToolCalls >= 0
85
+ ? Math.floor(measuredToolCalls)
86
+ : 0;
87
+ const toolAlerts = [];
88
+
89
+ if (totalToolCalls < minimumToolCalls) {
90
+ toolAlerts.push({
91
+ id: 'minimum-tool-calls',
92
+ severity: 'block',
93
+ message: `Need ${minimumToolCalls} observed tool calls; observed ${totalToolCalls}.`,
94
+ });
95
+ }
96
+ for (const tool of (toolKpis.tools || [])) {
97
+ if (tool.requestCount < 3) continue;
98
+ if (tool.successRate < 90) {
99
+ toolAlerts.push({
100
+ id: `tool-success-${tool.toolName}`,
101
+ severity: 'block',
102
+ actual: tool.successRate,
103
+ expected: 'gte 90',
104
+ message: `${tool.toolName} success rate ${tool.successRate}% is below 90%.`,
105
+ });
106
+ }
107
+ if (tool.p95 > 500) {
108
+ toolAlerts.push({
109
+ id: `tool-latency-${tool.toolName}`,
110
+ severity: 'warn',
111
+ actual: tool.p95,
112
+ expected: 'lte 500',
113
+ message: `${tool.toolName} p95 latency ${tool.p95}ms is above 500ms.`,
114
+ });
115
+ }
116
+ }
117
+
118
+ const alerts = [...taskReport.alerts, ...toolAlerts];
119
+ let verdict = taskReport.verdict;
120
+ if (taskReport.verdict !== 'blocked' && toolAlerts.some((alert) => alert.id === 'minimum-tool-calls')) {
121
+ verdict = 'insufficient_evidence';
122
+ } else if (toolAlerts.some((alert) => alert.severity === 'block')) {
123
+ verdict = 'blocked';
124
+ } else if (verdict === 'healthy' && toolAlerts.length > 0) {
125
+ verdict = 'watch';
126
+ }
127
+
128
+ return {
129
+ ...taskReport,
130
+ verdict,
131
+ alerts,
132
+ observability: {
133
+ minimumToolCalls,
134
+ totalToolCalls,
135
+ evidenceStatus: totalToolCalls >= minimumToolCalls ? 'measured' : 'insufficient_evidence',
136
+ tools: toolKpis.tools || [],
137
+ servers: toolKpis.servers || [],
138
+ },
139
+ };
140
+ }
141
+
75
142
  function flattenMetricValues(metrics) {
76
143
  return {
77
144
  workingRate: metrics.task.workingRate,
@@ -228,8 +295,9 @@ async function main(argv = process.argv.slice(2)) {
228
295
 
229
296
  const report = options.hosted
230
297
  ? await fetchHostedMonitor(options)
231
- : monitorTaskOutcomes(
298
+ : monitorProductionSignals(
232
299
  readTaskOutcomes({ inputPath: options.inputPath }),
300
+ computeToolKpis(),
233
301
  { thresholdsPath: options.thresholdsPath },
234
302
  );
235
303
  if (options.outputPath) {
@@ -255,7 +323,9 @@ module.exports = {
255
323
  flattenMetricValues,
256
324
  installAgentOutcomeMonitorSchedule,
257
325
  main,
326
+ monitorProductionSignals,
258
327
  monitorTaskOutcomes,
259
328
  parseArgs,
260
329
  passesRule,
330
+ DEFAULT_MINIMUM_TOOL_CALLS,
261
331
  };
@@ -30,6 +30,7 @@ const {
30
30
  resolveFallbackArtifactPath,
31
31
  } = require('./feedback-paths');
32
32
  const { getTelemetryAnalytics, getTelemetrySourceDiagnostics } = require('./telemetry-analytics');
33
+ const { normalizeCampaignId } = require('./growth-campaigns');
33
34
  const {
34
35
  PRO_MONTHLY_PRICE_ID,
35
36
  PRO_ANNUAL_PRICE_ID,
@@ -1154,7 +1155,7 @@ function extractAttribution(metadata = {}) {
1154
1155
  return {
1155
1156
  source: normalizeText(safe.utmSource || safe.source),
1156
1157
  medium: normalizeText(safe.utmMedium || safe.medium),
1157
- campaign: normalizeText(safe.utmCampaign || safe.campaign),
1158
+ campaign: normalizeCampaignId(safe.utmCampaign || safe.campaign),
1158
1159
  content: normalizeText(safe.utmContent || safe.content),
1159
1160
  term: normalizeText(safe.utmTerm || safe.term),
1160
1161
  creator: normalizeText(safe.creator || safe.creatorHandle || safe.creator_handle),
@@ -4237,6 +4238,7 @@ module.exports = {
4237
4238
  _TRIAL_EMAIL_LEDGER_PATH: () => CONFIG.TRIAL_EMAIL_LEDGER_PATH,
4238
4239
  _ORDER_EMAIL_LEDGER_PATH: () => CONFIG.ORDER_EMAIL_LEDGER_PATH,
4239
4240
  _LOCAL_MODE: () => LOCAL_MODE(),
4241
+ _extractAttribution: extractAttribution,
4240
4242
  _withTimeout: withTimeout,
4241
4243
  _mailer: mailer,
4242
4244
  };
@@ -8,7 +8,6 @@ const {
8
8
  buildFeedbackSourceIdentity,
9
9
  getFeedbackPaths,
10
10
  readJSONL,
11
- analyzeFeedback,
12
11
  } = require('./feedback-loop');
13
12
  const { detectFeedbackSignal, normalizeFeedbackText } = require('./feedback-quality');
14
13
  const {
@@ -279,6 +278,7 @@ function syncClaudeHistoryFeedback(options = {}) {
279
278
  whatWorked: candidate.signal === 'up' ? candidate.promptText : undefined,
280
279
  tags: ['claude-history-sync', 'auto-capture-fallback'],
281
280
  sourceEvent,
281
+ reviewOrigin: 'human',
282
282
  });
283
283
 
284
284
  if (captureResult?.duplicate) {
@@ -300,7 +300,8 @@ function syncClaudeHistoryFeedback(options = {}) {
300
300
  }, { feedbackDir });
301
301
 
302
302
  if (importedCount > 0) {
303
- refreshStatuslineCache(analyzeFeedback(path.join(feedbackDir, 'feedback-log.jsonl')), path.join(feedbackDir, 'statusline_cache.json'));
303
+ const { analyzeFeedback } = require('./feedback-loop');
304
+ refreshStatuslineCache(analyzeFeedback(path.join(feedbackDir, 'feedback-log.jsonl'), { humanOnly: true }), path.join(feedbackDir, 'statusline_cache.json'));
304
305
  }
305
306
 
306
307
  return {
@@ -13,13 +13,9 @@
13
13
  * node scripts/cli-feedback.js down "what went wrong"
14
14
  */
15
15
 
16
- const { captureFeedback } = require('./feedback-loop');
16
+ const { captureFeedback, analyzeFeedback } = require('./feedback-loop');
17
17
  const { loadOptionalModule } = require('./private-core-boundary');
18
- // `history-distiller` is a PRIVATE_CORE_MODULE present in this checkout and
19
- // in ThumbGate-Core, but intentionally excluded from the public npm tarball.
20
- // The hard `require('./history-distiller')` form crashed `hook-auto-capture`
21
- // in published 1.19.0 with MODULE_NOT_FOUND. Public-shell fallback returns
22
- // null distillation; caller already handles a null distillResult.
18
+ // Keep the optional distiller from breaking the public package.
23
19
  const { distillFromHistory } = loadOptionalModule('./history-distiller', () => ({
24
20
  distillFromHistory: () => null,
25
21
  }));
@@ -60,6 +56,7 @@ function processInlineFeedback({ signal, context, chatHistory, whatWentWrong, wh
60
56
  whatWorked: whatWorked || undefined,
61
57
  chatHistory,
62
58
  sourceEvent,
59
+ reviewOrigin: 'human',
63
60
  });
64
61
  } catch (err) {
65
62
  feedbackResult = { accepted: false, reason: err.message };
@@ -75,7 +72,16 @@ function processInlineFeedback({ signal, context, chatHistory, whatWentWrong, wh
75
72
 
76
73
  // 3. Get the most recent lesson and stats
77
74
  const recentLesson = getRecentLesson();
78
- const stats = getLessonStats();
75
+ const lessonStats = getLessonStats();
76
+ const feedbackStats = analyzeFeedback(undefined, { humanOnly: true });
77
+ const stats = {
78
+ ...lessonStats,
79
+ total: feedbackStats.total,
80
+ positive: feedbackStats.totalPositive,
81
+ negative: feedbackStats.totalNegative,
82
+ rawTotal: feedbackStats.rawTotal,
83
+ excludedTotal: feedbackStats.excludedTotal,
84
+ };
79
85
 
80
86
  // 4. If the user wrote an explicit "never …" / "always …" directive, surface
81
87
  // an OFFER (never auto-act): "never" on a thumbs-down → offer force-gate now.
@@ -133,6 +133,9 @@ async function retrieveWithReranking(toolName, actionContext, options = {}) {
133
133
  const candidates = retrieveRelevantLessons(toolName, actionContext, {
134
134
  maxResults: candidateCount,
135
135
  feedbackDir,
136
+ scope: options.scope,
137
+ requireScope: options.requireScope,
138
+ includeShared: options.includeShared,
136
139
  });
137
140
 
138
141
  if (candidates.length === 0) return [];
@@ -239,11 +239,12 @@ function trendFromRateWindows(windows) {
239
239
  function computeAggregateFeedbackStats(options = {}) {
240
240
  const { entries, stores } = collectAggregateLogEntries(FEEDBACK_LOG, options);
241
241
  const memory = collectAggregateLogEntries(MEMORY_LOG, options);
242
- const { totalPositive, totalNegative, rubricSamples } = summarizeFeedbackEntries(entries);
242
+ const humanReviewedEntries = entries.filter((entry) => entry.reviewOrigin === 'human');
243
+ const { totalPositive, totalNegative, rubricSamples } = summarizeFeedbackEntries(humanReviewedEntries);
243
244
  const total = totalPositive + totalNegative;
244
245
  const approvalRate = total > 0 ? Math.round((totalPositive / total) * 1000) / 1000 : 0;
245
246
  const windows = createRateWindows(total, totalPositive, approvalRate);
246
- updateRateWindows(windows, entries);
247
+ updateRateWindows(windows, humanReviewedEntries);
247
248
  finalizeRateWindows(windows);
248
249
  const trend = trendFromRateWindows(windows);
249
250
 
@@ -255,6 +256,8 @@ function computeAggregateFeedbackStats(options = {}) {
255
256
  recentRate: windows['7d'].rate || approvalRate,
256
257
  trend,
257
258
  windows,
259
+ rawTotal: entries.length,
260
+ excludedTotal: entries.length - humanReviewedEntries.length,
258
261
  rubric: { samples: rubricSamples || memory.entries.length, blockedPromotions: 0, failingCriteria: {} },
259
262
  aggregate: {
260
263
  enabled: true,