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
package/src/api/server.js CHANGED
@@ -172,7 +172,8 @@ const {
172
172
  readTaskOutcomes,
173
173
  recordTaskOutcome,
174
174
  } = require('../../scripts/task-outcomes');
175
- const { monitorTaskOutcomes } = require('../../scripts/agent-outcome-monitor');
175
+ const { monitorProductionSignals } = require('../../scripts/agent-outcome-monitor');
176
+ const { computeToolKpis, recordToolCall } = require('../../scripts/tool-kpi-tracker');
176
177
  const {
177
178
  calculateEscalationMetrics,
178
179
  decideEscalation,
@@ -237,6 +238,7 @@ const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
237
238
  const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
238
239
  const mcpOauth = require('../../scripts/mcp-oauth');
239
240
  const { packCheckoutReference } = require('../../scripts/checkout-attribution-reference');
241
+ const { normalizeCampaignId } = require('../../scripts/growth-campaigns');
240
242
  // OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
241
243
  // (Claude Connectors Directory requires OAuth for authenticated services).
242
244
  const oauthStore = mcpOauth.createStore();
@@ -266,6 +268,11 @@ const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugi
266
268
  const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
267
269
  const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
268
270
  const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
271
+ const ARCHITECTURE_PAGE_PATH = path.resolve(__dirname, '../../public/architecture.html');
272
+ const WHITEPAPER_PAGE_PATH = path.resolve(__dirname, '../../public/whitepaper.html');
273
+ const EVAL_SCORECARD_PAGE_PATH = path.resolve(__dirname, '../../public/eval-scorecard.html');
274
+ const EVALUATIONS_PAGE_PATH = path.resolve(__dirname, '../../public/evaluations.html');
275
+ const CASE_STUDIES_PAGE_PATH = path.resolve(__dirname, '../../public/case-studies.html');
269
276
  const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
270
277
  const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
271
278
  const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
@@ -417,7 +424,7 @@ const BUILD_METADATA = resolveBuildMetadata();
417
424
  const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled']);
418
425
  const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
419
426
  const JOB_CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
420
- const TRACKED_LINK_QUERY_KEYS = [
427
+ const TRACKED_LINK_QUERY_KEYS = new Set([
421
428
  'utm_source',
422
429
  'utm_medium',
423
430
  'utm_campaign',
@@ -445,7 +452,7 @@ const TRACKED_LINK_QUERY_KEYS = [
445
452
  'seat_count',
446
453
  'landing_path',
447
454
  'referrer_host',
448
- ];
455
+ ]);
449
456
  const TRACKED_LINK_TARGETS = Object.freeze({
450
457
  gpt: {
451
458
  href: 'https://chatgpt.com/g/g-69dcfd1cd5f881918ae31874631d6f08-thumbgate',
@@ -876,11 +883,13 @@ function updateLessonRecord(feedbackDir, lessonId, updater) {
876
883
  }
877
884
 
878
885
  function getPublicMcpTools() {
879
- return MCP_TOOLS.map((tool) => ({
886
+ const { getExposedTools } = require('../../adapters/mcp/server-stdio');
887
+ return getExposedTools().map((tool) => ({
880
888
  name: tool.name,
881
889
  ...(tool.title ? { title: tool.title } : {}),
882
890
  description: tool.description,
883
891
  inputSchema: tool.inputSchema,
892
+ ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
884
893
  // Serve the tool-registry annotations (readOnlyHint/destructiveHint). Required
885
894
  // by the Claude Connectors Directory (missing annotations = the #1 rejection
886
895
  // cause) and used by MCP clients for permission prompts. Was being dropped here.
@@ -889,13 +898,7 @@ function getPublicMcpTools() {
889
898
  }
890
899
 
891
900
  function getServerCardTools() {
892
- return MCP_TOOLS.map((tool) => ({
893
- name: tool.name,
894
- ...(tool.title ? { title: tool.title } : {}),
895
- description: tool.description,
896
- inputSchema: tool.inputSchema,
897
- ...(tool.annotations ? { annotations: tool.annotations } : {}),
898
- }));
901
+ return getPublicMcpTools();
899
902
  }
900
903
 
901
904
  function buildPublicUrl(hostedConfig, pathname) {
@@ -905,7 +908,7 @@ function buildPublicUrl(hostedConfig, pathname) {
905
908
  const VERIFICATION_EVIDENCE_URL = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md';
906
909
 
907
910
  function getToolDiscoveryIndex(hostedConfig) {
908
- return MCP_TOOLS.map((tool) => ({
911
+ return getPublicMcpTools().map((tool) => ({
909
912
  name: tool.name,
910
913
  description: tool.description,
911
914
  annotations: tool.annotations || {},
@@ -915,7 +918,7 @@ function getToolDiscoveryIndex(hostedConfig) {
915
918
 
916
919
  function getContextFootprintReport(hostedConfig) {
917
920
  return buildContextFootprintReport({
918
- tools: MCP_TOOLS,
921
+ tools: getPublicMcpTools(),
919
922
  schemaUrlTemplate: buildPublicUrl(hostedConfig, '/.well-known/mcp/tools/{name}.json'),
920
923
  });
921
924
  }
@@ -1273,7 +1276,8 @@ function inferSearchQuery(referrer) {
1273
1276
 
1274
1277
  function getAttributionValue(params, key, fallbackValue) {
1275
1278
  const value = params.get(key);
1276
- return value && value.trim() ? value.trim() : fallbackValue;
1279
+ const resolved = value?.trim() || fallbackValue;
1280
+ return key === 'utm_campaign' ? normalizeCampaignId(resolved) : resolved;
1277
1281
  }
1278
1282
 
1279
1283
  function parseUrlSearchParams(urlValue) {
@@ -1645,17 +1649,28 @@ function projectWorkflowIntakeQueueLead(lead = {}, options = {}) {
1645
1649
  page: attribution.page || null,
1646
1650
  landingPath: attribution.landingPath || null,
1647
1651
  },
1648
- nextOperatorStep: options.closePacket?.status === 'approval_ready_not_authorized'
1649
- ? 'request_action_time_approval'
1650
- : options.discoveryPacket?.status === 'approval_ready_not_authorized'
1651
- ? 'request_action_time_approval_for_discovery'
1652
- : lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold',
1652
+ nextOperatorStep: resolveNextOperatorStep(lead, options),
1653
1653
  qualificationCard: options.qualificationCard || buildIntakeQualificationCard(lead, options),
1654
1654
  discoveryPacket: options.discoveryPacket || buildIntakeDiscoveryPacket(lead, options),
1655
1655
  closePacket: options.closePacket || buildIntakeClosePacket(lead, options),
1656
1656
  };
1657
1657
  }
1658
1658
 
1659
+ /**
1660
+ * Which action the operator should take next on an intake lead. Extracted from a three-level
1661
+ * nested ternary: the precedence (close packet, then discovery packet, then lead status) is the
1662
+ * actual logic and is far easier to check when it reads top to bottom.
1663
+ */
1664
+ function resolveNextOperatorStep(lead, options) {
1665
+ if (options.closePacket?.status === 'approval_ready_not_authorized') {
1666
+ return 'request_action_time_approval';
1667
+ }
1668
+ if (options.discoveryPacket?.status === 'approval_ready_not_authorized') {
1669
+ return 'request_action_time_approval_for_discovery';
1670
+ }
1671
+ return lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold';
1672
+ }
1673
+
1659
1674
  function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, options = {}) {
1660
1675
  const { limit, statuses } = resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake);
1661
1676
  const leads = workflowSprintIntake.loadWorkflowSprintLeads(feedbackDir);
@@ -1734,7 +1749,7 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
1734
1749
  }
1735
1750
 
1736
1751
  function sendWorkflowIntakeQueueProblem(res, err) {
1737
- const status = Number(err && err.statusCode) || 400;
1752
+ const status = Number(err?.statusCode) || 400;
1738
1753
  sendProblem(res, {
1739
1754
  type: status >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.INVALID_REQUEST,
1740
1755
  title: status >= 500
@@ -2689,7 +2704,10 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
2689
2704
  for (const key of TRACKED_LINK_QUERY_KEYS) {
2690
2705
  const value = params.get(key);
2691
2706
  if (value && value.trim()) {
2692
- destinationUrl.searchParams.set(key, value.trim());
2707
+ const normalizedValue = key === 'utm_campaign'
2708
+ ? normalizeCampaignId(value)
2709
+ : value.trim();
2710
+ destinationUrl.searchParams.set(key, normalizedValue);
2693
2711
  }
2694
2712
  }
2695
2713
  if (target.allowCustomerEmail) {
@@ -2730,7 +2748,7 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
2730
2748
  const source = pickFirstText(
2731
2749
  params.get('source'),
2732
2750
  params.get('utm_source'),
2733
- target.defaults && target.defaults.utm_source,
2751
+ target.defaults?.utm_source,
2734
2752
  inferSource(referrerHost)
2735
2753
  );
2736
2754
 
@@ -2744,19 +2762,21 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
2744
2762
  traceId: pickFirstText(params.get('trace_id')),
2745
2763
  source,
2746
2764
  utmSource: pickFirstText(params.get('utm_source'), source),
2747
- utmMedium: pickFirstText(params.get('utm_medium'), target.defaults && target.defaults.utm_medium, 'link_router'),
2748
- utmCampaign: pickFirstText(params.get('utm_campaign'), target.defaults && target.defaults.utm_campaign, 'first_party_redirect'),
2765
+ utmMedium: pickFirstText(params.get('utm_medium'), target.defaults?.utm_medium, 'link_router'),
2766
+ utmCampaign: normalizeCampaignId(
2767
+ pickFirstText(params.get('utm_campaign'), target.defaults?.utm_campaign, 'first_party_redirect')
2768
+ ),
2749
2769
  utmContent: pickFirstText(params.get('utm_content')),
2750
2770
  utmTerm: pickFirstText(params.get('utm_term')),
2751
2771
  creator: pickFirstText(params.get('creator'), params.get('creator_handle')),
2752
2772
  community: pickFirstText(params.get('community'), params.get('subreddit')),
2753
2773
  postId: pickFirstText(params.get('post_id')),
2754
2774
  commentId: pickFirstText(params.get('comment_id')),
2755
- campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults && target.defaults.campaign_variant),
2775
+ campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults?.campaign_variant),
2756
2776
  offerCode: pickFirstText(params.get('offer_code')),
2757
2777
  ctaId: pickFirstText(params.get('cta_id'), target.ctaId),
2758
2778
  ctaPlacement: pickFirstText(params.get('cta_placement'), target.ctaPlacement),
2759
- planId: pickFirstText(params.get('plan_id'), target.defaults && target.defaults.plan_id),
2779
+ planId: pickFirstText(params.get('plan_id'), target.defaults?.plan_id),
2760
2780
  landingPath: pickFirstText(params.get('landing_path'), `/go/${target.slug}`),
2761
2781
  page: `/go/${target.slug}`,
2762
2782
  referrer,
@@ -3822,6 +3842,12 @@ function renderSitemapXml(runtimeConfig) {
3822
3842
  { path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
3823
3843
  { path: '/agents-cost-savings', changefreq: 'weekly', priority: '0.85' },
3824
3844
  { path: '/ai-malpractice-prevention', changefreq: 'weekly', priority: '0.9' },
3845
+ { path: '/whitepaper', changefreq: 'weekly', priority: '0.9' },
3846
+ { path: '/architecture', changefreq: 'weekly', priority: '0.85' },
3847
+ { path: '/eval-scorecard', changefreq: 'weekly', priority: '0.9' },
3848
+ { path: '/evaluations', changefreq: 'weekly', priority: '0.85' },
3849
+ { path: '/case-studies', changefreq: 'weekly', priority: '0.9' },
3850
+ { path: '/numbers', changefreq: 'weekly', priority: '0.8' },
3825
3851
  { path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
3826
3852
  { path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
3827
3853
  { path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
@@ -5273,6 +5299,14 @@ function createApiServer() {
5273
5299
  ? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
5274
5300
  : rawKeyValid;
5275
5301
  if (!authed) {
5302
+ recordToolCall({
5303
+ toolName: msg.params?.name || 'unknown',
5304
+ serverName: 'mcp-http',
5305
+ latencyMs: 0,
5306
+ success: false,
5307
+ agentId: 'unauthenticated',
5308
+ metadata: { denied: true, deniedReason: 'authentication_required' },
5309
+ });
5276
5310
  res.writeHead(401, {
5277
5311
  'Content-Type': 'application/json',
5278
5312
  // RFC 9728: point unauthenticated clients at the resource metadata.
@@ -5295,6 +5329,14 @@ function createApiServer() {
5295
5329
  const tool = MCP_TOOLS.find((t) => t.name === name);
5296
5330
  const readOnly = Boolean(tool && tool.annotations && tool.annotations.readOnlyHint === true);
5297
5331
  if (!readOnly) {
5332
+ recordToolCall({
5333
+ toolName: name || 'unknown',
5334
+ serverName: 'mcp-http',
5335
+ latencyMs: 0,
5336
+ success: false,
5337
+ agentId: 'reviewer',
5338
+ metadata: { denied: true, deniedReason: 'reviewer_read_only' },
5339
+ });
5298
5340
  sendJson(res, 200, {
5299
5341
  jsonrpc: '2.0', id: msg.id,
5300
5342
  error: { code: -32002, message: `Tool "${name}" requires write access; the reviewer credential is read-only.` },
@@ -5302,15 +5344,39 @@ function createApiServer() {
5302
5344
  return;
5303
5345
  }
5304
5346
  }
5347
+ if (oauthSession) {
5348
+ const name = msg.params && msg.params.name;
5349
+ const tool = MCP_TOOLS.find((candidate) => candidate.name === name);
5350
+ const requiredScope = tool?.annotations?.readOnlyHint === true ? 'mcp:read' : 'mcp:write';
5351
+ if (!mcpOauth.scopeAllows(oauthSession, requiredScope)) {
5352
+ recordToolCall({
5353
+ toolName: name || 'unknown',
5354
+ serverName: 'mcp-http',
5355
+ latencyMs: 0,
5356
+ success: false,
5357
+ agentId: oauthSession.clientId || 'oauth-client',
5358
+ metadata: {
5359
+ denied: true,
5360
+ deniedReason: 'insufficient_scope',
5361
+ requiredScope,
5362
+ },
5363
+ });
5364
+ sendJson(res, 200, {
5365
+ jsonrpc: '2.0', id: msg.id,
5366
+ error: { code: -32003, message: `OAuth token is missing required scope "${requiredScope}".` },
5367
+ });
5368
+ return;
5369
+ }
5370
+ }
5305
5371
  (async () => {
5306
5372
  try {
5307
5373
  const { callTool } = require('../../adapters/mcp/server-stdio');
5308
- const name = msg.params && msg.params.name;
5374
+ const name = msg.params?.name;
5309
5375
  const args = (msg.params && msg.params.arguments) || {};
5310
5376
  const result = await callTool(name, args);
5311
5377
  sendJson(res, 200, {
5312
5378
  jsonrpc: '2.0', id: msg.id,
5313
- result: { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) }] },
5379
+ result,
5314
5380
  });
5315
5381
  } catch (err) {
5316
5382
  sendJson(res, 200, {
@@ -5593,7 +5659,7 @@ function createApiServer() {
5593
5659
  let trackedParsed = parsed;
5594
5660
  if (req.method === 'POST') {
5595
5661
  const target = getTrackedLinkTarget(trackedLinkMatch[1]);
5596
- if (!target || !target.requiresPost) {
5662
+ if (!target?.requiresPost) {
5597
5663
  sendJson(res, target ? 405 : 404, target
5598
5664
  ? { error: 'POST is not supported for this tracked link' }
5599
5665
  : { error: 'Tracked link not found', allowed: Object.keys(TRACKED_LINK_TARGETS) }, {
@@ -5612,7 +5678,7 @@ function createApiServer() {
5612
5678
  const form = await parseFormBody(req, 16 * 1024);
5613
5679
  trackedParsed = new URL(parsed.toString());
5614
5680
  for (const [key, value] of Object.entries(form)) {
5615
- if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.includes(key)) {
5681
+ if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.has(key)) {
5616
5682
  trackedParsed.searchParams.set(key, String(value || '').trim());
5617
5683
  }
5618
5684
  }
@@ -5684,10 +5750,31 @@ function createApiServer() {
5684
5750
  return;
5685
5751
  }
5686
5752
 
5687
- // Quick feedback capture via GET for statusline clickable links
5688
- if (isGetLikeRequest && pathname === '/feedback/quick') {
5753
+ // GET/HEAD only render a confirmation. Capture requires an authenticated
5754
+ // POST so prefetchers and link checkers cannot create human feedback.
5755
+ if ((isGetLikeRequest || req.method === 'POST') && pathname === '/feedback/quick') {
5689
5756
  const signal = parsed.searchParams.get('signal');
5690
5757
  if (signal === 'up' || signal === 'down') {
5758
+ const emoji = signal === 'up' ? '👍' : '👎';
5759
+ const label = signal === 'up' ? 'Positive' : 'Negative';
5760
+ if (isHeadRequest) {
5761
+ sendHtml(res, 200, '', {}, { headOnly: true });
5762
+ return;
5763
+ }
5764
+ if (req.method === 'GET') {
5765
+ sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate feedback</title></head>
5766
+ <body style="background:#0a0a0a;color:#fff;font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">
5767
+ <form method="post" action="/feedback/quick?signal=${signal}" style="text-align:center;background:#141414;padding:40px;border-radius:16px">
5768
+ <div style="font-size:64px">${emoji}</div><h1>Record ${label.toLowerCase()} feedback?</h1>
5769
+ <p>This confirmation prevents previews and crawlers from recording feedback.</p>
5770
+ <button type="submit" style="padding:10px 20px;font-weight:700">Record ${emoji} feedback</button>
5771
+ </form></body></html>`);
5772
+ return;
5773
+ }
5774
+ if (!isAuthorized(req, expectedApiKey)) {
5775
+ sendJson(res, 401, { error: 'Authentication required to record human feedback' });
5776
+ return;
5777
+ }
5691
5778
  const chatHistory = readRecentConversationWindow({
5692
5779
  feedbackDir: requestSafeDataDir,
5693
5780
  limit: 10,
@@ -5697,35 +5784,17 @@ function createApiServer() {
5697
5784
  context: 'Quick capture from Claude Code statusline',
5698
5785
  chatHistory,
5699
5786
  tags: ['statusline', 'quick-capture'],
5787
+ reviewOrigin: 'human',
5700
5788
  });
5701
- const emoji = signal === 'up' ? '👍' : '👎';
5702
- const color = signal === 'up' ? '#22c55e' : '#ef4444';
5703
- const label = signal === 'up' ? 'Positive' : 'Negative';
5704
5789
  const opposite = signal === 'up' ? 'down' : 'up';
5705
5790
  const oppEmoji = signal === 'up' ? '👎' : '👍';
5706
5791
  const feedbackId = result.feedbackEvent?.id || 'saved';
5707
5792
  const promoted = result.accepted ? 'Promoted to memory' : 'Stored';
5708
5793
  sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate — ${label} feedback</title>
5709
5794
  <style>
5710
- *{box-sizing:border-box}
5711
5795
  body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
5712
5796
  .card{text-align:center;background:#141414;border:1px solid #222;border-radius:16px;padding:48px 40px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,.5)}
5713
- .emoji{font-size:80px;margin-bottom:12px;animation:pop .4s ease-out}
5714
- @keyframes pop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
5715
- .msg{font-size:22px;color:${color};font-weight:700;margin-bottom:4px}
5716
- .sub{font-size:13px;color:#666;margin-top:6px;font-family:ui-monospace,monospace}
5717
- .context-form{margin-top:20px;text-align:left}
5718
- .context-form label{font-size:12px;color:#888;display:block;margin-bottom:6px}
5719
- .context-form textarea{width:100%;background:#1a1a1a;border:1px solid #333;border-radius:8px;color:#ccc;padding:10px;font-size:13px;resize:vertical;min-height:60px;font-family:system-ui}
5720
- .context-form textarea:focus{outline:none;border-color:${color}}
5721
- .context-form button{margin-top:8px;background:${color};color:#000;border:none;border-radius:8px;padding:8px 20px;font-size:13px;font-weight:600;cursor:pointer}
5722
- .context-form button:hover{opacity:.85}
5723
- .actions{margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
5724
- .actions a{color:#22d3ee;text-decoration:none;font-size:13px;padding:8px 16px;border:1px solid #333;border-radius:8px;transition:all .15s}
5725
- .actions a:hover{background:#1a2a2e;border-color:#22d3ee}
5726
- .toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#22c55e;color:#000;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:600;display:none;animation:slideUp .3s ease-out}
5727
- @keyframes slideUp{from{opacity:0;transform:translateX(-50%) translateY(12px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
5728
- .badge{display:inline-block;font-size:11px;padding:2px 8px;border-radius:4px;background:#1a1a1a;border:1px solid #333;color:#888;margin-top:8px}
5797
+ .emoji{font-size:64px}.sub,label{color:#888}textarea{width:100%;min-height:60px}.actions{margin-top:20px}.actions a{color:#22d3ee;margin:8px}
5729
5798
  </style></head><body>
5730
5799
  <div class="card">
5731
5800
  <div class="emoji">${emoji}</div>
@@ -5738,7 +5807,7 @@ body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-seri
5738
5807
  </div>
5739
5808
  <div class="actions">
5740
5809
  <a href="/lessons/${feedbackId}" title="View the full lesson and edit it">📋 View Lesson</a>
5741
- <a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">Undo → send ${oppEmoji} instead</a>
5810
+ <a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">Record ${oppEmoji} instead</a>
5742
5811
  <a href="/dashboard">Dashboard →</a>
5743
5812
  </div>
5744
5813
  </div>
@@ -5762,6 +5831,10 @@ async function addContext(){
5762
5831
  }
5763
5832
 
5764
5833
  if (req.method === 'POST' && pathname === '/feedback/quick/context') {
5834
+ if (!isAuthorized(req, expectedApiKey)) {
5835
+ sendJson(res, 401, { error: 'Authentication required to update human feedback' });
5836
+ return;
5837
+ }
5765
5838
  const body = await parseJsonBody(req);
5766
5839
  const signal = body.signal;
5767
5840
  const context = typeof body.context === 'string' ? body.context.trim() : '';
@@ -6206,6 +6279,74 @@ async function addContext(){
6206
6279
  return;
6207
6280
  }
6208
6281
 
6282
+ if (isGetLikeRequest && (pathname === '/architecture' || pathname === '/architecture.html')) {
6283
+ try {
6284
+ servePublicMarketingPage({
6285
+ req,
6286
+ res,
6287
+ parsed,
6288
+ hostedConfig,
6289
+ isHeadRequest,
6290
+ renderHtml: () => fs.readFileSync(ARCHITECTURE_PAGE_PATH, 'utf-8'),
6291
+ extraTelemetry: { pageType: 'architecture' },
6292
+ });
6293
+ } catch {
6294
+ sendJson(res, 404, { error: 'Architecture page not found' });
6295
+ }
6296
+ return;
6297
+ }
6298
+
6299
+ if (isGetLikeRequest && (pathname === '/whitepaper' || pathname === '/whitepaper.html' || pathname === '/how-we-know' || pathname === '/how-we-know-it-works')) {
6300
+ try {
6301
+ servePublicMarketingPage({
6302
+ req,
6303
+ res,
6304
+ parsed,
6305
+ hostedConfig,
6306
+ isHeadRequest,
6307
+ renderHtml: () => fs.readFileSync(WHITEPAPER_PAGE_PATH, 'utf-8'),
6308
+ extraTelemetry: { pageType: 'whitepaper' },
6309
+ });
6310
+ } catch {
6311
+ sendJson(res, 404, { error: 'White paper page not found' });
6312
+ }
6313
+ return;
6314
+ }
6315
+
6316
+ if (isGetLikeRequest && (pathname === '/eval-scorecard' || pathname === '/eval-scorecard.html' || pathname === '/scorecard' || pathname === '/bench')) {
6317
+ try {
6318
+ servePublicMarketingPage({
6319
+ req,
6320
+ res,
6321
+ parsed,
6322
+ hostedConfig,
6323
+ isHeadRequest,
6324
+ renderHtml: () => fs.readFileSync(EVAL_SCORECARD_PAGE_PATH, 'utf-8'),
6325
+ extraTelemetry: { pageType: 'eval_scorecard' },
6326
+ });
6327
+ } catch {
6328
+ sendJson(res, 404, { error: 'Eval scorecard page not found' });
6329
+ }
6330
+ return;
6331
+ }
6332
+
6333
+ if (isGetLikeRequest && (pathname === '/evaluations' || pathname === '/evaluations.html')) {
6334
+ try {
6335
+ servePublicMarketingPage({
6336
+ req,
6337
+ res,
6338
+ parsed,
6339
+ hostedConfig,
6340
+ isHeadRequest,
6341
+ renderHtml: () => fs.readFileSync(EVALUATIONS_PAGE_PATH, 'utf-8'),
6342
+ extraTelemetry: { pageType: 'evaluations' },
6343
+ });
6344
+ } catch {
6345
+ sendJson(res, 404, { error: 'Evaluations page not found' });
6346
+ }
6347
+ return;
6348
+ }
6349
+
6209
6350
  if (isGetLikeRequest && (pathname === '/federal' || pathname === '/federal.html' || pathname === '/government' || pathname === '/gov')) {
6210
6351
  // Federal lead-gen page. Routed through servePublicMarketingPage so agency
6211
6352
  // arrivals via SBIR / GSA / outbound channels capture UTM attribution and
@@ -6470,7 +6611,7 @@ async function addContext(){
6470
6611
  if (
6471
6612
  key === 'confirm'
6472
6613
  || key === 'customer_email'
6473
- || TRACKED_LINK_QUERY_KEYS.includes(key)
6614
+ || TRACKED_LINK_QUERY_KEYS.has(key)
6474
6615
  ) {
6475
6616
  parsed.searchParams.set(key, String(value || '').trim());
6476
6617
  }
@@ -6986,7 +7127,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
6986
7127
  // are configured (production) the key MUST match a configured admin /
6987
7128
  // operator / reviewer key — otherwise OAuth would authenticate nobody.
6988
7129
  // In insecure/dev mode (no keys configured) any non-empty key is accepted.
6989
- if (!resolveKeyRole(form.get('api_key') || '')) {
7130
+ const keyRole = resolveKeyRole(form.get('api_key') || '');
7131
+ if (!keyRole) {
6990
7132
  sendJson(res, 400, { error: 'access_denied', error_description: 'invalid ThumbGate API key' });
6991
7133
  return;
6992
7134
  }
@@ -6996,6 +7138,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
6996
7138
  codeChallenge: authorizationParams.codeChallenge,
6997
7139
  codeChallengeMethod: authorizationParams.codeChallengeMethod,
6998
7140
  scope: authorizationParams.scope,
7141
+ allowedScopes: keyRole === 'reviewer' ? ['mcp:read'] : mcpOauth.SUPPORTED_SCOPES,
6999
7142
  resource: authorizationParams.resource || buildPublicUrl(hostedConfig, '/mcp'),
7000
7143
  boundKey: form.get('api_key') || '',
7001
7144
  state,
@@ -7039,7 +7182,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7039
7182
  sendJson(res, 200, {
7040
7183
  name: 'thumbgate',
7041
7184
  version: pkg.version,
7042
- count: MCP_TOOLS.length,
7185
+ count: getPublicMcpTools().length,
7043
7186
  tools: getToolDiscoveryIndex(hostedConfig),
7044
7187
  }, {}, {
7045
7188
  headOnly: isHeadRequest,
@@ -7072,7 +7215,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7072
7215
  });
7073
7216
  return;
7074
7217
  }
7075
- const tool = MCP_TOOLS.find((candidate) => candidate.name === toolName);
7218
+ const tool = getPublicMcpTools().find((candidate) => candidate.name === toolName);
7076
7219
  if (!tool) {
7077
7220
  sendJson(res, 404, {
7078
7221
  error: 'tool_not_found',
@@ -7088,6 +7231,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
7088
7231
  description: tool.description,
7089
7232
  annotations: tool.annotations || {},
7090
7233
  inputSchema: tool.inputSchema,
7234
+ ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
7091
7235
  }, {}, {
7092
7236
  headOnly: isHeadRequest,
7093
7237
  });
@@ -8078,29 +8222,25 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8078
8222
  return;
8079
8223
  }
8080
8224
 
8081
- // Public case studies — proof surface for buyers. Conversion-optimization
8082
- // surface that was missing: thumbgate.ai had no /case-studies, so visitors
8083
- // landed on CLI install commands without seeing whether anyone actually
8084
- // got value. First entry is the Aiventyx Teams listing integration: real
8085
- // third-party CTR signal (5/8 clicks before the /go/teams fix, now routed
8086
- // through team intake so scope happens before checkout).
8087
- if (isGetLikeRequest && pathname === '/case-studies') {
8088
- sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Case Studies — ThumbGate</title><meta name="description" content="Real integrations of ThumbGate's pre-action checks for AI coding agents. Proof, not promises."><style>body{font-family:system-ui,-apple-system,sans-serif;max-width:780px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}h1{font-size:32px;margin:0 0 8px}.lede{color:#6b7280;font-size:18px;margin:0 0 32px}article{border:1px solid #e5e7eb;border-radius:12px;padding:24px;margin-bottom:24px;background:#fff}article h2{margin:0 0 4px;font-size:22px}.meta{color:#6b7280;font-size:13px;margin-bottom:16px}h3{font-size:15px;margin:20px 0 8px;color:#374151;text-transform:uppercase;letter-spacing:0.5px}.metric{display:inline-block;background:#0f172a;color:#fff;padding:4px 10px;border-radius:6px;font-weight:600;font-size:14px;margin:0 4px 4px 0}p{margin:8px 0}a{color:#0066cc}code{background:#f3f4f6;padding:1px 6px;border-radius:4px;font-size:13.5px}footer{margin-top:48px;padding-top:24px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
8089
- <h1>Case Studies</h1>
8090
- <p class="lede">Real integrations. No fabricated logos, no aspirational numbers — every claim below is reproducible.</p>
8091
-
8092
- <p><em>New case studies for individual Pro operators coming soon.</em></p>
8093
-
8094
- <footer>
8095
- <p>Want to be the next case study? The product is real, the integration is 30 seconds: <code>npx thumbgate init</code>. If you ship something with ThumbGate and want it documented here, email <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>.</p>
8096
- <p><a href="https://thumbgate.ai">Home</a> · <a href="/pricing">Pricing</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a> · <a href="/support">Support</a></p>
8097
- </footer>
8098
- </body></html>`, {}, {
8099
- headOnly: isHeadRequest,
8100
- });
8225
+ // Public case studies — dogfood proof narratives (static HTML; no fabricated logos).
8226
+ if (isGetLikeRequest && (pathname === '/case-studies' || pathname === '/case-studies.html')) {
8227
+ try {
8228
+ servePublicMarketingPage({
8229
+ req,
8230
+ res,
8231
+ parsed,
8232
+ hostedConfig,
8233
+ isHeadRequest,
8234
+ renderHtml: () => fs.readFileSync(CASE_STUDIES_PAGE_PATH, 'utf-8'),
8235
+ extraTelemetry: { pageType: 'case_studies' },
8236
+ });
8237
+ } catch {
8238
+ sendJson(res, 404, { error: 'Case studies page not found' });
8239
+ }
8101
8240
  return;
8102
8241
  }
8103
8242
 
8243
+
8104
8244
  // Public canonical pricing page. The audit flagged "pricing schizophrenia":
8105
8245
  // sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
8106
8246
  // and there was no buyer-facing surface to reconcile the two. This is now
@@ -8255,7 +8395,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8255
8395
  });
8256
8396
  return;
8257
8397
  }
8258
- if (result && result.retryable === true) {
8398
+ if (result?.retryable === true) {
8259
8399
  sendProblem(res, {
8260
8400
  type: PROBLEM_TYPES.INTERNAL,
8261
8401
  title: 'Fulfillment temporarily unavailable',
@@ -8443,14 +8583,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8443
8583
  }
8444
8584
 
8445
8585
  const resolvedTraceId = result.traceId || requestedTraceId;
8446
- const nextSteps = result.apiKey
8447
- ? {
8586
+ let nextSteps = {};
8587
+ if (result.apiKey) {
8588
+ nextSteps = {
8448
8589
  env: `THUMBGATE_API_KEY=${result.apiKey}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
8449
8590
  curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n+ -H 'Authorization: Bearer ${result.apiKey}' \\\n+ -H 'Content-Type: application/json' \\\n+ -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
8450
- }
8451
- : result.offerKind === 'workflow_hardening_diagnostic'
8452
- ? { fulfillment: 'Payment confirmed. Diagnostic fulfillment is processing; an email will arrive when it completes.' }
8453
- : {};
8591
+ };
8592
+ } else if (result.offerKind === 'workflow_hardening_diagnostic') {
8593
+ nextSteps = { fulfillment: 'Payment confirmed. Diagnostic fulfillment is processing; an email will arrive when it completes.' };
8594
+ }
8454
8595
 
8455
8596
  sendJson(res, 200, {
8456
8597
  ...result,
@@ -8514,7 +8655,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8514
8655
  if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
8515
8656
  const stats = shouldAggregateFeedback()
8516
8657
  ? computeAggregateFeedbackStats({ feedbackDir: requestFeedbackPaths.FEEDBACK_DIR })
8517
- : analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH);
8658
+ : analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH, { humanOnly: true });
8518
8659
  try {
8519
8660
  const { getStatuslineMeta } = require('../../scripts/statusline-meta');
8520
8661
  const meta = getStatuslineMeta({ env: process.env });
@@ -9034,6 +9175,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9034
9175
  workflowContract: body.workflowContract,
9035
9176
  repoPath: body.repoPath,
9036
9177
  localOnly: body.localOnly === true,
9178
+ ttlMs: body.ttlMs,
9037
9179
  clear: body.clear === true,
9038
9180
  });
9039
9181
  sendJson(res, 200, { scope });
@@ -9116,7 +9258,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9116
9258
 
9117
9259
  if (req.method === 'GET' && pathname === '/v1/task-outcomes/monitor') {
9118
9260
  const outcomes = readTaskOutcomes({ feedbackDir: requestFeedbackDir });
9119
- sendJson(res, 200, monitorTaskOutcomes(outcomes));
9261
+ const toolKpis = computeToolKpis({ feedbackDir: requestFeedbackDir });
9262
+ sendJson(res, 200, monitorProductionSignals(outcomes, toolKpis));
9120
9263
  return;
9121
9264
  }
9122
9265
 
@@ -9329,6 +9472,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9329
9472
  guardrails: body.guardrails,
9330
9473
  tags: extractTags(body.tags),
9331
9474
  skill: body.skill,
9475
+ reviewOrigin: 'human',
9332
9476
  });
9333
9477
  const actionIntegration = inferActionIntegration(body, req.headers);
9334
9478
  appendBestEffortTelemetry(requestFeedbackDir, {