thumbgate 1.15.0 → 1.16.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 (129) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +59 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +210 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +157 -8
  12. package/config/evals/agent-safety-eval.json +338 -22
  13. package/config/gates/routine.json +43 -0
  14. package/config/github-about.json +3 -3
  15. package/config/model-candidates.json +131 -0
  16. package/openapi/openapi.yaml +118 -2
  17. package/package.json +57 -49
  18. package/public/blog.html +7 -7
  19. package/public/codex-plugin.html +6 -6
  20. package/public/compare.html +29 -23
  21. package/public/dashboard.html +82 -10
  22. package/public/guide.html +28 -28
  23. package/public/index.html +216 -98
  24. package/public/learn.html +50 -22
  25. package/public/lessons.html +1 -1
  26. package/public/numbers.html +17 -17
  27. package/public/pro.html +82 -18
  28. package/scripts/agent-audit-trace.js +55 -0
  29. package/scripts/agent-memory-lifecycle.js +96 -0
  30. package/scripts/agent-readiness-plan.js +118 -0
  31. package/scripts/agentic-data-pipeline.js +21 -1
  32. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  33. package/scripts/ai-org-governance.js +98 -0
  34. package/scripts/ai-search-distribution.js +43 -0
  35. package/scripts/artifact-agent-plan.js +81 -0
  36. package/scripts/billing.js +27 -8
  37. package/scripts/cli-schema.js +18 -2
  38. package/scripts/code-mode-mcp-plan.js +71 -0
  39. package/scripts/context-engine.js +1 -2
  40. package/scripts/context-manager.js +4 -1
  41. package/scripts/dashboard-render-spec.js +1 -1
  42. package/scripts/dashboard.js +275 -9
  43. package/scripts/decision-journal.js +13 -3
  44. package/scripts/document-workflow-governance.js +62 -0
  45. package/scripts/enterprise-agent-rollout.js +34 -0
  46. package/scripts/experience-replay-governance.js +69 -0
  47. package/scripts/export-hf-dataset.js +1 -1
  48. package/scripts/feedback-loop.js +92 -4
  49. package/scripts/feedback-to-rules.js +17 -23
  50. package/scripts/gates-engine.js +4 -6
  51. package/scripts/growth-campaigns.js +49 -0
  52. package/scripts/harness-selector.js +16 -4
  53. package/scripts/hybrid-supervisor-agent.js +64 -0
  54. package/scripts/inference-cache-policy.js +72 -0
  55. package/scripts/inference-economics.js +53 -0
  56. package/scripts/internal-agent-bootstrap.js +12 -2
  57. package/scripts/knowledge-layer-plan.js +108 -0
  58. package/scripts/lesson-inference.js +183 -44
  59. package/scripts/lesson-search.js +4 -1
  60. package/scripts/llm-client.js +157 -26
  61. package/scripts/mailer/resend-mailer.js +112 -1
  62. package/scripts/mcp-transport-strategy.js +66 -0
  63. package/scripts/memory-store-governance.js +60 -0
  64. package/scripts/meta-agent-loop.js +7 -13
  65. package/scripts/model-access-eligibility.js +38 -0
  66. package/scripts/model-migration-readiness.js +55 -0
  67. package/scripts/operational-integrity.js +96 -3
  68. package/scripts/otel-declarative-config.js +56 -0
  69. package/scripts/perplexity-client.js +1 -1
  70. package/scripts/post-training-governance.js +34 -0
  71. package/scripts/private-core-boundary.js +72 -0
  72. package/scripts/production-agent-readiness.js +40 -0
  73. package/scripts/prompt-eval.js +564 -32
  74. package/scripts/prompt-programs.js +93 -0
  75. package/scripts/provider-action-normalizer.js +585 -0
  76. package/scripts/scaling-law-claims.js +60 -0
  77. package/scripts/security-scanner.js +1 -1
  78. package/scripts/self-distill-agent.js +7 -32
  79. package/scripts/seo-gsd.js +232 -55
  80. package/scripts/skill-rag-router.js +53 -0
  81. package/scripts/spec-gate.js +1 -1
  82. package/scripts/student-consistent-training.js +73 -0
  83. package/scripts/synthetic-data-provenance.js +98 -0
  84. package/scripts/task-context-result.js +81 -0
  85. package/scripts/telemetry-analytics.js +149 -0
  86. package/scripts/thompson-sampling.js +2 -2
  87. package/scripts/token-savings.js +7 -6
  88. package/scripts/token-tco.js +46 -0
  89. package/scripts/tool-registry.js +63 -3
  90. package/scripts/verification-loop.js +10 -1
  91. package/scripts/verifier-scoring.js +71 -0
  92. package/scripts/workflow-sentinel.js +284 -28
  93. package/scripts/workspace-agent-routines.js +118 -0
  94. package/src/api/server.js +381 -120
  95. package/scripts/analytics-report.js +0 -328
  96. package/scripts/autonomous-workflow.js +0 -377
  97. package/scripts/billing-setup.js +0 -109
  98. package/scripts/creator-campaigns.js +0 -239
  99. package/scripts/cross-encoder-reranker.js +0 -235
  100. package/scripts/daemon-manager.js +0 -108
  101. package/scripts/decision-trace.js +0 -354
  102. package/scripts/delegation-runtime.js +0 -896
  103. package/scripts/dispatch-brief.js +0 -159
  104. package/scripts/distribution-surfaces.js +0 -110
  105. package/scripts/feedback-history-distiller.js +0 -382
  106. package/scripts/funnel-analytics.js +0 -35
  107. package/scripts/history-distiller.js +0 -200
  108. package/scripts/hosted-job-launcher.js +0 -256
  109. package/scripts/intent-router.js +0 -392
  110. package/scripts/lesson-reranker.js +0 -263
  111. package/scripts/lesson-retrieval.js +0 -148
  112. package/scripts/managed-lesson-agent.js +0 -183
  113. package/scripts/operational-dashboard.js +0 -103
  114. package/scripts/operational-summary.js +0 -129
  115. package/scripts/operator-artifacts.js +0 -608
  116. package/scripts/optimize-context.js +0 -17
  117. package/scripts/org-dashboard.js +0 -206
  118. package/scripts/partner-orchestration.js +0 -146
  119. package/scripts/predictive-insights.js +0 -356
  120. package/scripts/pulse.js +0 -80
  121. package/scripts/reflector-agent.js +0 -221
  122. package/scripts/sales-pipeline.js +0 -681
  123. package/scripts/session-episode-store.js +0 -329
  124. package/scripts/session-health-sensor.js +0 -242
  125. package/scripts/session-report.js +0 -120
  126. package/scripts/swarm-coordinator.js +0 -81
  127. package/scripts/tool-kpi-tracker.js +0 -12
  128. package/scripts/webhook-delivery.js +0 -62
  129. package/scripts/workflow-sprint-intake.js +0 -475
package/src/api/server.js CHANGED
@@ -3,7 +3,12 @@ const http = require('http');
3
3
  const https = require('https');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { EventEmitter } = require('node:events');
6
7
  const pkg = require('../../package.json');
8
+ const {
9
+ createUnavailableAsyncOperation,
10
+ loadOptionalModule,
11
+ } = require('../../scripts/private-core-boundary');
7
12
 
8
13
  const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
9
14
  const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
@@ -50,7 +55,9 @@ const {
50
55
  } = require('../../scripts/feedback-paths');
51
56
  const {
52
57
  readRecentConversationWindow,
53
- } = require('../../scripts/feedback-history-distiller');
58
+ } = loadOptionalModule(path.join(__dirname, '../../scripts/feedback-history-distiller'), () => ({
59
+ readRecentConversationWindow: () => [],
60
+ }));
54
61
  const {
55
62
  readJSONL,
56
63
  exportDpoFromMemories,
@@ -69,14 +76,6 @@ const {
69
76
  const {
70
77
  buildRubricEvaluation,
71
78
  } = require('../../scripts/rubric-engine');
72
- const {
73
- listIntents,
74
- planIntent,
75
- } = require('../../scripts/intent-router');
76
- const {
77
- startHandoff,
78
- completeHandoff,
79
- } = require('../../scripts/delegation-runtime');
80
79
  const {
81
80
  bootstrapInternalAgent,
82
81
  } = require('../../scripts/internal-agent-bootstrap');
@@ -118,14 +117,6 @@ const {
118
117
  buildHostedSuccessUrl,
119
118
  buildHostedCancelUrl,
120
119
  } = require('../../scripts/hosted-config');
121
- const {
122
- PRO_MONTHLY_PRICE_DOLLARS,
123
- PRO_ANNUAL_PRICE_DOLLARS,
124
- TEAM_MONTHLY_PRICE_DOLLARS,
125
- normalizePlanId,
126
- normalizeBillingCycle,
127
- normalizeSeatCount,
128
- } = require('../../scripts/commercial-offer');
129
120
  const {
130
121
  generateSkills,
131
122
  } = require('../../scripts/skill-generator');
@@ -146,6 +137,9 @@ const {
146
137
  const {
147
138
  evaluateWorkflowSentinel,
148
139
  } = require('../../scripts/workflow-sentinel');
140
+ const {
141
+ normalizeProviderAction,
142
+ } = require('../../scripts/provider-action-normalizer');
149
143
  const {
150
144
  recordDecisionEvaluation,
151
145
  recordDecisionOutcome,
@@ -163,14 +157,6 @@ const {
163
157
  const {
164
158
  getSettingsStatus,
165
159
  } = require('../../scripts/settings-hierarchy');
166
- const {
167
- searchLessons,
168
- } = require('../../scripts/lesson-search');
169
- const {
170
- updateRecordInJsonl,
171
- deleteRecordFromJsonl,
172
- readJSONLLocal,
173
- } = require('../../scripts/lesson-synthesis');
174
160
  const {
175
161
  searchThumbgate,
176
162
  } = require('../../scripts/thumbgate-search');
@@ -187,17 +173,6 @@ const {
187
173
  const {
188
174
  resolveAnalyticsWindow,
189
175
  } = require('../../scripts/analytics-window');
190
- const {
191
- launchDpoExportJob,
192
- launchHarnessJob,
193
- pauseQueuedJob,
194
- cancelQueuedJob,
195
- resumeHostedJob,
196
- } = require('../../scripts/hosted-job-launcher');
197
- const {
198
- appendWorkflowSprintLead,
199
- advanceWorkflowSprintLead,
200
- } = require('../../scripts/workflow-sprint-intake');
201
176
  const {
202
177
  importDocument,
203
178
  listImportedDocuments,
@@ -209,6 +184,7 @@ const {
209
184
  } = require('../../scripts/rate-limiter');
210
185
  const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
211
186
  const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
187
+ const resendMailer = require('../../scripts/mailer/resend-mailer');
212
188
  const {
213
189
  buildContextFootprintReport,
214
190
  } = require('../../scripts/context-footprint');
@@ -253,6 +229,80 @@ const STATIC_MIME_BY_EXT = Object.freeze({
253
229
  '.txt': 'text/plain; charset=utf-8',
254
230
  '.json': 'application/json; charset=utf-8',
255
231
  });
232
+ const PRIVATE_API_MODULES = Object.freeze({
233
+ intentRouter: path.resolve(__dirname, '../../scripts/intent-router.js'),
234
+ delegationRuntime: path.resolve(__dirname, '../../scripts/delegation-runtime.js'),
235
+ hostedJobLauncher: path.resolve(__dirname, '../../scripts/hosted-job-launcher.js'),
236
+ workflowSprintIntake: path.resolve(__dirname, '../../scripts/workflow-sprint-intake.js'),
237
+ lessonSearch: path.resolve(__dirname, '../../scripts/lesson-search.js'),
238
+ lessonSynthesis: path.resolve(__dirname, '../../scripts/lesson-synthesis.js'),
239
+ semanticLayer: path.resolve(__dirname, '../../scripts/semantic-layer.js'),
240
+ commercialOffer: path.resolve(__dirname, '../../scripts/commercial-offer.js'),
241
+ });
242
+
243
+ function createPrivateCoreUnavailableError(feature) {
244
+ const error = new Error(`${feature} is only available in the ThumbGate private core or hosted runtime.`);
245
+ error.statusCode = 503;
246
+ error.code = 'PRIVATE_CORE_REQUIRED';
247
+ return error;
248
+ }
249
+
250
+ function loadPrivateApiModule(key) {
251
+ const modulePath = PRIVATE_API_MODULES[key];
252
+ if (!modulePath) {
253
+ throw new Error(`Unknown private API module: ${key}`);
254
+ }
255
+ try {
256
+ return require(modulePath);
257
+ } catch (error) {
258
+ const message = String(error && error.message || '');
259
+ if ((error && (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND'))
260
+ && (message.includes(modulePath) || message.includes(path.basename(modulePath)))) {
261
+ return null;
262
+ }
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ function requirePrivateApiModule(key, feature) {
268
+ const module = loadPrivateApiModule(key);
269
+ if (!module) {
270
+ throw createPrivateCoreUnavailableError(feature);
271
+ }
272
+ return module;
273
+ }
274
+
275
+ function getCommercialOfferModule() {
276
+ return requirePrivateApiModule('commercialOffer', 'Commercial offer planning');
277
+ }
278
+
279
+ function normalizePlanId(value) {
280
+ return getCommercialOfferModule().normalizePlanId(value);
281
+ }
282
+
283
+ function normalizeBillingCycle(value) {
284
+ return getCommercialOfferModule().normalizeBillingCycle(value);
285
+ }
286
+
287
+ function normalizeSeatCount(value, fallback) {
288
+ return getCommercialOfferModule().normalizeSeatCount(value, fallback);
289
+ }
290
+
291
+ function getLessonSynthesisModule() {
292
+ return requirePrivateApiModule('lessonSynthesis', 'Lesson synthesis');
293
+ }
294
+
295
+ function readLessonJsonl(filePath, options) {
296
+ return getLessonSynthesisModule().readJSONLLocal(filePath, options);
297
+ }
298
+
299
+ function updateLessonJsonlRecord(filePath, recordId, record) {
300
+ return getLessonSynthesisModule().updateRecordInJsonl(filePath, recordId, record);
301
+ }
302
+
303
+ function deleteLessonJsonlRecord(filePath, recordId) {
304
+ return getLessonSynthesisModule().deleteRecordFromJsonl(filePath, recordId);
305
+ }
256
306
 
257
307
  function serveStaticFile(res, filePath, { headOnly = false, cacheSeconds = 86400 } = {}) {
258
308
  const ext = path.extname(filePath).toLowerCase();
@@ -506,11 +556,11 @@ function findRecordById(id, feedbackDir) {
506
556
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
507
557
  let memoryRecord = null;
508
558
  let feedbackEvent = null;
509
- const memoryRecords = readJSONLLocal(memoryLogPath, { maxLines: 0 });
559
+ const memoryRecords = readLessonJsonl(memoryLogPath, { maxLines: 0 });
510
560
  for (const rec of memoryRecords) {
511
561
  if (rec.id === id) { memoryRecord = rec; break; }
512
562
  }
513
- const feedbackRecords = readJSONLLocal(feedbackLogPath, { maxLines: 0 });
563
+ const feedbackRecords = readLessonJsonl(feedbackLogPath, { maxLines: 0 });
514
564
  for (const rec of feedbackRecords) {
515
565
  if (rec.id === id) { feedbackEvent = rec; break; }
516
566
  }
@@ -535,8 +585,8 @@ function updateLessonRecord(feedbackDir, lessonId, updater) {
535
585
  if (!updated) return null;
536
586
  const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
537
587
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
538
- const updatedMemory = updateRecordInJsonl(memoryLogPath, lessonId, updated);
539
- const updatedFeedback = updateRecordInJsonl(feedbackLogPath, lessonId, updated);
588
+ const updatedMemory = updateLessonJsonlRecord(memoryLogPath, lessonId, updated);
589
+ const updatedFeedback = updateLessonJsonlRecord(feedbackLogPath, lessonId, updated);
540
590
  if (!updatedMemory && !updatedFeedback) return null;
541
591
  return updated;
542
592
  }
@@ -1115,6 +1165,61 @@ function resolveBillingSummaryOptions(parsed) {
1115
1165
  });
1116
1166
  }
1117
1167
 
1168
+ function sendInvalidAnalyticsWindowProblem(res, title, err) {
1169
+ sendProblem(res, {
1170
+ type: PROBLEM_TYPES.INVALID_REQUEST,
1171
+ title,
1172
+ status: 400,
1173
+ detail: err && err.message ? err.message : 'Invalid analytics window request.',
1174
+ });
1175
+ }
1176
+
1177
+ function resolveBillingSummaryOptionsOrRespondProblem(res, parsed, invalidTitle) {
1178
+ try {
1179
+ return resolveBillingSummaryOptions(parsed);
1180
+ } catch (err) {
1181
+ sendInvalidAnalyticsWindowProblem(res, invalidTitle, err);
1182
+ return null;
1183
+ }
1184
+ }
1185
+
1186
+ async function buildLiveDashboardData(parsed, feedbackDir) {
1187
+ const summaryOptions = resolveBillingSummaryOptions(parsed);
1188
+ const billingSummary = await getBillingSummaryLive(summaryOptions);
1189
+ const data = generateDashboard(feedbackDir, {
1190
+ analyticsWindow: summaryOptions,
1191
+ billingSummary,
1192
+ billingSource: 'live',
1193
+ authContext: { tier: 'pro' },
1194
+ });
1195
+ return { summaryOptions, data };
1196
+ }
1197
+
1198
+ async function loadLiveDashboardDataOrRespondProblem(res, parsed, feedbackDir, invalidTitle) {
1199
+ try {
1200
+ return await buildLiveDashboardData(parsed, feedbackDir);
1201
+ } catch (err) {
1202
+ sendInvalidAnalyticsWindowProblem(res, invalidTitle, err);
1203
+ return null;
1204
+ }
1205
+ }
1206
+
1207
+ function buildLossAnalyticsResponse(data, summaryOptions) {
1208
+ return {
1209
+ window: data.analytics.window || summaryOptions,
1210
+ lossAnalysis: data.analytics.lossAnalysis || null,
1211
+ buyerLoss: data.analytics.buyerLoss || null,
1212
+ funnel: data.analytics.funnel || null,
1213
+ revenue: data.analytics.revenue || null,
1214
+ telemetry: {
1215
+ conversionFunnel: data.analytics.telemetry && data.analytics.telemetry.conversionFunnel,
1216
+ behavior: data.analytics.telemetry && data.analytics.telemetry.behavior,
1217
+ ctas: data.analytics.telemetry && data.analytics.telemetry.ctas,
1218
+ visitors: data.analytics.telemetry && data.analytics.telemetry.visitors,
1219
+ },
1220
+ };
1221
+ }
1222
+
1118
1223
  function createJourneyId(prefix) {
1119
1224
  return createTraceId(prefix).replace(/^trace_/, `${prefix}_`);
1120
1225
  }
@@ -1338,6 +1443,7 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
1338
1443
  }
1339
1444
 
1340
1445
  function resolveCheckoutOfferSummary(metadata = {}) {
1446
+ const commercialOffer = getCommercialOfferModule();
1341
1447
  const planId = normalizePlanId(metadata.planId);
1342
1448
  const billingCycle = normalizeBillingCycle(metadata.billingCycle);
1343
1449
 
@@ -1348,8 +1454,8 @@ function resolveCheckoutOfferSummary(metadata = {}) {
1348
1454
  billingCycle: 'monthly',
1349
1455
  seatCount,
1350
1456
  type: 'subscription',
1351
- price: TEAM_MONTHLY_PRICE_DOLLARS * seatCount,
1352
- priceLabel: `$${TEAM_MONTHLY_PRICE_DOLLARS}/seat/mo`,
1457
+ price: commercialOffer.TEAM_MONTHLY_PRICE_DOLLARS * seatCount,
1458
+ priceLabel: `$${commercialOffer.TEAM_MONTHLY_PRICE_DOLLARS}/seat/mo`,
1353
1459
  };
1354
1460
  }
1355
1461
 
@@ -1359,7 +1465,7 @@ function resolveCheckoutOfferSummary(metadata = {}) {
1359
1465
  billingCycle: 'annual',
1360
1466
  seatCount: 1,
1361
1467
  type: 'subscription',
1362
- price: PRO_ANNUAL_PRICE_DOLLARS,
1468
+ price: commercialOffer.PRO_ANNUAL_PRICE_DOLLARS,
1363
1469
  priceLabel: '$149/yr',
1364
1470
  };
1365
1471
  }
@@ -1369,7 +1475,7 @@ function resolveCheckoutOfferSummary(metadata = {}) {
1369
1475
  billingCycle: 'monthly',
1370
1476
  seatCount: 1,
1371
1477
  type: 'subscription',
1372
- price: PRO_MONTHLY_PRICE_DOLLARS,
1478
+ price: commercialOffer.PRO_MONTHLY_PRICE_DOLLARS,
1373
1479
  priceLabel: '$19/mo',
1374
1480
  };
1375
1481
  }
@@ -3133,6 +3239,18 @@ function createApiServer() {
3133
3239
  const expectedApiKey = getExpectedApiKey();
3134
3240
  const expectedOperatorKey = getExpectedOperatorKey();
3135
3241
 
3242
+ // Live-event bus. Feedback captures, prevention-rule regenerations, and
3243
+ // gate decisions push to this emitter; the /v1/events SSE endpoint streams
3244
+ // those events to connected dashboard clients so they render in real time
3245
+ // instead of waiting for the next manual refresh.
3246
+ //
3247
+ // See .changeset/dashboard-sse-live.md for the ROI rationale — this is a
3248
+ // direct application of the "persistent channel beats per-turn HTTP" pattern
3249
+ // to ThumbGate's dashboard surface (the primary UI for watching team
3250
+ // feedback flow).
3251
+ const eventBus = new EventEmitter();
3252
+ eventBus.setMaxListeners(200);
3253
+
3136
3254
  return http.createServer(async (req, res) => {
3137
3255
  const parsed = new URL(req.url, 'http://localhost');
3138
3256
  const pathname = parsed.pathname;
@@ -3366,6 +3484,19 @@ function createApiServer() {
3366
3484
  landingPath,
3367
3485
  attribution,
3368
3486
  }) + '\n');
3487
+ // Fire-and-forget welcome email. Never blocks the 200 response, never
3488
+ // throws — the mailer returns a structured result even on failure, so
3489
+ // the signup still succeeds if Resend is down or RESEND_API_KEY is unset.
3490
+ Promise.resolve()
3491
+ .then(() => resendMailer.sendNewsletterWelcomeEmail({ to: email }))
3492
+ .then((result) => {
3493
+ if (!result || result.sent !== true) {
3494
+ console.warn('[newsletter] welcome email not sent:', email, result && result.reason);
3495
+ }
3496
+ })
3497
+ .catch((err) => {
3498
+ console.warn('[newsletter] welcome email threw:', email, err && err.message);
3499
+ });
3369
3500
  }
3370
3501
  const journeyState = resolveJourneyState(req, parsed);
3371
3502
  appendBestEffortTelemetry(getFeedbackPaths().FEEDBACK_DIR, {
@@ -3688,8 +3819,8 @@ async function addContext(){
3688
3819
  const feedbackDir = requestSafeDataDir;
3689
3820
  const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
3690
3821
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
3691
- const deletedMemory = deleteRecordFromJsonl(memoryLogPath, lessonId);
3692
- const deletedFeedback = deleteRecordFromJsonl(feedbackLogPath, lessonId);
3822
+ const deletedMemory = deleteLessonJsonlRecord(memoryLogPath, lessonId);
3823
+ const deletedFeedback = deleteLessonJsonlRecord(feedbackLogPath, lessonId);
3693
3824
  if (!deletedMemory && !deletedFeedback) {
3694
3825
  sendJson(res, 404, { error: 'Record not found' });
3695
3826
  return;
@@ -4426,7 +4557,8 @@ async function addContext(){
4426
4557
  const body = isFormSubmission
4427
4558
  ? await parseFormBody(req, 24 * 1024)
4428
4559
  : await parseJsonBody(req, 24 * 1024);
4429
- const lead = appendWorkflowSprintLead({
4560
+ const workflowSprintIntake = requirePrivateApiModule('workflowSprintIntake', 'Workflow sprint intake');
4561
+ const lead = workflowSprintIntake.appendWorkflowSprintLead({
4430
4562
  ...body,
4431
4563
  traceId: body.traceId || traceId,
4432
4564
  acquisitionId: body.acquisitionId || journeyState.acquisitionId,
@@ -4837,15 +4969,71 @@ async function addContext(){
4837
4969
  return;
4838
4970
  }
4839
4971
 
4972
+ // Server-Sent Events stream of live feedback / rule-regen / gate events.
4973
+ // Dashboard clients subscribe once (with the same Bearer auth already
4974
+ // required for /v1/feedback/stats) and receive pushed events as they
4975
+ // happen — no polling, no per-event HTTP round trip. Replaces the
4976
+ // implicit "refresh the page" loop that used to be the only way to see
4977
+ // new feedback land.
4978
+ if (req.method === 'GET' && pathname === '/v1/events') {
4979
+ res.writeHead(200, {
4980
+ 'Content-Type': 'text/event-stream',
4981
+ 'Cache-Control': 'no-cache, no-transform',
4982
+ 'Connection': 'keep-alive',
4983
+ 'X-Accel-Buffering': 'no', // disable nginx buffering if any proxy is in front
4984
+ });
4985
+ // Initial handshake so the client knows the stream is live. Carries
4986
+ // the server version so clients can detect mid-session upgrades.
4987
+ res.write(`event: connected\ndata: ${JSON.stringify({ version: pkg.version, ts: Date.now() })}\n\n`);
4988
+
4989
+ // Both writes below can fail once the client has disconnected (the
4990
+ // socket is destroyed but our subscriber hasn't been removed yet).
4991
+ // We intentionally swallow the error and rely on the 'close'/'aborted'
4992
+ // handlers below to unsubscribe and clear the heartbeat — there is no
4993
+ // useful recovery action to take inline for a closed stream.
4994
+ const safeWrite = (chunk) => {
4995
+ try {
4996
+ res.write(chunk);
4997
+ } catch (writeErr) {
4998
+ // Connection closed between the emit and the flush; cleanup runs
4999
+ // via the 'close' listener so we don't need to act here. Keep the
5000
+ // exception binding so Sonar's "handle or don't catch" rule is
5001
+ // satisfied without adding log noise on every disconnect.
5002
+ void writeErr;
5003
+ }
5004
+ };
5005
+
5006
+ const onEvent = (payload) => {
5007
+ safeWrite(`event: ${payload.type}\ndata: ${JSON.stringify(payload)}\n\n`);
5008
+ };
5009
+ eventBus.on('broadcast', onEvent);
5010
+
5011
+ // Heartbeat every 25s keeps proxies (Railway, CDNs) from idle-closing
5012
+ // the connection. Clients ignore comment frames per the SSE spec.
5013
+ const heartbeat = setInterval(() => {
5014
+ safeWrite(':ping\n\n');
5015
+ }, 25_000);
5016
+
5017
+ const cleanup = () => {
5018
+ clearInterval(heartbeat);
5019
+ eventBus.removeListener('broadcast', onEvent);
5020
+ };
5021
+ req.on('close', cleanup);
5022
+ req.on('aborted', cleanup);
5023
+ res.on('close', cleanup);
5024
+ return;
5025
+ }
5026
+
4840
5027
  if (req.method === 'GET' && pathname === '/v1/intents/catalog') {
4841
5028
  const mcpProfile = parsed.searchParams.get('mcpProfile') || undefined;
4842
5029
  const bundleId = parsed.searchParams.get('bundleId') || undefined;
4843
5030
  const partnerProfile = parsed.searchParams.get('partnerProfile') || undefined;
4844
5031
  try {
4845
- const catalog = listIntents({ mcpProfile, bundleId, partnerProfile });
5032
+ const intentRouter = requirePrivateApiModule('intentRouter', 'Intent catalog');
5033
+ const catalog = intentRouter.listIntents({ mcpProfile, bundleId, partnerProfile });
4846
5034
  sendJson(res, 200, catalog);
4847
5035
  } catch (err) {
4848
- throw createHttpError(400, err.message || 'Invalid intent catalog request');
5036
+ throw createHttpError(err.statusCode || 400, err.message || 'Invalid intent catalog request');
4849
5037
  }
4850
5038
  return;
4851
5039
  }
@@ -4853,7 +5041,8 @@ async function addContext(){
4853
5041
  if (req.method === 'POST' && pathname === '/v1/intents/plan') {
4854
5042
  const body = await parseJsonBody(req);
4855
5043
  try {
4856
- const plan = planIntent({
5044
+ const intentRouter = requirePrivateApiModule('intentRouter', 'Intent planning');
5045
+ const plan = intentRouter.planIntent({
4857
5046
  intentId: body.intentId,
4858
5047
  context: body.context || '',
4859
5048
  mcpProfile: body.mcpProfile,
@@ -4865,7 +5054,7 @@ async function addContext(){
4865
5054
  });
4866
5055
  sendJson(res, 200, plan);
4867
5056
  } catch (err) {
4868
- throw createHttpError(400, err.message || 'Invalid intent plan request');
5057
+ throw createHttpError(err.statusCode || 400, err.message || 'Invalid intent plan request');
4869
5058
  }
4870
5059
  return;
4871
5060
  }
@@ -4873,7 +5062,9 @@ async function addContext(){
4873
5062
  if (req.method === 'POST' && pathname === '/v1/handoffs/start') {
4874
5063
  const body = await parseJsonBody(req);
4875
5064
  try {
4876
- const plan = planIntent({
5065
+ const intentRouter = requirePrivateApiModule('intentRouter', 'Handoff planning');
5066
+ const delegationRuntime = requirePrivateApiModule('delegationRuntime', 'Sequential handoffs');
5067
+ const plan = intentRouter.planIntent({
4877
5068
  intentId: body.intentId,
4878
5069
  context: body.context || '',
4879
5070
  mcpProfile: body.mcpProfile,
@@ -4883,7 +5074,7 @@ async function addContext(){
4883
5074
  approved: body.approved === true,
4884
5075
  repoPath: body.repoPath,
4885
5076
  });
4886
- const result = startHandoff({
5077
+ const result = delegationRuntime.startHandoff({
4887
5078
  plan,
4888
5079
  context: body.context || '',
4889
5080
  mcpProfile: body.mcpProfile || plan.mcpProfile,
@@ -4902,7 +5093,8 @@ async function addContext(){
4902
5093
  if (req.method === 'POST' && pathname === '/v1/handoffs/complete') {
4903
5094
  const body = await parseJsonBody(req);
4904
5095
  try {
4905
- const result = completeHandoff({
5096
+ const delegationRuntime = requirePrivateApiModule('delegationRuntime', 'Sequential handoffs');
5097
+ const result = delegationRuntime.completeHandoff({
4906
5098
  handoffId: body.handoffId,
4907
5099
  outcome: body.outcome,
4908
5100
  resultContext: body.resultContext || '',
@@ -4993,7 +5185,8 @@ async function addContext(){
4993
5185
  }
4994
5186
  const inputs = parseOptionalObject(body.inputs, 'inputs') || {};
4995
5187
  try {
4996
- const launched = launchHarnessJob(identifier, inputs, {
5188
+ const hostedJobLauncher = requirePrivateApiModule('hostedJobLauncher', 'Hosted harness jobs');
5189
+ const launched = hostedJobLauncher.launchHarnessJob(identifier, inputs, {
4997
5190
  jobId: normalizeNullableText(body.jobId) || undefined,
4998
5191
  skill: normalizeNullableText(body.skill) || undefined,
4999
5192
  partnerProfile: normalizeNullableText(body.partnerProfile) || undefined,
@@ -5052,8 +5245,9 @@ async function addContext(){
5052
5245
  throw createHttpError(409, `Job ${jobId} is already ${state.status}`);
5053
5246
  }
5054
5247
 
5248
+ const hostedJobLauncher = requirePrivateApiModule('hostedJobLauncher', 'Hosted job control');
5055
5249
  if (action === 'resume') {
5056
- const launched = resumeHostedJob(jobId);
5250
+ const launched = hostedJobLauncher.resumeHostedJob(jobId);
5057
5251
  sendJson(res, 202, {
5058
5252
  accepted: true,
5059
5253
  action,
@@ -5067,8 +5261,8 @@ async function addContext(){
5067
5261
 
5068
5262
  if (IDLE_JOB_STATUSES.has(state.status)) {
5069
5263
  const job = action === 'pause'
5070
- ? pauseQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {})
5071
- : cancelQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {});
5264
+ ? hostedJobLauncher.pauseQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {})
5265
+ : hostedJobLauncher.cancelQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {});
5072
5266
  sendJson(res, 202, {
5073
5267
  accepted: true,
5074
5268
  action,
@@ -5197,7 +5391,8 @@ async function addContext(){
5197
5391
  .split(',')
5198
5392
  .map((tag) => tag.trim())
5199
5393
  .filter(Boolean);
5200
- const results = searchLessons(query, {
5394
+ const lessonSearch = requirePrivateApiModule('lessonSearch', 'Lesson search');
5395
+ const results = lessonSearch.searchLessons(query, {
5201
5396
  limit: Number.isFinite(limit) ? limit : 10,
5202
5397
  category,
5203
5398
  tags,
@@ -5311,6 +5506,19 @@ async function addContext(){
5311
5506
  tags: extractTags(body.tags),
5312
5507
  skill: body.skill,
5313
5508
  });
5509
+ if (result?.accepted) {
5510
+ // Fan out to any connected dashboard clients so they re-render
5511
+ // without polling. Non-sensitive summary only (no chat history,
5512
+ // no evidence blobs).
5513
+ eventBus.emit('broadcast', {
5514
+ type: 'feedback',
5515
+ signal: body.signal,
5516
+ tags: Array.isArray(body.tags) ? body.tags.slice(0, 8) : [],
5517
+ feedbackId: result.feedbackId,
5518
+ promoted: Boolean(result.promoted),
5519
+ ts: Date.now(),
5520
+ });
5521
+ }
5314
5522
  const code = result.accepted ? 200 : 422;
5315
5523
  sendJson(res, code, result);
5316
5524
  return;
@@ -5323,6 +5531,13 @@ async function addContext(){
5323
5531
  ? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
5324
5532
  : undefined;
5325
5533
  const result = writePreventionRules(outputPath, Number.isFinite(minOccurrences) ? minOccurrences : 2);
5534
+ // Tell live dashboard clients the rules file just changed so they can
5535
+ // re-fetch the summary without waiting on a poll tick.
5536
+ eventBus.emit('broadcast', {
5537
+ type: 'rules-updated',
5538
+ path: result.path,
5539
+ ts: Date.now(),
5540
+ });
5326
5541
  sendJson(res, 200, {
5327
5542
  path: result.path,
5328
5543
  markdown: result.markdown,
@@ -5380,7 +5595,8 @@ async function addContext(){
5380
5595
 
5381
5596
  if (wantsAsync) {
5382
5597
  try {
5383
- const launched = launchDpoExportJob(paths, {
5598
+ const hostedJobLauncher = requirePrivateApiModule('hostedJobLauncher', 'Hosted DPO export jobs');
5599
+ const launched = hostedJobLauncher.launchDpoExportJob(paths, {
5384
5600
  jobId: normalizeNullableText(body.jobId) || undefined,
5385
5601
  });
5386
5602
  sendJson(res, 202, {
@@ -5423,8 +5639,8 @@ async function addContext(){
5423
5639
  const feedbackDir = requestSafeDataDir;
5424
5640
  const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
5425
5641
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
5426
- const memories = readJSONLLocal(memoryLogPath, { maxLines: 0 });
5427
- const feedbacks = readJSONLLocal(feedbackLogPath, { maxLines: 0 });
5642
+ const memories = readLessonJsonl(memoryLogPath, { maxLines: 0 });
5643
+ const feedbacks = readLessonJsonl(feedbackLogPath, { maxLines: 0 });
5428
5644
 
5429
5645
  // Merge into unified lesson records
5430
5646
  const lessonMap = new Map();
@@ -5514,7 +5730,7 @@ async function addContext(){
5514
5730
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
5515
5731
 
5516
5732
  // Load existing IDs for dedup
5517
- const existing = readJSONLLocal(feedbackLogPath, { maxLines: 0 });
5733
+ const existing = readLessonJsonl(feedbackLogPath, { maxLines: 0 });
5518
5734
  const existingIds = new Set(existing.map((r) => r.id).filter(Boolean));
5519
5735
  // Also dedup by title+signal content hash
5520
5736
  const existingHashes = new Set(existing.map((r) => {
@@ -5699,12 +5915,12 @@ async function addContext(){
5699
5915
 
5700
5916
  // GET /v1/semantic/describe — get canonical definition of a business entity
5701
5917
  if (req.method === 'GET' && pathname === '/v1/semantic/describe') {
5702
- const { describeSemanticSchema } = require('../../scripts/semantic-layer');
5918
+ const semanticLayer = requirePrivateApiModule('semanticLayer', 'Semantic schema');
5703
5919
  const type = parsed.query.type;
5704
5920
  if (!type) {
5705
5921
  throw createHttpError(400, 'type query parameter is required');
5706
5922
  }
5707
- const schema = describeSemanticSchema();
5923
+ const schema = semanticLayer.describeSemanticSchema();
5708
5924
  const entity = schema.entities[type] || schema.metrics[type];
5709
5925
  if (!entity) {
5710
5926
  sendProblem(res, {
@@ -5780,16 +5996,12 @@ async function addContext(){
5780
5996
  return;
5781
5997
  }
5782
5998
 
5783
- let summaryOptions;
5784
- try {
5785
- summaryOptions = resolveBillingSummaryOptions(parsed);
5786
- } catch (err) {
5787
- sendProblem(res, {
5788
- type: PROBLEM_TYPES.INVALID_REQUEST,
5789
- title: 'Invalid billing summary query',
5790
- status: 400,
5791
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
5792
- });
5999
+ const summaryOptions = resolveBillingSummaryOptionsOrRespondProblem(
6000
+ res,
6001
+ parsed,
6002
+ 'Invalid billing summary query',
6003
+ );
6004
+ if (!summaryOptions) {
5793
6005
  return;
5794
6006
  }
5795
6007
 
@@ -5813,7 +6025,8 @@ async function addContext(){
5813
6025
  const { FEEDBACK_DIR } = getFeedbackPaths();
5814
6026
  try {
5815
6027
  const body = await parseJsonBody(req, 24 * 1024);
5816
- const result = advanceWorkflowSprintLead(body, { feedbackDir: FEEDBACK_DIR });
6028
+ const workflowSprintIntake = requirePrivateApiModule('workflowSprintIntake', 'Workflow sprint intake');
6029
+ const result = workflowSprintIntake.advanceWorkflowSprintLead(body, { feedbackDir: FEEDBACK_DIR });
5817
6030
 
5818
6031
  appendBestEffortTelemetry(FEEDBACK_DIR, {
5819
6032
  eventType: 'workflow_sprint_lead_advanced',
@@ -5910,28 +6123,36 @@ async function addContext(){
5910
6123
  return;
5911
6124
  }
5912
6125
 
6126
+ // GET /v1/analytics/losses -- explain where buyer dollars are falling out of the funnel
6127
+ if (req.method === 'GET' && pathname === '/v1/analytics/losses') {
6128
+ const dashboardResult = await loadLiveDashboardDataOrRespondProblem(
6129
+ res,
6130
+ parsed,
6131
+ requestFeedbackDir,
6132
+ 'Invalid loss analytics query',
6133
+ );
6134
+ if (!dashboardResult) {
6135
+ return;
6136
+ }
6137
+ const { summaryOptions, data } = dashboardResult;
6138
+
6139
+ sendJson(res, 200, buildLossAnalyticsResponse(data, summaryOptions));
6140
+ return;
6141
+ }
6142
+
5913
6143
  // GET /v1/dashboard -- Full ThumbGate dashboard JSON
5914
6144
  if (req.method === 'GET' && pathname === '/v1/dashboard') {
5915
- let summaryOptions;
5916
- try {
5917
- summaryOptions = resolveBillingSummaryOptions(parsed);
5918
- } catch (err) {
5919
- sendProblem(res, {
5920
- type: PROBLEM_TYPES.INVALID_REQUEST,
5921
- title: 'Invalid dashboard query',
5922
- status: 400,
5923
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
5924
- });
6145
+ const dashboardResult = await loadLiveDashboardDataOrRespondProblem(
6146
+ res,
6147
+ parsed,
6148
+ requestFeedbackDir,
6149
+ 'Invalid dashboard query',
6150
+ );
6151
+ if (!dashboardResult) {
5925
6152
  return;
5926
6153
  }
6154
+ const { data } = dashboardResult;
5927
6155
 
5928
- const billingSummary = await getBillingSummaryLive(summaryOptions);
5929
- const data = generateDashboard(requestFeedbackDir, {
5930
- analyticsWindow: summaryOptions,
5931
- billingSummary,
5932
- billingSource: 'live',
5933
- authContext: { tier: 'pro' },
5934
- });
5935
6156
  sendJson(res, 200, data);
5936
6157
  return;
5937
6158
  }
@@ -5968,27 +6189,18 @@ async function addContext(){
5968
6189
 
5969
6190
  // GET /v1/dashboard/render-spec -- Constrained hosted dashboard JSON spec
5970
6191
  if (req.method === 'GET' && pathname === '/v1/dashboard/render-spec') {
5971
- let summaryOptions;
5972
- try {
5973
- summaryOptions = resolveBillingSummaryOptions(parsed);
5974
- } catch (err) {
5975
- sendProblem(res, {
5976
- type: PROBLEM_TYPES.INVALID_REQUEST,
5977
- title: 'Invalid render-spec query',
5978
- status: 400,
5979
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
5980
- });
6192
+ const dashboardResult = await loadLiveDashboardDataOrRespondProblem(
6193
+ res,
6194
+ parsed,
6195
+ requestFeedbackDir,
6196
+ 'Invalid render-spec query',
6197
+ );
6198
+ if (!dashboardResult) {
5981
6199
  return;
5982
6200
  }
6201
+ const { summaryOptions, data } = dashboardResult;
5983
6202
 
5984
6203
  try {
5985
- const billingSummary = await getBillingSummaryLive(summaryOptions);
5986
- const data = generateDashboard(requestFeedbackDir, {
5987
- analyticsWindow: summaryOptions,
5988
- billingSummary,
5989
- billingSource: 'live',
5990
- authContext: { tier: 'pro' },
5991
- });
5992
6204
  const renderSpec = buildDashboardRenderSpec(data, {
5993
6205
  view: parsed.searchParams.get('view') || undefined,
5994
6206
  now: summaryOptions.now,
@@ -6007,42 +6219,87 @@ async function addContext(){
6007
6219
 
6008
6220
  if (req.method === 'POST' && pathname === '/v1/decisions/evaluate') {
6009
6221
  const body = await parseJsonBody(req);
6010
- if (!body.toolName) {
6222
+ const normalizedRequestAction = normalizeProviderAction(body);
6223
+ const hasProviderNativeAction = Boolean(
6224
+ body.providerToolCall
6225
+ || body.toolCall
6226
+ || body.toolUse
6227
+ || body.content
6228
+ || body.mcp
6229
+ || body.mcpToolCall
6230
+ || body.method === 'tools/call'
6231
+ );
6232
+ if (!body.toolName && !hasProviderNativeAction) {
6011
6233
  sendProblem(res, {
6012
6234
  type: PROBLEM_TYPES.BAD_REQUEST,
6013
6235
  title: 'Bad Request',
6014
6236
  status: 400,
6015
- detail: 'toolName is required.',
6237
+ detail: 'toolName or provider tool call is required.',
6016
6238
  });
6017
6239
  return;
6018
6240
  }
6019
6241
 
6020
- const report = evaluateWorkflowSentinel(body.toolName, {
6242
+ const changedFiles = Array.isArray(body.changedFiles)
6243
+ ? body.changedFiles
6244
+ : normalizedRequestAction.affectedFiles;
6245
+ const scopeState = getScopeState();
6246
+ const toolInput = {
6021
6247
  command: body.command,
6022
6248
  path: body.filePath,
6023
- changed_files: Array.isArray(body.changedFiles) ? body.changedFiles : [],
6249
+ changed_files: changedFiles,
6024
6250
  repoPath: body.repoPath,
6025
6251
  baseBranch: body.baseBranch,
6026
- }, {
6252
+ providerToolCall: body.providerToolCall,
6253
+ toolCall: body.toolCall,
6254
+ toolUse: body.toolUse,
6255
+ content: body.content,
6256
+ input: body.input,
6257
+ arguments: body.arguments,
6258
+ method: body.method,
6259
+ params: body.params,
6260
+ mcp: body.mcp,
6261
+ mcpToolCall: body.mcpToolCall,
6262
+ budget: body.budget,
6263
+ usage: body.usage,
6264
+ };
6265
+
6266
+ const report = evaluateWorkflowSentinel(normalizedRequestAction.toolName || body.toolName, toolInput, {
6267
+ provider: body.provider,
6268
+ model: body.model,
6269
+ normalizedAction: normalizedRequestAction,
6270
+ usage: body.usage,
6271
+ tokenEstimate: body.tokenEstimate,
6272
+ costUsd: body.costUsd,
6273
+ budget: body.budget,
6027
6274
  repoPath: body.repoPath,
6028
6275
  baseBranch: body.baseBranch,
6029
- affectedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : undefined,
6276
+ affectedFiles: changedFiles.length > 0 ? changedFiles : undefined,
6030
6277
  requirePrForReleaseSensitive: body.requirePrForReleaseSensitive === true,
6031
6278
  requireVersionNotBehindBase: body.requireVersionNotBehindBase === true,
6032
- governanceState: getScopeState(),
6279
+ governanceState: {
6280
+ ...scopeState,
6281
+ branchGovernance: body.workflowDispatch && typeof body.workflowDispatch === 'object'
6282
+ ? {
6283
+ ...(scopeState.branchGovernance || {}),
6284
+ workflowDispatch: body.workflowDispatch,
6285
+ }
6286
+ : scopeState.branchGovernance,
6287
+ },
6033
6288
  feedbackDir: requestFeedbackDir,
6034
6289
  });
6035
6290
  const evaluation = recordDecisionEvaluation(report, {
6036
6291
  source: 'api',
6037
- toolName: body.toolName,
6292
+ toolName: report.toolName,
6038
6293
  toolInput: {
6039
6294
  command: body.command,
6040
6295
  filePath: body.filePath,
6041
- changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
6296
+ changedFiles,
6042
6297
  repoPath: body.repoPath,
6043
6298
  baseBranch: body.baseBranch,
6299
+ normalizedAction: report.normalizedAction,
6300
+ costControl: report.costControl,
6044
6301
  },
6045
- changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
6302
+ changedFiles,
6046
6303
  }, {
6047
6304
  feedbackDir: requestFeedbackDir,
6048
6305
  });
@@ -6252,9 +6509,13 @@ module.exports = {
6252
6509
  startServer,
6253
6510
  __test__: {
6254
6511
  buildCheckoutFallbackUrl,
6512
+ createPrivateCoreUnavailableError,
6255
6513
  buildPosthogProxyRequestOptions,
6256
6514
  getPosthogProxyPath,
6257
6515
  isAllowedPosthogProxyPath,
6516
+ PRIVATE_API_MODULES,
6517
+ loadPrivateApiModule,
6518
+ requirePrivateApiModule,
6258
6519
  renderSitemapXml,
6259
6520
  renderPackagedDashboardHtml,
6260
6521
  renderPackagedLessonsHtml,