thumbgate 1.3.0 → 1.4.1

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 (156) hide show
  1. package/.claude-plugin/README.md +25 -0
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +242 -126
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/INSTALL.md +59 -4
  9. package/adapters/chatgpt/openapi.yaml +168 -0
  10. package/adapters/claude/.mcp.json +2 -2
  11. package/adapters/codex/config.toml +2 -2
  12. package/adapters/mcp/server-stdio.js +84 -1
  13. package/adapters/opencode/opencode.json +1 -1
  14. package/bin/cli.js +204 -13
  15. package/bin/postinstall.js +8 -2
  16. package/config/budget.json +18 -0
  17. package/config/gates/code-edit.json +61 -0
  18. package/config/gates/db-write.json +61 -0
  19. package/config/gates/default.json +154 -3
  20. package/config/gates/deploy.json +61 -0
  21. package/config/github-about.json +2 -1
  22. package/config/merge-quality-checks.json +23 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -11
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/opencode-profile/INSTALL.md +1 -1
  34. package/public/blog.html +73 -0
  35. package/public/compare/mem0.html +189 -0
  36. package/public/compare/speclock.html +180 -0
  37. package/public/compare.html +10 -2
  38. package/public/guide.html +2 -2
  39. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/codex-cli-guardrails.html +158 -0
  41. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  42. package/public/guides/pre-action-gates.html +162 -0
  43. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  44. package/public/index.html +172 -65
  45. package/public/lessons.html +33 -24
  46. package/public/llm-context.md +140 -0
  47. package/public/pro.html +24 -22
  48. package/scripts/access-anomaly-detector.js +1 -1
  49. package/scripts/adk-consolidator.js +1 -5
  50. package/scripts/agent-security-hardening.js +4 -6
  51. package/scripts/agentic-data-pipeline.js +1 -3
  52. package/scripts/async-job-runner.js +1 -5
  53. package/scripts/audit-trail.js +1 -5
  54. package/scripts/auto-promote-gates.js +5 -3
  55. package/scripts/background-agent-governance.js +2 -10
  56. package/scripts/billing-setup.js +109 -0
  57. package/scripts/billing.js +2 -16
  58. package/scripts/budget-enforcer.js +173 -0
  59. package/scripts/build-claude-mcpb.js +71 -5
  60. package/scripts/build-codex-plugin.js +152 -0
  61. package/scripts/check-congruence.js +132 -14
  62. package/scripts/commercial-offer.js +5 -7
  63. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  64. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  65. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  66. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  67. package/scripts/context-engine.js +21 -6
  68. package/scripts/contextfs.js +1 -21
  69. package/scripts/dashboard.js +20 -0
  70. package/scripts/decision-journal.js +341 -0
  71. package/scripts/delegation-runtime.js +1 -5
  72. package/scripts/distribution-surfaces.js +54 -0
  73. package/scripts/document-intake.js +927 -0
  74. package/scripts/ephemeral-agent-store.js +1 -8
  75. package/scripts/evolution-state.js +1 -5
  76. package/scripts/experiment-tracker.js +1 -5
  77. package/scripts/export-databricks-bundle.js +1 -5
  78. package/scripts/export-hf-dataset.js +1 -5
  79. package/scripts/export-training.js +1 -5
  80. package/scripts/feedback-attribution.js +1 -16
  81. package/scripts/feedback-history-distiller.js +1 -16
  82. package/scripts/feedback-loop.js +1 -5
  83. package/scripts/feedback-root-consolidator.js +2 -21
  84. package/scripts/feedback-session.js +49 -0
  85. package/scripts/feedback-to-rules.js +215 -36
  86. package/scripts/filesystem-search.js +1 -9
  87. package/scripts/fs-utils.js +104 -0
  88. package/scripts/gates-engine.js +200 -11
  89. package/scripts/github-about.js +32 -8
  90. package/scripts/gtm-revenue-loop.js +1 -5
  91. package/scripts/harness-selector.js +148 -0
  92. package/scripts/hosted-config.js +2 -0
  93. package/scripts/hosted-job-launcher.js +1 -5
  94. package/scripts/hybrid-feedback-context.js +33 -49
  95. package/scripts/intervention-policy.js +58 -1
  96. package/scripts/lesson-db.js +3 -18
  97. package/scripts/lesson-inference.js +194 -16
  98. package/scripts/lesson-retrieval.js +60 -24
  99. package/scripts/llm-client.js +59 -0
  100. package/scripts/managed-lesson-agent.js +183 -0
  101. package/scripts/marketing-experiment.js +8 -22
  102. package/scripts/meta-agent-loop.js +624 -0
  103. package/scripts/metered-billing.js +1 -1
  104. package/scripts/money-watcher.js +1 -4
  105. package/scripts/obsidian-export.js +1 -5
  106. package/scripts/operational-integrity.js +15 -3
  107. package/scripts/operational-summary.js +41 -5
  108. package/scripts/org-dashboard.js +6 -1
  109. package/scripts/per-step-scoring.js +2 -4
  110. package/scripts/pr-manager.js +201 -19
  111. package/scripts/pro-features.js +3 -2
  112. package/scripts/prompt-dlp.js +3 -3
  113. package/scripts/prove-adapters.js +1 -5
  114. package/scripts/prove-attribution.js +1 -5
  115. package/scripts/prove-automation.js +1 -3
  116. package/scripts/prove-cloudflare-sandbox.js +1 -3
  117. package/scripts/prove-data-pipeline.js +1 -3
  118. package/scripts/prove-intelligence.js +1 -3
  119. package/scripts/prove-lancedb.js +1 -5
  120. package/scripts/prove-local-intelligence.js +1 -3
  121. package/scripts/prove-packaged-runtime.js +75 -9
  122. package/scripts/prove-predictive-insights.js +1 -3
  123. package/scripts/prove-training-export.js +1 -3
  124. package/scripts/prove-workflow-contract.js +1 -5
  125. package/scripts/ralph-loop.js +376 -0
  126. package/scripts/ralph-mode-ci.js +331 -0
  127. package/scripts/rate-limiter.js +3 -1
  128. package/scripts/reddit-dm-outreach.js +14 -4
  129. package/scripts/rotate-stripe-webhook-secret.js +314 -0
  130. package/scripts/schedule-manager.js +3 -5
  131. package/scripts/security-scanner.js +448 -0
  132. package/scripts/self-distill-agent.js +579 -0
  133. package/scripts/semantic-dedup.js +115 -0
  134. package/scripts/skill-exporter.js +1 -3
  135. package/scripts/skill-generator.js +1 -5
  136. package/scripts/social-analytics/engagement-audit.js +1 -18
  137. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  138. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  139. package/scripts/social-analytics/publishers/zernio.js +51 -0
  140. package/scripts/social-pipeline.js +1 -3
  141. package/scripts/social-post-hourly.js +47 -4
  142. package/scripts/statusline-links.js +6 -5
  143. package/scripts/statusline.sh +29 -153
  144. package/scripts/sync-branch-protection.js +340 -0
  145. package/scripts/tessl-export.js +1 -3
  146. package/scripts/thumbgate-search.js +32 -1
  147. package/scripts/tool-kpi-tracker.js +1 -1
  148. package/scripts/tool-registry.js +106 -2
  149. package/scripts/vector-store.js +1 -5
  150. package/scripts/weekly-auto-post.js +1 -1
  151. package/scripts/workflow-sentinel.js +91 -0
  152. package/skills/thumbgate/SKILL.md +1 -1
  153. package/src/api/server.js +296 -7
  154. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  155. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  156. /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';
@@ -1005,6 +1020,7 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
1005
1020
  '__AUTOMATION_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/proof/automation/report.json',
1006
1021
  '__GTM_PLAN_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/GO_TO_MARKET_REVENUE_WEDGE_2026-03.md',
1007
1022
  '__GITHUB_URL__': 'https://github.com/IgorGanapolsky/ThumbGate',
1023
+ '__POSTHOG_API_KEY__': runtimeConfig.posthogApiKey || '',
1008
1024
  });
1009
1025
  }
1010
1026
 
@@ -1345,6 +1361,32 @@ function renderRobotsTxt(runtimeConfig) {
1345
1361
  return [
1346
1362
  'User-agent: *',
1347
1363
  'Allow: /',
1364
+ '',
1365
+ '# AI crawler access — allow all major LLM crawlers',
1366
+ 'User-agent: GPTBot',
1367
+ 'Allow: /',
1368
+ '',
1369
+ 'User-agent: ClaudeBot',
1370
+ 'Allow: /',
1371
+ '',
1372
+ 'User-agent: PerplexityBot',
1373
+ 'Allow: /',
1374
+ '',
1375
+ 'User-agent: Googlebot',
1376
+ 'Allow: /',
1377
+ '',
1378
+ 'User-agent: Bingbot',
1379
+ 'Allow: /',
1380
+ '',
1381
+ 'User-agent: anthropic-ai',
1382
+ 'Allow: /',
1383
+ '',
1384
+ 'User-agent: Google-Extended',
1385
+ 'Allow: /',
1386
+ '',
1387
+ '# LLM context document — clean declarative content for AI retrieval',
1388
+ `# ${runtimeConfig.appOrigin}/llm-context.md`,
1389
+ '',
1348
1390
  `Sitemap: ${runtimeConfig.appOrigin}/sitemap.xml`,
1349
1391
  ].join('\n');
1350
1392
  }
@@ -1353,6 +1395,7 @@ function renderSitemapXml(runtimeConfig) {
1353
1395
  const entries = [
1354
1396
  { path: '/', changefreq: 'weekly', priority: '1.0' },
1355
1397
  { path: '/pro', changefreq: 'weekly', priority: '0.9' },
1398
+ { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
1356
1399
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
1357
1400
  ];
1358
1401
  return [
@@ -2111,6 +2154,11 @@ function getExpectedApiKey() {
2111
2154
  return configured;
2112
2155
  }
2113
2156
 
2157
+ function getExpectedOperatorKey() {
2158
+ const key = String(process.env.THUMBGATE_OPERATOR_KEY || '').trim();
2159
+ return key || null;
2160
+ }
2161
+
2114
2162
  function isAuthorized(req, expected) {
2115
2163
  if (!expected) return true;
2116
2164
  const token = extractApiKey(req);
@@ -2162,6 +2210,19 @@ function isStaticAdminAuthorized(req, expected) {
2162
2210
  return extractApiKey(req) === expected;
2163
2211
  }
2164
2212
 
2213
+ /**
2214
+ * Billing summary guard: accepts either the static admin key OR the operator key.
2215
+ * The operator key (THUMBGATE_OPERATOR_KEY) allows read-only billing data access
2216
+ * without exposing the full admin key to CLI clients.
2217
+ */
2218
+ function isBillingSummaryAuthorized(req, expectedAdminKey, expectedOperatorKey) {
2219
+ if (!expectedAdminKey && !expectedOperatorKey) return true;
2220
+ const token = extractApiKey(req);
2221
+ if (expectedAdminKey && token === expectedAdminKey) return true;
2222
+ if (expectedOperatorKey && token === expectedOperatorKey) return true;
2223
+ return false;
2224
+ }
2225
+
2165
2226
  function extractTags(input) {
2166
2227
  if (Array.isArray(input)) return input;
2167
2228
  if (typeof input === 'string') {
@@ -2240,8 +2301,43 @@ function normalizeJobIdFromPath(pathname, suffix = '') {
2240
2301
  return match ? decodeURIComponent(match[1]) : null;
2241
2302
  }
2242
2303
 
2304
+ function normalizeDocumentIdFromPath(pathname) {
2305
+ const match = pathname.match(/^\/v1\/documents\/([^/]+)$/);
2306
+ return match ? decodeURIComponent(match[1]) : null;
2307
+ }
2308
+
2309
+ function isWithinDir(targetPath, baseDir) {
2310
+ if (!baseDir) return false;
2311
+ const resolvedBase = path.resolve(baseDir);
2312
+ const resolvedTarget = path.resolve(targetPath);
2313
+ const relative = path.relative(resolvedBase, resolvedTarget);
2314
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
2315
+ }
2316
+
2317
+ function resolveDocumentImportFilePath(inputPath, options = {}) {
2318
+ const normalized = normalizeNullableText(inputPath);
2319
+ if (!normalized) return null;
2320
+
2321
+ const { req, parsed, safeDataDir } = options;
2322
+ if (!isLoopbackHost(getRequestHostHeader(req))) {
2323
+ throw createHttpError(400, 'filePath import is only available on localhost requests; use content for hosted imports');
2324
+ }
2325
+
2326
+ const projectDir = resolveRequestProjectDir(req, parsed) || process.cwd();
2327
+ const resolved = path.resolve(projectDir, normalized);
2328
+ const allowed = isWithinDir(resolved, projectDir) || isWithinDir(resolved, safeDataDir);
2329
+ if (!allowed) {
2330
+ throw createHttpError(400, `Path must stay within ${projectDir} or ${safeDataDir}`);
2331
+ }
2332
+ if (!fs.existsSync(resolved)) {
2333
+ throw createHttpError(400, `Path does not exist: ${resolved}`);
2334
+ }
2335
+ return resolved;
2336
+ }
2337
+
2243
2338
  function createApiServer() {
2244
2339
  const expectedApiKey = getExpectedApiKey();
2340
+ const expectedOperatorKey = getExpectedOperatorKey();
2245
2341
 
2246
2342
  return http.createServer(async (req, res) => {
2247
2343
  const parsed = new URL(req.url, 'http://localhost');
@@ -2516,6 +2612,17 @@ function createApiServer() {
2516
2612
  return;
2517
2613
  }
2518
2614
 
2615
+ if (isGetLikeRequest && pathname === '/.well-known/llms.txt') {
2616
+ const llmsTxtPath = path.join(__dirname, '..', '..', '.well-known', 'llms.txt');
2617
+ try {
2618
+ const content = fs.readFileSync(llmsTxtPath, 'utf8');
2619
+ sendText(res, 200, content, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=86400' }, { headOnly: isHeadRequest });
2620
+ } catch {
2621
+ sendJson(res, 404, { error: 'llms.txt not found' });
2622
+ }
2623
+ return;
2624
+ }
2625
+
2519
2626
  if (isGetLikeRequest && pathname === '/sitemap.xml') {
2520
2627
  sendText(res, 200, renderSitemapXml(hostedConfig), {
2521
2628
  'Content-Type': 'application/xml; charset=utf-8',
@@ -2525,6 +2632,22 @@ function createApiServer() {
2525
2632
  return;
2526
2633
  }
2527
2634
 
2635
+ if (isGetLikeRequest && pathname === '/llm-context.md') {
2636
+ const llmContextPath = path.resolve(__dirname, '../../public/llm-context.md');
2637
+ try {
2638
+ const content = fs.readFileSync(llmContextPath, 'utf8');
2639
+ sendText(res, 200, content, {
2640
+ 'Content-Type': 'text/markdown; charset=utf-8',
2641
+ 'X-Robots-Tag': 'all',
2642
+ }, {
2643
+ headOnly: isHeadRequest,
2644
+ });
2645
+ } catch (_err) {
2646
+ sendJson(res, 404, { error: 'Not found' });
2647
+ }
2648
+ return;
2649
+ }
2650
+
2528
2651
  // Quick feedback capture via GET — for statusline clickable links
2529
2652
  if (isGetLikeRequest && pathname === '/feedback/quick') {
2530
2653
  const signal = parsed.searchParams.get('signal');
@@ -2852,6 +2975,28 @@ async function addContext(){
2852
2975
  return;
2853
2976
  }
2854
2977
 
2978
+ if (isGetLikeRequest && pathname.startsWith('/guides/')) {
2979
+ try {
2980
+ const slug = pathname.replace('/guides/', '').replace(/[^a-z0-9-]/g, '');
2981
+ const guidePath = path.join(GUIDES_DIR, `${slug}.html`);
2982
+ if (!guidePath.startsWith(GUIDES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
2983
+ const html = fs.readFileSync(guidePath, 'utf-8');
2984
+ sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
2985
+ } catch { sendJson(res, 404, { error: 'Guide not found' }); }
2986
+ return;
2987
+ }
2988
+
2989
+ if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
2990
+ try {
2991
+ const slug = pathname.replace('/compare/', '').replace(/[^a-z0-9-]/g, '');
2992
+ const comparePath = path.join(COMPARE_DIR, `${slug}.html`);
2993
+ if (!comparePath.startsWith(COMPARE_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
2994
+ const html = fs.readFileSync(comparePath, 'utf-8');
2995
+ sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
2996
+ } catch { sendJson(res, 404, { error: 'Comparison not found' }); }
2997
+ return;
2998
+ }
2999
+
2855
3000
  if (isGetLikeRequest && pathname === '/') {
2856
3001
  if (wantsJson(req, parsed)) {
2857
3002
  sendJson(res, 200, {
@@ -2859,7 +3004,7 @@ async function addContext(){
2859
3004
  version: pkg.version,
2860
3005
  status: 'ok',
2861
3006
  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'],
3007
+ 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
3008
  }, {}, {
2864
3009
  headOnly: isHeadRequest,
2865
3010
  });
@@ -4054,6 +4199,34 @@ async function addContext(){
4054
4199
  return;
4055
4200
  }
4056
4201
 
4202
+ if (req.method === 'GET' && pathname === '/v1/documents') {
4203
+ const limit = Number(parsed.searchParams.get('limit') || 20);
4204
+ const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
4205
+ const tag = parsed.searchParams.get('tag') || '';
4206
+ const results = listImportedDocuments({
4207
+ feedbackDir: requestFeedbackDir,
4208
+ limit: Number.isFinite(limit) ? limit : 20,
4209
+ query,
4210
+ tag,
4211
+ });
4212
+ sendJson(res, 200, results);
4213
+ return;
4214
+ }
4215
+
4216
+ {
4217
+ const documentId = normalizeDocumentIdFromPath(pathname);
4218
+ if (req.method === 'GET' && documentId) {
4219
+ const document = readImportedDocument(documentId, {
4220
+ feedbackDir: requestFeedbackDir,
4221
+ });
4222
+ if (!document) {
4223
+ throw createHttpError(404, `Imported document not found: ${documentId}`);
4224
+ }
4225
+ sendJson(res, 200, { document });
4226
+ return;
4227
+ }
4228
+ }
4229
+
4057
4230
  if (req.method === 'POST' && pathname === '/v1/feedback/capture') {
4058
4231
  const captureLimit = checkLimit('capture_feedback');
4059
4232
  if (!captureLimit.allowed) {
@@ -4127,6 +4300,31 @@ async function addContext(){
4127
4300
  return;
4128
4301
  }
4129
4302
 
4303
+ if (req.method === 'POST' && pathname === '/v1/documents/import') {
4304
+ const body = await parseJsonBody(req, 2 * 1024 * 1024);
4305
+ const document = importDocument({
4306
+ filePath: body.filePath
4307
+ ? resolveDocumentImportFilePath(body.filePath, {
4308
+ req,
4309
+ parsed,
4310
+ safeDataDir: requestSafeDataDir,
4311
+ })
4312
+ : null,
4313
+ content: normalizeNullableText(body.content),
4314
+ title: normalizeNullableText(body.title),
4315
+ sourceFormat: normalizeNullableText(body.sourceFormat),
4316
+ sourceUrl: normalizeNullableText(body.sourceUrl),
4317
+ tags: extractTags(body.tags),
4318
+ proposeGates: body.proposeGates !== false,
4319
+ feedbackDir: requestFeedbackDir,
4320
+ });
4321
+ sendJson(res, 201, {
4322
+ ok: true,
4323
+ document,
4324
+ });
4325
+ return;
4326
+ }
4327
+
4130
4328
  if (req.method === 'POST' && pathname === '/v1/dpo/export') {
4131
4329
  const body = await parseJsonBody(req);
4132
4330
  const paths = resolveDpoExportPaths(body, {
@@ -4362,14 +4560,14 @@ async function addContext(){
4362
4560
  return;
4363
4561
  }
4364
4562
 
4365
- // GET /v1/billing/summary — admin-only operational billing summary
4563
+ // GET /v1/billing/summary — operator billing summary (admin key or operator key)
4366
4564
  if (req.method === 'GET' && pathname === '/v1/billing/summary') {
4367
- if (!isStaticAdminAuthorized(req, expectedApiKey)) {
4565
+ if (!isBillingSummaryAuthorized(req, expectedApiKey, expectedOperatorKey)) {
4368
4566
  sendProblem(res, {
4369
4567
  type: PROBLEM_TYPES.FORBIDDEN,
4370
4568
  title: 'Forbidden',
4371
4569
  status: 403,
4372
- detail: 'Admin API key required for this endpoint.',
4570
+ detail: 'Admin or operator API key required for this endpoint.',
4373
4571
  });
4374
4572
  return;
4375
4573
  }
@@ -4569,6 +4767,85 @@ async function addContext(){
4569
4767
  return;
4570
4768
  }
4571
4769
 
4770
+ if (req.method === 'POST' && pathname === '/v1/decisions/evaluate') {
4771
+ const body = await parseJsonBody(req);
4772
+ if (!body.toolName) {
4773
+ sendProblem(res, {
4774
+ type: PROBLEM_TYPES.BAD_REQUEST,
4775
+ title: 'Bad Request',
4776
+ status: 400,
4777
+ detail: 'toolName is required.',
4778
+ });
4779
+ return;
4780
+ }
4781
+
4782
+ const report = evaluateWorkflowSentinel(body.toolName, {
4783
+ command: body.command,
4784
+ path: body.filePath,
4785
+ changed_files: Array.isArray(body.changedFiles) ? body.changedFiles : [],
4786
+ repoPath: body.repoPath,
4787
+ baseBranch: body.baseBranch,
4788
+ }, {
4789
+ repoPath: body.repoPath,
4790
+ baseBranch: body.baseBranch,
4791
+ affectedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : undefined,
4792
+ requirePrForReleaseSensitive: body.requirePrForReleaseSensitive === true,
4793
+ requireVersionNotBehindBase: body.requireVersionNotBehindBase === true,
4794
+ governanceState: getScopeState(),
4795
+ feedbackDir: requestFeedbackDir,
4796
+ });
4797
+ const evaluation = recordDecisionEvaluation(report, {
4798
+ source: 'api',
4799
+ toolName: body.toolName,
4800
+ toolInput: {
4801
+ command: body.command,
4802
+ filePath: body.filePath,
4803
+ changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
4804
+ repoPath: body.repoPath,
4805
+ baseBranch: body.baseBranch,
4806
+ },
4807
+ changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
4808
+ }, {
4809
+ feedbackDir: requestFeedbackDir,
4810
+ });
4811
+ report.actionId = evaluation.actionId;
4812
+ if (report.decisionControl) report.decisionControl.actionId = evaluation.actionId;
4813
+ sendJson(res, 200, report);
4814
+ return;
4815
+ }
4816
+
4817
+ if (req.method === 'POST' && pathname === '/v1/decisions/outcome') {
4818
+ const body = await parseJsonBody(req);
4819
+ if (!body.actionId || !body.outcome) {
4820
+ sendProblem(res, {
4821
+ type: PROBLEM_TYPES.BAD_REQUEST,
4822
+ title: 'Bad Request',
4823
+ status: 400,
4824
+ detail: 'actionId and outcome are required.',
4825
+ });
4826
+ return;
4827
+ }
4828
+ const outcome = recordDecisionOutcome({
4829
+ actionId: body.actionId,
4830
+ outcome: body.outcome,
4831
+ actualDecision: body.actualDecision,
4832
+ actor: body.actor,
4833
+ notes: body.notes,
4834
+ metadata: body.metadata,
4835
+ latencyMs: body.latencyMs,
4836
+ source: 'api',
4837
+ }, {
4838
+ feedbackDir: requestFeedbackDir,
4839
+ });
4840
+ sendJson(res, 200, outcome);
4841
+ return;
4842
+ }
4843
+
4844
+ if (req.method === 'GET' && pathname === '/v1/decisions/metrics') {
4845
+ sendJson(res, 200, computeDecisionMetrics(requestFeedbackDir));
4846
+ return;
4847
+ }
4848
+
4572
4849
  // GET /v1/settings/status -- Resolved settings hierarchy with origin metadata
4573
4850
  if (req.method === 'GET' && pathname === '/v1/settings/status') {
4574
4851
  sendJson(res, 200, getSettingsStatus());
@@ -4599,9 +4876,9 @@ async function addContext(){
4599
4876
  return;
4600
4877
  }
4601
4878
 
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.
4879
+ // POST /webhook/stripe — legacy Stripe event log bridge kept for backward compatibility.
4880
+ // When STRIPE_WEBHOOK_SECRET is configured, verify the same Stripe signature used by
4881
+ // the /v1/billing/webhook route before touching any payload.
4605
4882
  if (req.method === 'POST' && pathname === '/webhook/stripe') {
4606
4883
  try {
4607
4884
  const rawBody = await new Promise((resolve, reject) => {
@@ -4610,6 +4887,18 @@ async function addContext(){
4610
4887
  req.on('end', () => resolve(Buffer.concat(chunks)));
4611
4888
  req.on('error', reject);
4612
4889
  });
4890
+
4891
+ const sig = req.headers['stripe-signature'] || '';
4892
+ if (!verifyWebhookSignature(rawBody, sig)) {
4893
+ sendProblem(res, {
4894
+ type: PROBLEM_TYPES.WEBHOOK_INVALID,
4895
+ title: 'Invalid webhook signature',
4896
+ status: 400,
4897
+ detail: 'The webhook signature could not be verified.',
4898
+ });
4899
+ return;
4900
+ }
4901
+
4613
4902
  let event;
4614
4903
  try {
4615
4904
  event = JSON.parse(rawBody.toString('utf-8'));