thumbgate 0.9.9 → 0.9.11

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 (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +2 -2
  7. package/adapters/amp/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +2 -2
  9. package/adapters/claude/.mcp.json +3 -3
  10. package/adapters/codex/config.toml +4 -4
  11. package/adapters/gemini/function-declarations.json +1 -1
  12. package/adapters/mcp/server-stdio.js +66 -6
  13. package/adapters/opencode/opencode.json +4 -2
  14. package/bin/cli.js +188 -39
  15. package/config/e2e-critical-flows.json +4 -0
  16. package/config/gates/default.json +74 -2
  17. package/config/github-about.json +1 -1
  18. package/config/mcp-allowlists.json +33 -6
  19. package/config/skill-packs/react-testing.json +1 -1
  20. package/config/tessl-tiles.json +3 -3
  21. package/openapi/openapi.yaml +2 -2
  22. package/package.json +23 -9
  23. package/plugins/amp-skill/INSTALL.md +3 -2
  24. package/plugins/amp-skill/SKILL.md +1 -0
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +5 -3
  27. package/plugins/claude-codex-bridge/README.md +1 -1
  28. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +1 -1
  29. package/plugins/claude-skill/INSTALL.md +4 -3
  30. package/plugins/claude-skill/SKILL.md +1 -1
  31. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  32. package/plugins/codex-profile/.mcp.json +5 -3
  33. package/plugins/codex-profile/INSTALL.md +2 -2
  34. package/plugins/codex-profile/README.md +1 -1
  35. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  36. package/plugins/cursor-marketplace/README.md +5 -5
  37. package/plugins/cursor-marketplace/mcp.json +4 -2
  38. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +1 -1
  39. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  40. package/plugins/gemini-extension/INSTALL.md +4 -4
  41. package/plugins/opencode-profile/INSTALL.md +5 -5
  42. package/public/dashboard.html +15 -8
  43. package/public/index.html +134 -375
  44. package/public/js/buyer-intent.js +252 -0
  45. package/public/pro.html +1085 -0
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/adk-consolidator.js +17 -5
  48. package/scripts/agent-readiness.js +3 -1
  49. package/scripts/agent-security-hardening.js +4 -4
  50. package/scripts/auto-promote-gates.js +8 -0
  51. package/scripts/auto-wire-hooks.js +105 -21
  52. package/scripts/billing.js +111 -7
  53. package/scripts/build-metadata.js +14 -0
  54. package/scripts/check-congruence.js +1 -1
  55. package/scripts/context-engine.js +2 -1
  56. package/scripts/daemon-manager.js +2 -2
  57. package/scripts/dashboard.js +2 -2
  58. package/scripts/data-governance.js +1 -1
  59. package/scripts/deploy-gcp.sh +1 -1
  60. package/scripts/deploy-policy.js +22 -4
  61. package/scripts/dispatch-brief.js +1 -1
  62. package/scripts/ensure-repo-bootstrap.js +1 -1
  63. package/scripts/feedback-attribution.js +22 -10
  64. package/scripts/feedback-fallback.js +3 -2
  65. package/scripts/feedback-inbox-read.js +1 -1
  66. package/scripts/feedback-loop.js +41 -3
  67. package/scripts/feedback-paths.js +8 -8
  68. package/scripts/feedback-schema.js +1 -1
  69. package/scripts/feedback-to-memory.js +2 -2
  70. package/scripts/filesystem-search.js +2 -2
  71. package/scripts/gates-engine.js +765 -34
  72. package/scripts/generate-paperbanana-diagrams.sh +3 -3
  73. package/scripts/github-about.js +1 -1
  74. package/scripts/gtm-revenue-loop.js +20 -1
  75. package/scripts/hook-runtime.js +89 -0
  76. package/scripts/hook-stop-self-score.sh +3 -3
  77. package/scripts/hook-thumbgate-cache-updater.js +98 -37
  78. package/scripts/hosted-config.js +12 -10
  79. package/scripts/hybrid-feedback-context.js +54 -13
  80. package/scripts/install-mcp.js +14 -1
  81. package/scripts/intent-router.js +1 -1
  82. package/scripts/internal-agent-bootstrap.js +1 -1
  83. package/scripts/lesson-inference.js +6 -1
  84. package/scripts/license.js +54 -16
  85. package/scripts/mcp-config.js +69 -7
  86. package/scripts/memory-migration.js +1 -1
  87. package/scripts/money-watcher.js +166 -16
  88. package/scripts/operational-integrity.js +480 -0
  89. package/scripts/optimize-context.js +1 -1
  90. package/scripts/perplexity-marketing.js +1 -1
  91. package/scripts/post-everywhere.js +7 -12
  92. package/scripts/post-to-x.js +1 -1
  93. package/scripts/pr-manager.js +14 -11
  94. package/scripts/problem-detail.js +10 -10
  95. package/scripts/profile-router.js +2 -0
  96. package/scripts/prompt-dlp.js +1 -0
  97. package/scripts/prove-adapters.js +6 -6
  98. package/scripts/prove-automation.js +1 -1
  99. package/scripts/prove-autoresearch.js +1 -1
  100. package/scripts/prove-claim-verification.js +3 -3
  101. package/scripts/prove-data-pipeline.js +5 -5
  102. package/scripts/prove-data-quality.js +1 -1
  103. package/scripts/prove-evolution.js +7 -7
  104. package/scripts/prove-harnesses.js +2 -2
  105. package/scripts/prove-lancedb.js +2 -2
  106. package/scripts/prove-local-intelligence.js +1 -1
  107. package/scripts/prove-loop-closure.js +1 -1
  108. package/scripts/prove-predictive-insights.js +2 -2
  109. package/scripts/prove-runtime.js +6 -6
  110. package/scripts/prove-seo-gsd.js +1 -1
  111. package/scripts/prove-settings.js +4 -4
  112. package/scripts/prove-subway-upgrades.js +1 -1
  113. package/scripts/prove-tessl.js +2 -2
  114. package/scripts/prove-xmemory.js +2 -2
  115. package/scripts/publish-decision.js +10 -0
  116. package/scripts/published-cli.js +34 -0
  117. package/scripts/rate-limiter.js +2 -2
  118. package/scripts/reddit-monitor-cron.sh +2 -2
  119. package/scripts/reminder-engine.js +1 -1
  120. package/scripts/schedule-manager.js +3 -3
  121. package/scripts/self-healing-check.js +1 -1
  122. package/scripts/shieldcortex-memory-firewall-runner.mjs +1 -1
  123. package/scripts/skill-quality-tracker.js +1 -1
  124. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  125. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  126. package/scripts/social-analytics/engagement-audit.js +202 -0
  127. package/scripts/social-analytics/generate-instagram-card.js +1 -1
  128. package/scripts/social-analytics/instagram-thumbgate-post.js +5 -1
  129. package/scripts/social-analytics/install-growth-automation.js +114 -0
  130. package/scripts/social-analytics/publish-instagram-thumbgate.js +8 -2
  131. package/scripts/social-analytics/publish-thumbgate-launch.js +1 -1
  132. package/scripts/social-analytics/publishers/reddit.js +7 -12
  133. package/scripts/social-analytics/publishers/zernio.js +19 -0
  134. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  135. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  136. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  137. package/scripts/social-pipeline.js +2 -2
  138. package/scripts/social-post-hourly.js +185 -0
  139. package/scripts/social-quality-gate.js +119 -3
  140. package/scripts/social-reply-monitor.js +150 -34
  141. package/scripts/statusline-cache-path.js +27 -0
  142. package/scripts/statusline-meta.js +22 -0
  143. package/scripts/statusline.sh +24 -32
  144. package/scripts/sync-version.js +24 -12
  145. package/scripts/telemetry-analytics.js +4 -4
  146. package/scripts/tessl-export.js +1 -1
  147. package/scripts/test-coverage.js +20 -13
  148. package/scripts/thumbgate-search.js +2 -2
  149. package/scripts/tool-registry.js +98 -1
  150. package/scripts/train_from_feedback.py +1 -1
  151. package/scripts/user-profile.js +4 -4
  152. package/scripts/validate-feedback.js +1 -1
  153. package/scripts/vector-store.js +1 -1
  154. package/scripts/verification-loop.js +1 -1
  155. package/scripts/verify-run.js +1 -1
  156. package/scripts/weekly-auto-post.js +1 -1
  157. package/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  158. package/src/api/server.js +291 -41
  159. package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
  160. package/scripts/social-analytics/db/social-analytics.db +0 -0
package/src/api/server.js CHANGED
@@ -90,7 +90,15 @@ const {
90
90
  loadStats: loadGateStats,
91
91
  setConstraint,
92
92
  loadConstraints,
93
+ setTaskScope,
94
+ setBranchGovernance,
95
+ getScopeState,
96
+ getBranchGovernanceState,
97
+ approveProtectedAction,
93
98
  } = require('../../scripts/gates-engine');
99
+ const {
100
+ evaluateOperationalIntegrity,
101
+ } = require('../../scripts/operational-integrity');
94
102
  const {
95
103
  generateDashboard,
96
104
  } = require('../../scripts/dashboard');
@@ -109,7 +117,7 @@ const {
109
117
  readJSONLLocal,
110
118
  } = require('../../scripts/lesson-synthesis');
111
119
  const {
112
- searchRlhf,
120
+ searchThumbgate,
113
121
  } = require('../../scripts/thumbgate-search');
114
122
  const {
115
123
  appendTelemetryPing,
@@ -141,11 +149,13 @@ const {
141
149
  } = require('../../scripts/seo-gsd');
142
150
 
143
151
  const LANDING_PAGE_PATH = path.resolve(__dirname, '../../public/index.html');
152
+ const PRO_PAGE_PATH = path.resolve(__dirname, '../../public/pro.html');
144
153
  const DASHBOARD_PAGE_PATH = path.resolve(__dirname, '../../public/dashboard.html');
145
154
  const LESSONS_PAGE_PATH = path.resolve(__dirname, '../../public/lessons.html');
146
155
  const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
147
156
  const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
148
157
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
158
+ const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
149
159
  const VISITOR_COOKIE_NAME = 'thumbgate_visitor_id';
150
160
  const SESSION_COOKIE_NAME = 'thumbgate_session_id';
151
161
  const ACQUISITION_COOKIE_NAME = 'thumbgate_acquisition_id';
@@ -215,6 +225,29 @@ function findRecordById(id, feedbackDir) {
215
225
  return { feedbackEvent, memoryRecord };
216
226
  }
217
227
 
228
+ function mergeFollowUpDetail(existingDetail, followUpText) {
229
+ const existing = normalizeNullableText(existingDetail);
230
+ const next = normalizeNullableText(followUpText);
231
+ if (!next) return existing;
232
+ if (!existing) return next;
233
+ if (existing.includes(next)) return existing;
234
+ return `${existing}\n\nFollow-up: ${next}`;
235
+ }
236
+
237
+ function updateLessonRecord(feedbackDir, lessonId, updater) {
238
+ const record = findRecordById(lessonId, feedbackDir);
239
+ if (!record) return null;
240
+ const existing = { ...(record.feedbackEvent || {}), ...(record.memoryRecord || {}) };
241
+ const updated = updater({ ...existing });
242
+ if (!updated) return null;
243
+ const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
244
+ const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
245
+ const updatedMemory = updateRecordInJsonl(memoryLogPath, lessonId, updated);
246
+ const updatedFeedback = updateRecordInJsonl(feedbackLogPath, lessonId, updated);
247
+ if (!updatedMemory && !updatedFeedback) return null;
248
+ return updated;
249
+ }
250
+
218
251
  function getPublicMcpTools() {
219
252
  return MCP_TOOLS.map((tool) => ({
220
253
  name: tool.name,
@@ -862,8 +895,8 @@ function escapeHtmlAttribute(value) {
862
895
  .replaceAll('>', '>');
863
896
  }
864
897
 
865
- function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
866
- const template = fs.readFileSync(LANDING_PAGE_PATH, 'utf-8');
898
+ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContext = {}) {
899
+ const template = fs.readFileSync(templatePath, 'utf-8');
867
900
  const googleSiteVerificationMeta = runtimeConfig.googleSiteVerification
868
901
  ? ` <meta name="google-site-verification" content="${escapeHtmlAttribute(runtimeConfig.googleSiteVerification)}" />`
869
902
  : '';
@@ -900,6 +933,14 @@ function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
900
933
  });
901
934
  }
902
935
 
936
+ function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
937
+ return loadPublicMarketingTemplateHtml(LANDING_PAGE_PATH, runtimeConfig, pageContext);
938
+ }
939
+
940
+ function loadProPageHtml(runtimeConfig, pageContext = {}) {
941
+ return loadPublicMarketingTemplateHtml(PRO_PAGE_PATH, runtimeConfig, pageContext);
942
+ }
943
+
903
944
  function loadDashboardPageHtml(req, expectedApiKey) {
904
945
  const template = fs.readFileSync(DASHBOARD_PAGE_PATH, 'utf-8');
905
946
  const forwardedHost = req.headers['x-forwarded-host'];
@@ -937,6 +978,13 @@ function loadLessonsPageHtml(req, expectedApiKey) {
937
978
 
938
979
  function esc(s) { return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
939
980
 
981
+ function normalizeLessonSignal(signal) {
982
+ const value = String(signal || '').toLowerCase();
983
+ if (value === 'up' || value === 'positive' || value === 'thumbs_up') return 'up';
984
+ if (value === 'down' || value === 'negative' || value === 'thumbs_down') return 'down';
985
+ return 'down';
986
+ }
987
+
940
988
  function renderLessonDetailHtml(record, lessonId) {
941
989
  if (!record) {
942
990
  return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Lesson Not Found</title>
@@ -952,7 +1000,7 @@ a{color:#22d3ee;text-decoration:none}</style></head><body>
952
1000
  const fb = record.feedbackEvent || {};
953
1001
  const mem = record.memoryRecord || {};
954
1002
  const merged = { ...fb, ...mem };
955
- const signal = merged.signal || 'down';
1003
+ const signal = normalizeLessonSignal(merged.signal);
956
1004
  const emoji = signal === 'up' ? '👍' : '👎';
957
1005
  const signalColor = signal === 'up' ? '#4ade80' : '#f87171';
958
1006
  const title = merged.title || merged.context || 'Untitled Lesson';
@@ -1228,6 +1276,7 @@ function renderRobotsTxt(runtimeConfig) {
1228
1276
  function renderSitemapXml(runtimeConfig) {
1229
1277
  const entries = [
1230
1278
  { path: '/', changefreq: 'weekly', priority: '1.0' },
1279
+ { path: '/pro', changefreq: 'weekly', priority: '0.9' },
1231
1280
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
1232
1281
  ];
1233
1282
  return [
@@ -1550,7 +1599,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
1550
1599
  }
1551
1600
 
1552
1601
  function sendTelemetryOnce(eventType, extra = {}) {
1553
- const marker = ['rlhf', eventType, sessionId || traceId || 'unknown'].join(':');
1602
+ const marker = ['thumbgate', eventType, sessionId || traceId || 'unknown'].join(':');
1554
1603
  try {
1555
1604
  if (window.sessionStorage && window.sessionStorage.getItem(marker)) {
1556
1605
  return;
@@ -2157,6 +2206,24 @@ function createApiServer() {
2157
2206
  return;
2158
2207
  }
2159
2208
 
2209
+ if (isGetLikeRequest && pathname === '/js/buyer-intent.js') {
2210
+ try {
2211
+ const script = fs.readFileSync(BUYER_INTENT_SCRIPT_PATH, 'utf-8');
2212
+ res.writeHead(200, {
2213
+ 'Content-Type': 'application/javascript; charset=utf-8',
2214
+ 'Cache-Control': 'public, max-age=86400',
2215
+ });
2216
+ if (!isHeadRequest) {
2217
+ res.end(script);
2218
+ } else {
2219
+ res.end();
2220
+ }
2221
+ } catch {
2222
+ sendJson(res, 404, { error: 'Buyer intent script not found' });
2223
+ }
2224
+ return;
2225
+ }
2226
+
2160
2227
 
2161
2228
  // User feedback → GitHub Issues
2162
2229
  if (req.method === 'POST' && pathname === '/api/feedback/submit') {
@@ -2189,6 +2256,9 @@ function createApiServer() {
2189
2256
  req.on('data', (chunk) => chunks.push(chunk));
2190
2257
  req.on('end', () => {
2191
2258
  try {
2259
+ const accepts = String(req.headers.accept || '').toLowerCase();
2260
+ const requestedWith = String(req.headers['x-requested-with'] || '').toLowerCase();
2261
+ const wantsJson = accepts.includes('application/json') || requestedWith === 'fetch';
2192
2262
  const body = Buffer.concat(chunks).toString();
2193
2263
  const params = new URLSearchParams(body);
2194
2264
  const email = (params.get('email') || '').trim().toLowerCase();
@@ -2199,7 +2269,65 @@ function createApiServer() {
2199
2269
  const newsletterPath = path.join(getFeedbackPaths().FEEDBACK_DIR, 'newsletter-subscribers.jsonl');
2200
2270
  const dir = path.dirname(newsletterPath);
2201
2271
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
2202
- fs.appendFileSync(newsletterPath, JSON.stringify({ email, subscribedAt: new Date().toISOString(), source: 'landing-page' }) + '\n');
2272
+ const existingEntries = fs.existsSync(newsletterPath)
2273
+ ? fs.readFileSync(newsletterPath, 'utf8').split('\n').map((line) => line.trim()).filter(Boolean)
2274
+ : [];
2275
+ const duplicate = existingEntries.some((line) => {
2276
+ try {
2277
+ const entry = JSON.parse(line);
2278
+ return String(entry.email || '').trim().toLowerCase() === email;
2279
+ } catch {
2280
+ return false;
2281
+ }
2282
+ });
2283
+ const referrer = String(req.headers.referer || req.headers.referrer || '').trim();
2284
+ let attribution = {};
2285
+ let referrerHost = null;
2286
+ let landingPath = '/';
2287
+ if (referrer) {
2288
+ try {
2289
+ const referrerUrl = new URL(referrer);
2290
+ referrerHost = referrerUrl.host || null;
2291
+ landingPath = referrerUrl.pathname || '/';
2292
+ attribution = {
2293
+ source: referrerUrl.searchParams.get('utm_source') || null,
2294
+ medium: referrerUrl.searchParams.get('utm_medium') || null,
2295
+ campaign: referrerUrl.searchParams.get('utm_campaign') || null,
2296
+ content: referrerUrl.searchParams.get('utm_content') || null,
2297
+ term: referrerUrl.searchParams.get('utm_term') || null,
2298
+ creator: referrerUrl.searchParams.get('creator') || null,
2299
+ community: referrerUrl.searchParams.get('community') || referrerUrl.searchParams.get('subreddit') || null,
2300
+ postId: referrerUrl.searchParams.get('post_id') || referrerUrl.searchParams.get('postId') || null,
2301
+ commentId: referrerUrl.searchParams.get('comment_id') || referrerUrl.searchParams.get('commentId') || null,
2302
+ campaignVariant: referrerUrl.searchParams.get('campaign_variant') || referrerUrl.searchParams.get('variant') || null,
2303
+ offerCode: referrerUrl.searchParams.get('offer_code') || referrerUrl.searchParams.get('offer') || null,
2304
+ landingPath,
2305
+ };
2306
+ } catch {
2307
+ // Ignore invalid referrer values.
2308
+ }
2309
+ }
2310
+ if (!duplicate) {
2311
+ fs.appendFileSync(newsletterPath, JSON.stringify({
2312
+ email,
2313
+ subscribedAt: new Date().toISOString(),
2314
+ source: attribution.source || 'landing-page',
2315
+ referrer: referrer || null,
2316
+ referrerHost,
2317
+ landingPath,
2318
+ attribution,
2319
+ }) + '\n');
2320
+ }
2321
+ if (wantsJson) {
2322
+ sendJson(res, 200, {
2323
+ accepted: true,
2324
+ duplicate,
2325
+ email,
2326
+ landingPath,
2327
+ source: attribution.source || 'landing-page',
2328
+ });
2329
+ return;
2330
+ }
2203
2331
  res.writeHead(302, { Location: '/?subscribed=1' });
2204
2332
  res.end();
2205
2333
  } catch {
@@ -2325,7 +2453,7 @@ async function addContext(){
2325
2453
  const ctx=document.getElementById('contextInput').value.trim();
2326
2454
  if(!ctx)return;
2327
2455
  try{
2328
- await fetch('/feedback/quick/context',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({signal:'${signal}',context:ctx,relatedFeedbackId:'${feedbackId}'})});
2456
+ await fetch('/feedback/quick/context',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({signal:'${signal}',context:ctx,relatedFeedbackId:'${feedbackId}',feedbackSessionId:'${result.feedbackSession?.sessionId || ''}'})});
2329
2457
  document.getElementById('toast').style.display='block';
2330
2458
  document.getElementById('contextForm').style.display='none';
2331
2459
  setTimeout(()=>document.getElementById('toast').style.display='none',3000);
@@ -2343,6 +2471,7 @@ async function addContext(){
2343
2471
  const signal = body.signal;
2344
2472
  const context = typeof body.context === 'string' ? body.context.trim() : '';
2345
2473
  const relatedFeedbackId = typeof body.relatedFeedbackId === 'string' ? body.relatedFeedbackId.trim() : '';
2474
+ const feedbackSessionId = typeof body.feedbackSessionId === 'string' ? body.feedbackSessionId.trim() : '';
2346
2475
  if (signal !== 'up' && signal !== 'down') {
2347
2476
  sendJson(res, 400, { error: 'signal must be up or down' });
2348
2477
  return;
@@ -2351,18 +2480,45 @@ async function addContext(){
2351
2480
  sendJson(res, 400, { error: 'context is required' });
2352
2481
  return;
2353
2482
  }
2354
- const result = captureFeedback({
2355
- signal,
2356
- context,
2483
+ if (!relatedFeedbackId) {
2484
+ sendJson(res, 400, { error: 'relatedFeedbackId is required' });
2485
+ return;
2486
+ }
2487
+ const feedbackDir = getSafeDataDir();
2488
+ const detailField = signal === 'down' ? 'whatWentWrong' : 'whatWorked';
2489
+ const updated = updateLessonRecord(feedbackDir, relatedFeedbackId, (existing) => {
2490
+ const nextTags = Array.from(new Set([
2491
+ ...((Array.isArray(existing.tags) ? existing.tags : []).filter(Boolean)),
2492
+ 'statusline',
2493
+ 'quick-capture',
2494
+ 'follow-up-context',
2495
+ ]));
2496
+ return {
2497
+ ...existing,
2498
+ tags: nextTags,
2499
+ [detailField]: mergeFollowUpDetail(existing[detailField], context),
2500
+ };
2501
+ });
2502
+ if (!updated) {
2503
+ sendJson(res, 404, { error: 'Related lesson not found' });
2504
+ return;
2505
+ }
2506
+ let feedbackSession = null;
2507
+ if (feedbackSessionId) {
2508
+ try {
2509
+ const { appendToSession } = require('../../scripts/feedback-session');
2510
+ feedbackSession = appendToSession(feedbackSessionId, context, 'user');
2511
+ } catch (_err) {
2512
+ feedbackSession = { status: 'error' };
2513
+ }
2514
+ }
2515
+ sendJson(res, 200, {
2516
+ ok: true,
2357
2517
  relatedFeedbackId,
2358
- chatHistory: readRecentConversationWindow({
2359
- feedbackDir: getSafeDataDir(),
2360
- limit: 10,
2361
- }),
2362
- tags: ['statusline', 'quick-capture', 'follow-up-context'],
2518
+ detailField,
2519
+ updated,
2520
+ feedbackSession,
2363
2521
  });
2364
- const code = result.accepted ? 200 : 422;
2365
- sendJson(res, code, result);
2366
2522
  return;
2367
2523
  }
2368
2524
 
@@ -2382,28 +2538,27 @@ async function addContext(){
2382
2538
  const lessonId = decodeURIComponent(lessonUpdateMatch[1]);
2383
2539
  const feedbackDir = getSafeDataDir();
2384
2540
  const body = await parseJsonBody(req);
2385
- const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
2386
2541
  const record = findRecordById(lessonId, feedbackDir);
2387
2542
  if (!record) {
2388
2543
  sendJson(res, 404, { error: 'Record not found' });
2389
2544
  return;
2390
2545
  }
2391
- const existing = record.memoryRecord || record.feedbackEvent || {};
2392
- const updated = { ...existing };
2393
- if (body.title !== undefined) updated.title = body.title;
2394
- if (body.content !== undefined) updated.context = body.content;
2395
- if (body.tags !== undefined) {
2396
- updated.tags = typeof body.tags === 'string'
2397
- ? body.tags.split(',').map((t) => t.trim()).filter(Boolean)
2398
- : body.tags;
2399
- }
2400
- if (body.whatWentWrong !== undefined) updated.whatWentWrong = body.whatWentWrong;
2401
- if (body.whatWorked !== undefined) updated.whatWorked = body.whatWorked;
2402
- const success = updateRecordInJsonl(memoryLogPath, lessonId, updated);
2403
- if (!success) {
2404
- // Try feedback log
2405
- const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
2406
- updateRecordInJsonl(feedbackLogPath, lessonId, updated);
2546
+ const updated = updateLessonRecord(feedbackDir, lessonId, (existing) => {
2547
+ const next = { ...existing };
2548
+ if (body.title !== undefined) next.title = body.title;
2549
+ if (body.content !== undefined) next.context = body.content;
2550
+ if (body.tags !== undefined) {
2551
+ next.tags = typeof body.tags === 'string'
2552
+ ? body.tags.split(',').map((t) => t.trim()).filter(Boolean)
2553
+ : body.tags;
2554
+ }
2555
+ if (body.whatWentWrong !== undefined) next.whatWentWrong = body.whatWentWrong;
2556
+ if (body.whatWorked !== undefined) next.whatWorked = body.whatWorked;
2557
+ return next;
2558
+ });
2559
+ if (!updated) {
2560
+ sendJson(res, 404, { error: 'Record not found' });
2561
+ return;
2407
2562
  }
2408
2563
  sendJson(res, 200, { ok: true, updated });
2409
2564
  return;
@@ -2472,6 +2627,25 @@ async function addContext(){
2472
2627
  return;
2473
2628
  }
2474
2629
 
2630
+ if (isGetLikeRequest && pathname === '/pro') {
2631
+ try {
2632
+ servePublicMarketingPage({
2633
+ req,
2634
+ res,
2635
+ parsed,
2636
+ hostedConfig,
2637
+ isHeadRequest,
2638
+ renderHtml: loadProPageHtml,
2639
+ extraTelemetry: {
2640
+ pageType: 'pro_landing',
2641
+ },
2642
+ });
2643
+ } catch (err) {
2644
+ sendText(res, 500, err.message || 'Pro page unavailable');
2645
+ }
2646
+ return;
2647
+ }
2648
+
2475
2649
  if (isGetLikeRequest && pathname === '/guide') {
2476
2650
  try {
2477
2651
  const html = fs.readFileSync(GUIDE_PAGE_PATH, 'utf-8');
@@ -2539,7 +2713,7 @@ async function addContext(){
2539
2713
  version: pkg.version,
2540
2714
  status: 'ok',
2541
2715
  docs: 'https://github.com/IgorGanapolsky/ThumbGate',
2542
- endpoints: ['/health', '/dashboard', '/guide', '/learn', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/analytics/databricks/export'],
2716
+ endpoints: ['/health', '/dashboard', '/guide', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/analytics/databricks/export'],
2543
2717
  }, {}, {
2544
2718
  headOnly: isHeadRequest,
2545
2719
  });
@@ -3490,6 +3664,80 @@ async function addContext(){
3490
3664
  return;
3491
3665
  }
3492
3666
 
3667
+ if (req.method === 'POST' && pathname === '/v1/gates/task-scope') {
3668
+ const body = await parseJsonBody(req);
3669
+ const scope = setTaskScope({
3670
+ taskId: body.taskId,
3671
+ summary: body.summary,
3672
+ allowedPaths: body.allowedPaths,
3673
+ protectedPaths: body.protectedPaths,
3674
+ repoPath: body.repoPath,
3675
+ localOnly: body.localOnly === true,
3676
+ clear: body.clear === true,
3677
+ });
3678
+ sendJson(res, 200, { scope });
3679
+ return;
3680
+ }
3681
+
3682
+ if (req.method === 'GET' && pathname === '/v1/gates/task-scope') {
3683
+ sendJson(res, 200, getScopeState());
3684
+ return;
3685
+ }
3686
+
3687
+ if (req.method === 'POST' && pathname === '/v1/gates/branch-governance') {
3688
+ const body = await parseJsonBody(req);
3689
+ const branchGovernance = setBranchGovernance({
3690
+ branchName: body.branchName,
3691
+ baseBranch: body.baseBranch,
3692
+ prRequired: body.prRequired,
3693
+ prNumber: body.prNumber,
3694
+ prUrl: body.prUrl,
3695
+ queueRequired: body.queueRequired,
3696
+ localOnly: body.localOnly === true,
3697
+ releaseVersion: body.releaseVersion,
3698
+ releaseEvidence: body.releaseEvidence,
3699
+ releaseSensitiveGlobs: body.releaseSensitiveGlobs,
3700
+ clear: body.clear === true,
3701
+ });
3702
+ sendJson(res, 200, { branchGovernance });
3703
+ return;
3704
+ }
3705
+
3706
+ if (req.method === 'GET' && pathname === '/v1/gates/branch-governance') {
3707
+ sendJson(res, 200, { branchGovernance: getBranchGovernanceState() });
3708
+ return;
3709
+ }
3710
+
3711
+ if (req.method === 'POST' && pathname === '/v1/gates/protected-approval') {
3712
+ const body = await parseJsonBody(req);
3713
+ const approval = approveProtectedAction({
3714
+ pathGlobs: body.pathGlobs,
3715
+ reason: body.reason,
3716
+ evidence: body.evidence,
3717
+ taskId: body.taskId,
3718
+ ttlMs: body.ttlMs,
3719
+ });
3720
+ sendJson(res, 200, { approved: true, approval });
3721
+ return;
3722
+ }
3723
+
3724
+ if (req.method === 'GET' && pathname === '/v1/ops/integrity') {
3725
+ const command = parsed.searchParams.get('command') || undefined;
3726
+ const baseBranch = parsed.searchParams.get('baseBranch') || undefined;
3727
+ const requirePrForReleaseSensitive = parsed.searchParams.get('requirePrForReleaseSensitive') === 'true';
3728
+ const requireVersionNotBehindBase = parsed.searchParams.get('requireVersionNotBehindBase') === 'true';
3729
+ const report = evaluateOperationalIntegrity({
3730
+ repoPath: process.cwd(),
3731
+ baseBranch,
3732
+ command,
3733
+ requirePrForReleaseSensitive,
3734
+ requireVersionNotBehindBase,
3735
+ branchGovernance: getBranchGovernanceState(),
3736
+ });
3737
+ sendJson(res, 200, report);
3738
+ return;
3739
+ }
3740
+
3493
3741
  if (req.method === 'GET' && pathname === '/v1/feedback/summary') {
3494
3742
  const recent = Number(parsed.searchParams.get('recent') || 20);
3495
3743
  const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20);
@@ -3521,7 +3769,7 @@ async function addContext(){
3521
3769
  const signal = parsed.searchParams.get('signal') || null;
3522
3770
  let results;
3523
3771
  try {
3524
- results = searchRlhf({
3772
+ results = searchThumbgate({
3525
3773
  query,
3526
3774
  limit: Number.isFinite(limit) ? limit : 10,
3527
3775
  source,
@@ -3538,7 +3786,7 @@ async function addContext(){
3538
3786
  const body = await parseJsonBody(req);
3539
3787
  let results;
3540
3788
  try {
3541
- results = searchRlhf({
3789
+ results = searchThumbgate({
3542
3790
  query: body.query || body.q || '',
3543
3791
  limit: body.limit,
3544
3792
  source: body.source,
@@ -4173,17 +4421,19 @@ async function addContext(){
4173
4421
  });
4174
4422
  }
4175
4423
 
4176
- function startServer({ port } = {}) {
4424
+ function startServer({ port, host } = {}) {
4177
4425
  const listenPort = Number(port ?? process.env.PORT ?? 8787);
4426
+ const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
4178
4427
  const server = createApiServer();
4179
4428
  return new Promise((resolve) => {
4180
- server.listen(listenPort, () => {
4429
+ server.listen(listenPort, listenHost, () => {
4181
4430
  const address = server.address();
4182
4431
  const actualPort = (address && typeof address === 'object' && address.port)
4183
4432
  ? address.port
4184
4433
  : listenPort;
4185
4434
  resolve({
4186
4435
  server,
4436
+ host: listenHost,
4187
4437
  port: actualPort,
4188
4438
  });
4189
4439
  });
@@ -4200,7 +4450,7 @@ module.exports = {
4200
4450
  };
4201
4451
 
4202
4452
  if (require.main === module) {
4203
- startServer().then(({ port }) => {
4204
- console.log(`ThumbGate API listening on http://localhost:${port}`);
4453
+ startServer().then(({ host, port }) => {
4454
+ console.log(`ThumbGate API listening on http://${host}:${port}`);
4205
4455
  });
4206
4456
  }