thumbgate 1.3.0 → 1.4.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +32 -13
  2. package/.claude-plugin/plugin.json +15 -2
  3. package/.well-known/llms.txt +60 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +109 -20
  6. package/adapters/README.md +1 -1
  7. package/adapters/chatgpt/openapi.yaml +168 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/mcp/server-stdio.js +84 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +200 -13
  13. package/bin/postinstall.js +8 -2
  14. package/config/budget.json +18 -0
  15. package/config/gates/code-edit.json +61 -0
  16. package/config/gates/db-write.json +61 -0
  17. package/config/gates/default.json +154 -3
  18. package/config/gates/deploy.json +61 -0
  19. package/config/github-about.json +2 -1
  20. package/config/merge-quality-checks.json +23 -0
  21. package/openapi/openapi.yaml +168 -0
  22. package/package.json +42 -10
  23. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  24. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  25. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  26. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  27. package/plugins/codex-profile/.mcp.json +1 -1
  28. package/plugins/codex-profile/INSTALL.md +27 -4
  29. package/plugins/codex-profile/README.md +33 -9
  30. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  31. package/plugins/opencode-profile/INSTALL.md +1 -1
  32. package/public/blog.html +73 -0
  33. package/public/compare/mem0.html +189 -0
  34. package/public/compare/speclock.html +180 -0
  35. package/public/compare.html +10 -2
  36. package/public/guide.html +2 -2
  37. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  38. package/public/guides/codex-cli-guardrails.html +158 -0
  39. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/pre-action-gates.html +162 -0
  41. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  42. package/public/index.html +136 -50
  43. package/public/lessons.html +33 -24
  44. package/public/llm-context.md +140 -0
  45. package/public/pro.html +24 -22
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/access-anomaly-detector.js +1 -1
  48. package/scripts/adk-consolidator.js +1 -5
  49. package/scripts/agent-security-hardening.js +4 -6
  50. package/scripts/agentic-data-pipeline.js +1 -3
  51. package/scripts/async-job-runner.js +1 -5
  52. package/scripts/audit-trail.js +1 -5
  53. package/scripts/background-agent-governance.js +2 -10
  54. package/scripts/billing.js +2 -16
  55. package/scripts/budget-enforcer.js +173 -0
  56. package/scripts/build-codex-plugin.js +152 -0
  57. package/scripts/check-congruence.js +132 -14
  58. package/scripts/commercial-offer.js +5 -7
  59. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  60. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  61. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  62. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  63. package/scripts/context-engine.js +21 -6
  64. package/scripts/contextfs.js +1 -21
  65. package/scripts/dashboard.js +20 -0
  66. package/scripts/decision-journal.js +341 -0
  67. package/scripts/delegation-runtime.js +1 -5
  68. package/scripts/distribution-surfaces.js +26 -0
  69. package/scripts/document-intake.js +927 -0
  70. package/scripts/ephemeral-agent-store.js +1 -8
  71. package/scripts/evolution-state.js +1 -5
  72. package/scripts/experiment-tracker.js +1 -5
  73. package/scripts/export-databricks-bundle.js +1 -5
  74. package/scripts/export-hf-dataset.js +1 -5
  75. package/scripts/export-training.js +1 -5
  76. package/scripts/feedback-attribution.js +1 -16
  77. package/scripts/feedback-history-distiller.js +1 -16
  78. package/scripts/feedback-loop.js +1 -5
  79. package/scripts/feedback-root-consolidator.js +2 -21
  80. package/scripts/feedback-session.js +49 -0
  81. package/scripts/feedback-to-rules.js +188 -28
  82. package/scripts/filesystem-search.js +1 -9
  83. package/scripts/fs-utils.js +104 -0
  84. package/scripts/gates-engine.js +149 -4
  85. package/scripts/github-about.js +32 -8
  86. package/scripts/gtm-revenue-loop.js +1 -5
  87. package/scripts/harness-selector.js +148 -0
  88. package/scripts/hosted-job-launcher.js +1 -5
  89. package/scripts/hybrid-feedback-context.js +7 -33
  90. package/scripts/intervention-policy.js +58 -1
  91. package/scripts/lesson-db.js +3 -18
  92. package/scripts/lesson-inference.js +194 -16
  93. package/scripts/lesson-retrieval.js +60 -24
  94. package/scripts/llm-client.js +59 -0
  95. package/scripts/managed-lesson-agent.js +183 -0
  96. package/scripts/marketing-experiment.js +8 -22
  97. package/scripts/meta-agent-loop.js +624 -0
  98. package/scripts/metered-billing.js +1 -1
  99. package/scripts/money-watcher.js +1 -4
  100. package/scripts/obsidian-export.js +1 -5
  101. package/scripts/operational-integrity.js +15 -3
  102. package/scripts/org-dashboard.js +6 -1
  103. package/scripts/per-step-scoring.js +2 -4
  104. package/scripts/pr-manager.js +201 -19
  105. package/scripts/pro-features.js +3 -2
  106. package/scripts/prompt-dlp.js +3 -3
  107. package/scripts/prove-adapters.js +1 -5
  108. package/scripts/prove-attribution.js +1 -5
  109. package/scripts/prove-automation.js +1 -3
  110. package/scripts/prove-cloudflare-sandbox.js +1 -3
  111. package/scripts/prove-data-pipeline.js +1 -3
  112. package/scripts/prove-intelligence.js +1 -3
  113. package/scripts/prove-lancedb.js +1 -5
  114. package/scripts/prove-local-intelligence.js +1 -3
  115. package/scripts/prove-packaged-runtime.js +75 -9
  116. package/scripts/prove-predictive-insights.js +1 -3
  117. package/scripts/prove-training-export.js +1 -3
  118. package/scripts/prove-workflow-contract.js +1 -5
  119. package/scripts/rate-limiter.js +3 -1
  120. package/scripts/reddit-dm-outreach.js +14 -4
  121. package/scripts/schedule-manager.js +3 -5
  122. package/scripts/security-scanner.js +448 -0
  123. package/scripts/self-distill-agent.js +579 -0
  124. package/scripts/semantic-dedup.js +115 -0
  125. package/scripts/skill-exporter.js +1 -3
  126. package/scripts/skill-generator.js +1 -5
  127. package/scripts/social-analytics/engagement-audit.js +1 -18
  128. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  129. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  130. package/scripts/social-analytics/publishers/zernio.js +51 -0
  131. package/scripts/social-pipeline.js +1 -3
  132. package/scripts/social-post-hourly.js +47 -4
  133. package/scripts/statusline-links.js +6 -5
  134. package/scripts/statusline.sh +29 -153
  135. package/scripts/sync-branch-protection.js +340 -0
  136. package/scripts/tessl-export.js +1 -3
  137. package/scripts/thumbgate-search.js +32 -1
  138. package/scripts/tool-kpi-tracker.js +1 -1
  139. package/scripts/tool-registry.js +106 -2
  140. package/scripts/vector-store.js +1 -5
  141. package/scripts/weekly-auto-post.js +1 -1
  142. package/scripts/workflow-sentinel.js +91 -0
  143. package/skills/thumbgate/SKILL.md +1 -1
  144. package/src/api/server.js +273 -4
  145. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  146. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -705,6 +705,11 @@ function buildReasoning(report) {
705
705
  `Workflow sentinel risk ${report.band} (${report.riskScore}) for ${report.toolName}.`,
706
706
  `Blast radius: ${report.blastRadius.summary}.`,
707
707
  ];
708
+ if (report.decisionControl) {
709
+ lines.push(
710
+ `Decision control: ${report.decisionControl.decisionOwner} owns a ${report.decisionControl.reversibility} action via ${report.decisionControl.executionMode}.`
711
+ );
712
+ }
708
713
  if (report.learnedPolicy && report.learnedPolicy.enabled && report.learnedPolicy.prediction) {
709
714
  lines.push(
710
715
  `Learned policy predicted ${report.learnedPolicy.prediction.label} (${report.learnedPolicy.prediction.confidence}).`
@@ -732,6 +737,80 @@ function getSentinelActionType(toolName) {
732
737
  return '';
733
738
  }
734
739
 
740
+ function classifyReversibility({ command, blastRadius, integrity, protectedSurface }) {
741
+ const text = String(command || '');
742
+ const blockers = integrity && Array.isArray(integrity.blockers) ? integrity.blockers : [];
743
+ const destructiveCommand = /\bgit\s+push\b.*(?:--force|-f)\b/i.test(text)
744
+ || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(text)
745
+ || /\brm\s+-rf\b/i.test(text)
746
+ || /\b(?:npm|yarn|pnpm)\s+publish\b/i.test(text)
747
+ || /\bgh\s+release\s+create\b/i.test(text)
748
+ || /\bgit\s+tag\b/i.test(text);
749
+ const releaseSensitive = blastRadius && Array.isArray(blastRadius.releaseSensitiveFiles)
750
+ ? blastRadius.releaseSensitiveFiles.length > 0
751
+ : false;
752
+ const unapprovedProtected = protectedSurface && Array.isArray(protectedSurface.unapprovedProtectedFiles)
753
+ ? protectedSurface.unapprovedProtectedFiles.length > 0
754
+ : false;
755
+ const hardBlockers = blockers.some((blocker) => /publish|merge|release|protected/i.test(String(blocker.code || '')));
756
+
757
+ if (destructiveCommand || releaseSensitive || unapprovedProtected || hardBlockers) {
758
+ return 'one_way_door';
759
+ }
760
+ if ((blastRadius && blastRadius.fileCount >= 4) || (blastRadius && blastRadius.surfaceCount >= 2)) {
761
+ return 'reviewable';
762
+ }
763
+ return 'two_way_door';
764
+ }
765
+
766
+ function buildDecisionControl({
767
+ decision,
768
+ risk,
769
+ command,
770
+ blastRadius,
771
+ integrity,
772
+ protectedSurface,
773
+ }) {
774
+ const reversibility = classifyReversibility({
775
+ command,
776
+ blastRadius,
777
+ integrity,
778
+ protectedSurface,
779
+ });
780
+ const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
781
+ const requiresCheckpoint = decision === 'warn'
782
+ || (decision === 'allow' && (reversibility !== 'two_way_door' || hasOperationalBlockers));
783
+ const executionMode = decision === 'deny'
784
+ ? 'blocked'
785
+ : requiresCheckpoint
786
+ ? 'checkpoint_required'
787
+ : 'auto_execute';
788
+ const decisionOwner = executionMode === 'blocked'
789
+ ? 'human'
790
+ : executionMode === 'checkpoint_required'
791
+ ? reversibility === 'two_way_door' && !hasOperationalBlockers
792
+ ? 'shared'
793
+ : 'human'
794
+ : 'agent';
795
+
796
+ return {
797
+ executionMode,
798
+ decisionOwner,
799
+ reversibility,
800
+ requiresHumanApproval: executionMode === 'checkpoint_required' && decisionOwner !== 'agent',
801
+ recommendedAction: executionMode === 'blocked'
802
+ ? 'halt'
803
+ : executionMode === 'checkpoint_required'
804
+ ? 'review'
805
+ : 'proceed',
806
+ summary: executionMode === 'blocked'
807
+ ? 'Do not proceed until the remediation steps are completed.'
808
+ : executionMode === 'checkpoint_required'
809
+ ? 'Pause for explicit review before executing this action.'
810
+ : 'Safe to execute quickly with standard evidence capture.',
811
+ };
812
+ }
813
+
735
814
  function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blastRadius, command }) {
736
815
  const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
737
816
  const destructiveBypass = /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command) || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
@@ -923,11 +1002,23 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
923
1002
  commandInfo: integrity.commandInfo,
924
1003
  },
925
1004
  };
1005
+ report.decisionControl = buildDecisionControl({
1006
+ decision,
1007
+ risk,
1008
+ command: toolInput.command || '',
1009
+ blastRadius: {
1010
+ ...blastRadius,
1011
+ unapprovedProtectedFiles: protectedSurfaceForRisk.unapprovedProtectedFiles.length,
1012
+ },
1013
+ integrity,
1014
+ protectedSurface: protectedSurfaceForRisk,
1015
+ });
926
1016
  report.reasoning = buildReasoning(report);
927
1017
  return report;
928
1018
  }
929
1019
 
930
1020
  module.exports = {
1021
+ buildDecisionControl,
931
1022
  DEFAULT_PROTECTED_FILE_GLOBS,
932
1023
  buildBlastRadius,
933
1024
  buildEvidence,
@@ -92,7 +92,7 @@ Bounded retrieval of relevant feedback history for the current task. The agent g
92
92
  | Dashboard | - | Yes | Yes |
93
93
  | DPO export | - | Yes | Yes |
94
94
  | Seats | 1 | 1 | Per-seat |
95
- | Price | $0 | $19/mo | $12/seat/mo |
95
+ | Price | $0 | $19/mo | $99/seat/mo |
96
96
 
97
97
  Start a 7-day free trial of Pro: <https://buy.stripe.com/fZu9AT3Ug6zcdWh0XN3sI08>
98
98
 
package/src/api/server.js CHANGED
@@ -108,6 +108,14 @@ const {
108
108
  const {
109
109
  evaluateOperationalIntegrity,
110
110
  } = require('../../scripts/operational-integrity');
111
+ const {
112
+ evaluateWorkflowSentinel,
113
+ } = require('../../scripts/workflow-sentinel');
114
+ const {
115
+ recordDecisionEvaluation,
116
+ recordDecisionOutcome,
117
+ computeDecisionMetrics,
118
+ } = require('../../scripts/decision-journal');
111
119
  const {
112
120
  generateDashboard,
113
121
  } = require('../../scripts/dashboard');
@@ -152,6 +160,11 @@ const {
152
160
  appendWorkflowSprintLead,
153
161
  advanceWorkflowSprintLead,
154
162
  } = require('../../scripts/workflow-sprint-intake');
163
+ const {
164
+ importDocument,
165
+ listImportedDocuments,
166
+ readImportedDocument,
167
+ } = require('../../scripts/document-intake');
155
168
  const {
156
169
  checkLimit,
157
170
  UPGRADE_MESSAGE: RATE_LIMIT_MESSAGE,
@@ -172,6 +185,8 @@ const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
172
185
  const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
173
186
  const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
174
187
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
188
+ const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
189
+ const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
175
190
  const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
176
191
  const VISITOR_COOKIE_NAME = 'thumbgate_visitor_id';
177
192
  const SESSION_COOKIE_NAME = 'thumbgate_session_id';
@@ -1345,6 +1360,32 @@ function renderRobotsTxt(runtimeConfig) {
1345
1360
  return [
1346
1361
  'User-agent: *',
1347
1362
  'Allow: /',
1363
+ '',
1364
+ '# AI crawler access — allow all major LLM crawlers',
1365
+ 'User-agent: GPTBot',
1366
+ 'Allow: /',
1367
+ '',
1368
+ 'User-agent: ClaudeBot',
1369
+ 'Allow: /',
1370
+ '',
1371
+ 'User-agent: PerplexityBot',
1372
+ 'Allow: /',
1373
+ '',
1374
+ 'User-agent: Googlebot',
1375
+ 'Allow: /',
1376
+ '',
1377
+ 'User-agent: Bingbot',
1378
+ 'Allow: /',
1379
+ '',
1380
+ 'User-agent: anthropic-ai',
1381
+ 'Allow: /',
1382
+ '',
1383
+ 'User-agent: Google-Extended',
1384
+ 'Allow: /',
1385
+ '',
1386
+ '# LLM context document — clean declarative content for AI retrieval',
1387
+ `# ${runtimeConfig.appOrigin}/llm-context.md`,
1388
+ '',
1348
1389
  `Sitemap: ${runtimeConfig.appOrigin}/sitemap.xml`,
1349
1390
  ].join('\n');
1350
1391
  }
@@ -1353,6 +1394,7 @@ function renderSitemapXml(runtimeConfig) {
1353
1394
  const entries = [
1354
1395
  { path: '/', changefreq: 'weekly', priority: '1.0' },
1355
1396
  { path: '/pro', changefreq: 'weekly', priority: '0.9' },
1397
+ { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
1356
1398
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
1357
1399
  ];
1358
1400
  return [
@@ -2240,6 +2282,40 @@ function normalizeJobIdFromPath(pathname, suffix = '') {
2240
2282
  return match ? decodeURIComponent(match[1]) : null;
2241
2283
  }
2242
2284
 
2285
+ function normalizeDocumentIdFromPath(pathname) {
2286
+ const match = pathname.match(/^\/v1\/documents\/([^/]+)$/);
2287
+ return match ? decodeURIComponent(match[1]) : null;
2288
+ }
2289
+
2290
+ function isWithinDir(targetPath, baseDir) {
2291
+ if (!baseDir) return false;
2292
+ const resolvedBase = path.resolve(baseDir);
2293
+ const resolvedTarget = path.resolve(targetPath);
2294
+ const relative = path.relative(resolvedBase, resolvedTarget);
2295
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
2296
+ }
2297
+
2298
+ function resolveDocumentImportFilePath(inputPath, options = {}) {
2299
+ const normalized = normalizeNullableText(inputPath);
2300
+ if (!normalized) return null;
2301
+
2302
+ const { req, parsed, safeDataDir } = options;
2303
+ if (!isLoopbackHost(getRequestHostHeader(req))) {
2304
+ throw createHttpError(400, 'filePath import is only available on localhost requests; use content for hosted imports');
2305
+ }
2306
+
2307
+ const projectDir = resolveRequestProjectDir(req, parsed) || process.cwd();
2308
+ const resolved = path.resolve(projectDir, normalized);
2309
+ const allowed = isWithinDir(resolved, projectDir) || isWithinDir(resolved, safeDataDir);
2310
+ if (!allowed) {
2311
+ throw createHttpError(400, `Path must stay within ${projectDir} or ${safeDataDir}`);
2312
+ }
2313
+ if (!fs.existsSync(resolved)) {
2314
+ throw createHttpError(400, `Path does not exist: ${resolved}`);
2315
+ }
2316
+ return resolved;
2317
+ }
2318
+
2243
2319
  function createApiServer() {
2244
2320
  const expectedApiKey = getExpectedApiKey();
2245
2321
 
@@ -2516,6 +2592,17 @@ function createApiServer() {
2516
2592
  return;
2517
2593
  }
2518
2594
 
2595
+ if (isGetLikeRequest && pathname === '/.well-known/llms.txt') {
2596
+ const llmsTxtPath = path.join(__dirname, '..', '..', '.well-known', 'llms.txt');
2597
+ try {
2598
+ const content = fs.readFileSync(llmsTxtPath, 'utf8');
2599
+ sendText(res, 200, content, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=86400' }, { headOnly: isHeadRequest });
2600
+ } catch {
2601
+ sendJson(res, 404, { error: 'llms.txt not found' });
2602
+ }
2603
+ return;
2604
+ }
2605
+
2519
2606
  if (isGetLikeRequest && pathname === '/sitemap.xml') {
2520
2607
  sendText(res, 200, renderSitemapXml(hostedConfig), {
2521
2608
  'Content-Type': 'application/xml; charset=utf-8',
@@ -2525,6 +2612,22 @@ function createApiServer() {
2525
2612
  return;
2526
2613
  }
2527
2614
 
2615
+ if (isGetLikeRequest && pathname === '/llm-context.md') {
2616
+ const llmContextPath = path.resolve(__dirname, '../../public/llm-context.md');
2617
+ try {
2618
+ const content = fs.readFileSync(llmContextPath, 'utf8');
2619
+ sendText(res, 200, content, {
2620
+ 'Content-Type': 'text/markdown; charset=utf-8',
2621
+ 'X-Robots-Tag': 'all',
2622
+ }, {
2623
+ headOnly: isHeadRequest,
2624
+ });
2625
+ } catch (_err) {
2626
+ sendJson(res, 404, { error: 'Not found' });
2627
+ }
2628
+ return;
2629
+ }
2630
+
2528
2631
  // Quick feedback capture via GET — for statusline clickable links
2529
2632
  if (isGetLikeRequest && pathname === '/feedback/quick') {
2530
2633
  const signal = parsed.searchParams.get('signal');
@@ -2852,6 +2955,28 @@ async function addContext(){
2852
2955
  return;
2853
2956
  }
2854
2957
 
2958
+ if (isGetLikeRequest && pathname.startsWith('/guides/')) {
2959
+ try {
2960
+ const slug = pathname.replace('/guides/', '').replace(/[^a-z0-9-]/g, '');
2961
+ const guidePath = path.join(GUIDES_DIR, `${slug}.html`);
2962
+ if (!guidePath.startsWith(GUIDES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
2963
+ const html = fs.readFileSync(guidePath, 'utf-8');
2964
+ sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
2965
+ } catch { sendJson(res, 404, { error: 'Guide not found' }); }
2966
+ return;
2967
+ }
2968
+
2969
+ if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
2970
+ try {
2971
+ const slug = pathname.replace('/compare/', '').replace(/[^a-z0-9-]/g, '');
2972
+ const comparePath = path.join(COMPARE_DIR, `${slug}.html`);
2973
+ if (!comparePath.startsWith(COMPARE_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
2974
+ const html = fs.readFileSync(comparePath, 'utf-8');
2975
+ sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
2976
+ } catch { sendJson(res, 404, { error: 'Comparison not found' }); }
2977
+ return;
2978
+ }
2979
+
2855
2980
  if (isGetLikeRequest && pathname === '/') {
2856
2981
  if (wantsJson(req, parsed)) {
2857
2982
  sendJson(res, 200, {
@@ -2859,7 +2984,7 @@ async function addContext(){
2859
2984
  version: pkg.version,
2860
2985
  status: 'ok',
2861
2986
  docs: 'https://github.com/IgorGanapolsky/ThumbGate',
2862
- endpoints: ['/health', '/dashboard', '/guide', '/compare', '/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/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
2987
+ endpoints: ['/health', '/dashboard', '/guide', '/compare', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
2863
2988
  }, {}, {
2864
2989
  headOnly: isHeadRequest,
2865
2990
  });
@@ -4054,6 +4179,34 @@ async function addContext(){
4054
4179
  return;
4055
4180
  }
4056
4181
 
4182
+ if (req.method === 'GET' && pathname === '/v1/documents') {
4183
+ const limit = Number(parsed.searchParams.get('limit') || 20);
4184
+ const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
4185
+ const tag = parsed.searchParams.get('tag') || '';
4186
+ const results = listImportedDocuments({
4187
+ feedbackDir: requestFeedbackDir,
4188
+ limit: Number.isFinite(limit) ? limit : 20,
4189
+ query,
4190
+ tag,
4191
+ });
4192
+ sendJson(res, 200, results);
4193
+ return;
4194
+ }
4195
+
4196
+ {
4197
+ const documentId = normalizeDocumentIdFromPath(pathname);
4198
+ if (req.method === 'GET' && documentId) {
4199
+ const document = readImportedDocument(documentId, {
4200
+ feedbackDir: requestFeedbackDir,
4201
+ });
4202
+ if (!document) {
4203
+ throw createHttpError(404, `Imported document not found: ${documentId}`);
4204
+ }
4205
+ sendJson(res, 200, { document });
4206
+ return;
4207
+ }
4208
+ }
4209
+
4057
4210
  if (req.method === 'POST' && pathname === '/v1/feedback/capture') {
4058
4211
  const captureLimit = checkLimit('capture_feedback');
4059
4212
  if (!captureLimit.allowed) {
@@ -4127,6 +4280,31 @@ async function addContext(){
4127
4280
  return;
4128
4281
  }
4129
4282
 
4283
+ if (req.method === 'POST' && pathname === '/v1/documents/import') {
4284
+ const body = await parseJsonBody(req, 2 * 1024 * 1024);
4285
+ const document = importDocument({
4286
+ filePath: body.filePath
4287
+ ? resolveDocumentImportFilePath(body.filePath, {
4288
+ req,
4289
+ parsed,
4290
+ safeDataDir: requestSafeDataDir,
4291
+ })
4292
+ : null,
4293
+ content: normalizeNullableText(body.content),
4294
+ title: normalizeNullableText(body.title),
4295
+ sourceFormat: normalizeNullableText(body.sourceFormat),
4296
+ sourceUrl: normalizeNullableText(body.sourceUrl),
4297
+ tags: extractTags(body.tags),
4298
+ proposeGates: body.proposeGates !== false,
4299
+ feedbackDir: requestFeedbackDir,
4300
+ });
4301
+ sendJson(res, 201, {
4302
+ ok: true,
4303
+ document,
4304
+ });
4305
+ return;
4306
+ }
4307
+
4130
4308
  if (req.method === 'POST' && pathname === '/v1/dpo/export') {
4131
4309
  const body = await parseJsonBody(req);
4132
4310
  const paths = resolveDpoExportPaths(body, {
@@ -4569,6 +4747,85 @@ async function addContext(){
4569
4747
  return;
4570
4748
  }
4571
4749
 
4750
+ if (req.method === 'POST' && pathname === '/v1/decisions/evaluate') {
4751
+ const body = await parseJsonBody(req);
4752
+ if (!body.toolName) {
4753
+ sendProblem(res, {
4754
+ type: PROBLEM_TYPES.BAD_REQUEST,
4755
+ title: 'Bad Request',
4756
+ status: 400,
4757
+ detail: 'toolName is required.',
4758
+ });
4759
+ return;
4760
+ }
4761
+
4762
+ const report = evaluateWorkflowSentinel(body.toolName, {
4763
+ command: body.command,
4764
+ path: body.filePath,
4765
+ changed_files: Array.isArray(body.changedFiles) ? body.changedFiles : [],
4766
+ repoPath: body.repoPath,
4767
+ baseBranch: body.baseBranch,
4768
+ }, {
4769
+ repoPath: body.repoPath,
4770
+ baseBranch: body.baseBranch,
4771
+ affectedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : undefined,
4772
+ requirePrForReleaseSensitive: body.requirePrForReleaseSensitive === true,
4773
+ requireVersionNotBehindBase: body.requireVersionNotBehindBase === true,
4774
+ governanceState: getScopeState(),
4775
+ feedbackDir: requestFeedbackDir,
4776
+ });
4777
+ const evaluation = recordDecisionEvaluation(report, {
4778
+ source: 'api',
4779
+ toolName: body.toolName,
4780
+ toolInput: {
4781
+ command: body.command,
4782
+ filePath: body.filePath,
4783
+ changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
4784
+ repoPath: body.repoPath,
4785
+ baseBranch: body.baseBranch,
4786
+ },
4787
+ changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
4788
+ }, {
4789
+ feedbackDir: requestFeedbackDir,
4790
+ });
4791
+ report.actionId = evaluation.actionId;
4792
+ if (report.decisionControl) report.decisionControl.actionId = evaluation.actionId;
4793
+ sendJson(res, 200, report);
4794
+ return;
4795
+ }
4796
+
4797
+ if (req.method === 'POST' && pathname === '/v1/decisions/outcome') {
4798
+ const body = await parseJsonBody(req);
4799
+ if (!body.actionId || !body.outcome) {
4800
+ sendProblem(res, {
4801
+ type: PROBLEM_TYPES.BAD_REQUEST,
4802
+ title: 'Bad Request',
4803
+ status: 400,
4804
+ detail: 'actionId and outcome are required.',
4805
+ });
4806
+ return;
4807
+ }
4808
+ const outcome = recordDecisionOutcome({
4809
+ actionId: body.actionId,
4810
+ outcome: body.outcome,
4811
+ actualDecision: body.actualDecision,
4812
+ actor: body.actor,
4813
+ notes: body.notes,
4814
+ metadata: body.metadata,
4815
+ latencyMs: body.latencyMs,
4816
+ source: 'api',
4817
+ }, {
4818
+ feedbackDir: requestFeedbackDir,
4819
+ });
4820
+ sendJson(res, 200, outcome);
4821
+ return;
4822
+ }
4823
+
4824
+ if (req.method === 'GET' && pathname === '/v1/decisions/metrics') {
4825
+ sendJson(res, 200, computeDecisionMetrics(requestFeedbackDir));
4826
+ return;
4827
+ }
4828
+
4572
4829
  // GET /v1/settings/status -- Resolved settings hierarchy with origin metadata
4573
4830
  if (req.method === 'GET' && pathname === '/v1/settings/status') {
4574
4831
  sendJson(res, 200, getSettingsStatus());
@@ -4599,9 +4856,9 @@ async function addContext(){
4599
4856
  return;
4600
4857
  }
4601
4858
 
4602
- // POST /webhook/stripe — Track Stripe subscription events (checkout, subscription changes)
4603
- // TODO: Add STRIPE_WEBHOOK_SECRET to .env and enable signature verification via
4604
- // verifyWebhookSignature() once the webhook endpoint is registered in the Stripe Dashboard.
4859
+ // POST /webhook/stripe — legacy Stripe event log bridge kept for backward compatibility.
4860
+ // When STRIPE_WEBHOOK_SECRET is configured, verify the same Stripe signature used by
4861
+ // the /v1/billing/webhook route before touching any payload.
4605
4862
  if (req.method === 'POST' && pathname === '/webhook/stripe') {
4606
4863
  try {
4607
4864
  const rawBody = await new Promise((resolve, reject) => {
@@ -4610,6 +4867,18 @@ async function addContext(){
4610
4867
  req.on('end', () => resolve(Buffer.concat(chunks)));
4611
4868
  req.on('error', reject);
4612
4869
  });
4870
+
4871
+ const sig = req.headers['stripe-signature'] || '';
4872
+ if (!verifyWebhookSignature(rawBody, sig)) {
4873
+ sendProblem(res, {
4874
+ type: PROBLEM_TYPES.WEBHOOK_INVALID,
4875
+ title: 'Invalid webhook signature',
4876
+ status: 400,
4877
+ detail: 'The webhook signature could not be verified.',
4878
+ });
4879
+ return;
4880
+ }
4881
+
4613
4882
  let event;
4614
4883
  try {
4615
4884
  event = JSON.parse(rawBody.toString('utf-8'));