thumbgate 1.14.1 → 1.16.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 (150) 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 +60 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +217 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +211 -8
  12. package/config/enforcement.json +59 -7
  13. package/config/evals/agent-safety-eval.json +338 -22
  14. package/config/gates/default.json +33 -0
  15. package/config/gates/routine.json +43 -0
  16. package/config/github-about.json +3 -3
  17. package/config/mcp-allowlists.json +4 -0
  18. package/config/merge-quality-checks.json +2 -1
  19. package/config/model-candidates.json +131 -0
  20. package/openapi/openapi.yaml +118 -2
  21. package/package.json +70 -51
  22. package/public/blog.html +7 -7
  23. package/public/codex-plugin.html +13 -7
  24. package/public/compare.html +29 -23
  25. package/public/dashboard.html +105 -12
  26. package/public/guide.html +28 -28
  27. package/public/index.html +233 -97
  28. package/public/learn.html +87 -20
  29. package/public/lessons.html +26 -2
  30. package/public/numbers.html +271 -0
  31. package/public/pro.html +89 -19
  32. package/scripts/agent-audit-trace.js +55 -0
  33. package/scripts/agent-memory-lifecycle.js +96 -0
  34. package/scripts/agent-readiness-plan.js +118 -0
  35. package/scripts/agentic-data-pipeline.js +21 -1
  36. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  37. package/scripts/ai-org-governance.js +98 -0
  38. package/scripts/ai-search-distribution.js +43 -0
  39. package/scripts/artifact-agent-plan.js +81 -0
  40. package/scripts/billing.js +27 -8
  41. package/scripts/cli-feedback.js +2 -1
  42. package/scripts/cli-schema.js +60 -5
  43. package/scripts/code-mode-mcp-plan.js +71 -0
  44. package/scripts/commercial-offer.js +1 -1
  45. package/scripts/context-engine.js +1 -2
  46. package/scripts/context-manager.js +4 -1
  47. package/scripts/contextfs.js +214 -32
  48. package/scripts/dashboard-render-spec.js +1 -1
  49. package/scripts/dashboard.js +275 -9
  50. package/scripts/decision-journal.js +13 -3
  51. package/scripts/document-workflow-governance.js +62 -0
  52. package/scripts/enterprise-agent-rollout.js +34 -0
  53. package/scripts/experience-replay-governance.js +69 -0
  54. package/scripts/export-hf-dataset.js +1 -1
  55. package/scripts/feedback-loop.js +141 -9
  56. package/scripts/feedback-to-rules.js +17 -23
  57. package/scripts/gates-engine.js +4 -6
  58. package/scripts/growth-campaigns.js +49 -0
  59. package/scripts/harness-selector.js +145 -1
  60. package/scripts/hybrid-supervisor-agent.js +64 -0
  61. package/scripts/inference-cache-policy.js +72 -0
  62. package/scripts/inference-economics.js +53 -0
  63. package/scripts/internal-agent-bootstrap.js +12 -2
  64. package/scripts/knowledge-layer-plan.js +108 -0
  65. package/scripts/lesson-canonical.js +181 -0
  66. package/scripts/lesson-db.js +71 -10
  67. package/scripts/lesson-inference.js +183 -44
  68. package/scripts/lesson-search.js +4 -1
  69. package/scripts/lesson-synthesis.js +23 -2
  70. package/scripts/llm-client.js +157 -26
  71. package/scripts/mailer/resend-mailer.js +112 -1
  72. package/scripts/mcp-transport-strategy.js +66 -0
  73. package/scripts/memory-store-governance.js +60 -0
  74. package/scripts/meta-agent-loop.js +7 -13
  75. package/scripts/model-access-eligibility.js +38 -0
  76. package/scripts/model-migration-readiness.js +55 -0
  77. package/scripts/native-messaging-audit.js +514 -0
  78. package/scripts/operational-integrity.js +96 -3
  79. package/scripts/otel-declarative-config.js +56 -0
  80. package/scripts/perplexity-client.js +1 -1
  81. package/scripts/post-training-governance.js +34 -0
  82. package/scripts/pr-manager.js +47 -7
  83. package/scripts/private-core-boundary.js +72 -0
  84. package/scripts/production-agent-readiness.js +40 -0
  85. package/scripts/profile-router.js +16 -1
  86. package/scripts/prompt-eval.js +564 -32
  87. package/scripts/prompt-programs.js +93 -0
  88. package/scripts/provider-action-normalizer.js +585 -0
  89. package/scripts/rule-validator.js +285 -0
  90. package/scripts/scaling-law-claims.js +60 -0
  91. package/scripts/security-scanner.js +1 -1
  92. package/scripts/self-distill-agent.js +7 -32
  93. package/scripts/seo-gsd.js +400 -43
  94. package/scripts/skill-rag-router.js +53 -0
  95. package/scripts/spec-gate.js +1 -1
  96. package/scripts/student-consistent-training.js +73 -0
  97. package/scripts/synthetic-data-provenance.js +98 -0
  98. package/scripts/task-context-result.js +81 -0
  99. package/scripts/telemetry-analytics.js +149 -0
  100. package/scripts/thompson-sampling.js +2 -2
  101. package/scripts/token-savings.js +7 -6
  102. package/scripts/token-tco.js +46 -0
  103. package/scripts/tool-registry.js +75 -3
  104. package/scripts/verification-loop.js +10 -1
  105. package/scripts/verifier-scoring.js +71 -0
  106. package/scripts/workflow-sentinel.js +284 -28
  107. package/scripts/workspace-agent-routines.js +118 -0
  108. package/skills/thumbgate/SKILL.md +1 -1
  109. package/src/api/server.js +434 -120
  110. package/.claude-plugin/README.md +0 -170
  111. package/adapters/README.md +0 -12
  112. package/scripts/analytics-report.js +0 -328
  113. package/scripts/autonomous-workflow.js +0 -377
  114. package/scripts/billing-setup.js +0 -109
  115. package/scripts/creator-campaigns.js +0 -239
  116. package/scripts/cross-encoder-reranker.js +0 -235
  117. package/scripts/daemon-manager.js +0 -108
  118. package/scripts/decision-trace.js +0 -354
  119. package/scripts/delegation-runtime.js +0 -896
  120. package/scripts/dispatch-brief.js +0 -159
  121. package/scripts/distribution-surfaces.js +0 -110
  122. package/scripts/feedback-history-distiller.js +0 -382
  123. package/scripts/funnel-analytics.js +0 -35
  124. package/scripts/history-distiller.js +0 -200
  125. package/scripts/hosted-job-launcher.js +0 -256
  126. package/scripts/intent-router.js +0 -392
  127. package/scripts/lesson-reranker.js +0 -263
  128. package/scripts/lesson-retrieval.js +0 -148
  129. package/scripts/managed-lesson-agent.js +0 -183
  130. package/scripts/operational-dashboard.js +0 -103
  131. package/scripts/operational-summary.js +0 -129
  132. package/scripts/operator-artifacts.js +0 -608
  133. package/scripts/optimize-context.js +0 -17
  134. package/scripts/org-dashboard.js +0 -206
  135. package/scripts/partner-orchestration.js +0 -146
  136. package/scripts/predictive-insights.js +0 -356
  137. package/scripts/pulse.js +0 -80
  138. package/scripts/reflector-agent.js +0 -221
  139. package/scripts/sales-pipeline.js +0 -681
  140. package/scripts/session-episode-store.js +0 -329
  141. package/scripts/session-health-sensor.js +0 -242
  142. package/scripts/session-report.js +0 -120
  143. package/scripts/swarm-coordinator.js +0 -81
  144. package/scripts/tool-kpi-tracker.js +0 -12
  145. package/scripts/webhook-delivery.js +0 -62
  146. package/scripts/workflow-sprint-intake.js +0 -475
  147. package/skills/agent-memory/SKILL.md +0 -97
  148. package/skills/solve-architecture-autonomy/SKILL.md +0 -17
  149. package/skills/solve-architecture-autonomy/tool.js +0 -33
  150. package/skills/thumbgate-feedback/SKILL.md +0 -49
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');
@@ -97,6 +96,7 @@ const {
97
96
  samplePosteriors,
98
97
  } = require('../../scripts/thompson-sampling');
99
98
  const {
99
+ appendFunnelEvent,
100
100
  createCheckoutSession,
101
101
  getCheckoutSessionStatus,
102
102
  provisionApiKey,
@@ -117,14 +117,6 @@ const {
117
117
  buildHostedSuccessUrl,
118
118
  buildHostedCancelUrl,
119
119
  } = require('../../scripts/hosted-config');
120
- const {
121
- PRO_MONTHLY_PRICE_DOLLARS,
122
- PRO_ANNUAL_PRICE_DOLLARS,
123
- TEAM_MONTHLY_PRICE_DOLLARS,
124
- normalizePlanId,
125
- normalizeBillingCycle,
126
- normalizeSeatCount,
127
- } = require('../../scripts/commercial-offer');
128
120
  const {
129
121
  generateSkills,
130
122
  } = require('../../scripts/skill-generator');
@@ -145,6 +137,9 @@ const {
145
137
  const {
146
138
  evaluateWorkflowSentinel,
147
139
  } = require('../../scripts/workflow-sentinel');
140
+ const {
141
+ normalizeProviderAction,
142
+ } = require('../../scripts/provider-action-normalizer');
148
143
  const {
149
144
  recordDecisionEvaluation,
150
145
  recordDecisionOutcome,
@@ -162,14 +157,6 @@ const {
162
157
  const {
163
158
  getSettingsStatus,
164
159
  } = require('../../scripts/settings-hierarchy');
165
- const {
166
- searchLessons,
167
- } = require('../../scripts/lesson-search');
168
- const {
169
- updateRecordInJsonl,
170
- deleteRecordFromJsonl,
171
- readJSONLLocal,
172
- } = require('../../scripts/lesson-synthesis');
173
160
  const {
174
161
  searchThumbgate,
175
162
  } = require('../../scripts/thumbgate-search');
@@ -186,17 +173,6 @@ const {
186
173
  const {
187
174
  resolveAnalyticsWindow,
188
175
  } = require('../../scripts/analytics-window');
189
- const {
190
- launchDpoExportJob,
191
- launchHarnessJob,
192
- pauseQueuedJob,
193
- cancelQueuedJob,
194
- resumeHostedJob,
195
- } = require('../../scripts/hosted-job-launcher');
196
- const {
197
- appendWorkflowSprintLead,
198
- advanceWorkflowSprintLead,
199
- } = require('../../scripts/workflow-sprint-intake');
200
176
  const {
201
177
  importDocument,
202
178
  listImportedDocuments,
@@ -208,6 +184,7 @@ const {
208
184
  } = require('../../scripts/rate-limiter');
209
185
  const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
210
186
  const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
187
+ const resendMailer = require('../../scripts/mailer/resend-mailer');
211
188
  const {
212
189
  buildContextFootprintReport,
213
190
  } = require('../../scripts/context-footprint');
@@ -225,6 +202,7 @@ const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
225
202
  const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugin.html');
226
203
  const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
227
204
  const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
205
+ const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
228
206
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
229
207
  const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
230
208
  const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
@@ -251,6 +229,80 @@ const STATIC_MIME_BY_EXT = Object.freeze({
251
229
  '.txt': 'text/plain; charset=utf-8',
252
230
  '.json': 'application/json; charset=utf-8',
253
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
+ }
254
306
 
255
307
  function serveStaticFile(res, filePath, { headOnly = false, cacheSeconds = 86400 } = {}) {
256
308
  const ext = path.extname(filePath).toLowerCase();
@@ -504,11 +556,11 @@ function findRecordById(id, feedbackDir) {
504
556
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
505
557
  let memoryRecord = null;
506
558
  let feedbackEvent = null;
507
- const memoryRecords = readJSONLLocal(memoryLogPath, { maxLines: 0 });
559
+ const memoryRecords = readLessonJsonl(memoryLogPath, { maxLines: 0 });
508
560
  for (const rec of memoryRecords) {
509
561
  if (rec.id === id) { memoryRecord = rec; break; }
510
562
  }
511
- const feedbackRecords = readJSONLLocal(feedbackLogPath, { maxLines: 0 });
563
+ const feedbackRecords = readLessonJsonl(feedbackLogPath, { maxLines: 0 });
512
564
  for (const rec of feedbackRecords) {
513
565
  if (rec.id === id) { feedbackEvent = rec; break; }
514
566
  }
@@ -533,8 +585,8 @@ function updateLessonRecord(feedbackDir, lessonId, updater) {
533
585
  if (!updated) return null;
534
586
  const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
535
587
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
536
- const updatedMemory = updateRecordInJsonl(memoryLogPath, lessonId, updated);
537
- const updatedFeedback = updateRecordInJsonl(feedbackLogPath, lessonId, updated);
588
+ const updatedMemory = updateLessonJsonlRecord(memoryLogPath, lessonId, updated);
589
+ const updatedFeedback = updateLessonJsonlRecord(feedbackLogPath, lessonId, updated);
538
590
  if (!updatedMemory && !updatedFeedback) return null;
539
591
  return updated;
540
592
  }
@@ -1113,6 +1165,61 @@ function resolveBillingSummaryOptions(parsed) {
1113
1165
  });
1114
1166
  }
1115
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
+
1116
1223
  function createJourneyId(prefix) {
1117
1224
  return createTraceId(prefix).replace(/^trace_/, `${prefix}_`);
1118
1225
  }
@@ -1336,6 +1443,7 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
1336
1443
  }
1337
1444
 
1338
1445
  function resolveCheckoutOfferSummary(metadata = {}) {
1446
+ const commercialOffer = getCommercialOfferModule();
1339
1447
  const planId = normalizePlanId(metadata.planId);
1340
1448
  const billingCycle = normalizeBillingCycle(metadata.billingCycle);
1341
1449
 
@@ -1346,8 +1454,8 @@ function resolveCheckoutOfferSummary(metadata = {}) {
1346
1454
  billingCycle: 'monthly',
1347
1455
  seatCount,
1348
1456
  type: 'subscription',
1349
- price: TEAM_MONTHLY_PRICE_DOLLARS * seatCount,
1350
- priceLabel: `$${TEAM_MONTHLY_PRICE_DOLLARS}/seat/mo`,
1457
+ price: commercialOffer.TEAM_MONTHLY_PRICE_DOLLARS * seatCount,
1458
+ priceLabel: `$${commercialOffer.TEAM_MONTHLY_PRICE_DOLLARS}/seat/mo`,
1351
1459
  };
1352
1460
  }
1353
1461
 
@@ -1357,7 +1465,7 @@ function resolveCheckoutOfferSummary(metadata = {}) {
1357
1465
  billingCycle: 'annual',
1358
1466
  seatCount: 1,
1359
1467
  type: 'subscription',
1360
- price: PRO_ANNUAL_PRICE_DOLLARS,
1468
+ price: commercialOffer.PRO_ANNUAL_PRICE_DOLLARS,
1361
1469
  priceLabel: '$149/yr',
1362
1470
  };
1363
1471
  }
@@ -1367,7 +1475,7 @@ function resolveCheckoutOfferSummary(metadata = {}) {
1367
1475
  billingCycle: 'monthly',
1368
1476
  seatCount: 1,
1369
1477
  type: 'subscription',
1370
- price: PRO_MONTHLY_PRICE_DOLLARS,
1478
+ price: commercialOffer.PRO_MONTHLY_PRICE_DOLLARS,
1371
1479
  priceLabel: '$19/mo',
1372
1480
  };
1373
1481
  }
@@ -2161,6 +2269,37 @@ function servePublicMarketingPage({
2161
2269
  'landing_page_view'
2162
2270
  );
2163
2271
 
2272
+ // Funnel-ledger write (2026-04-21): populate funnel-events.jsonl with a
2273
+ // discovery-stage event on every landing-page view so UTM-tagged social
2274
+ // traffic becomes visible in `npm run feedback:summary` and
2275
+ // `bin/cli.js cfo --today`. Prior to this wire, landing views wrote only
2276
+ // to telemetry-pings.jsonl (invisible to the CEO-facing revenue surface),
2277
+ // leaving funnel-events.jsonl empty despite 404 published Zernio posts.
2278
+ // Best-effort: wrapped in try/catch so a billing-ledger hiccup never
2279
+ // breaks a page render.
2280
+ try {
2281
+ appendFunnelEvent({
2282
+ stage: 'discovery',
2283
+ event: 'landing_view',
2284
+ installId: journeyState.visitorId || null,
2285
+ traceId: journeyState.acquisitionId || null,
2286
+ evidence: landingAttribution.landingPath || 'landing_view',
2287
+ metadata: {
2288
+ page: extraTelemetry.pageType || landingAttribution.page || 'landing',
2289
+ utmSource: landingAttribution.utmSource || null,
2290
+ utmMedium: landingAttribution.utmMedium || null,
2291
+ utmCampaign: landingAttribution.utmCampaign || null,
2292
+ utmContent: landingAttribution.utmContent || null,
2293
+ utmTerm: landingAttribution.utmTerm || null,
2294
+ referrerHost: landingAttribution.referrerHost || null,
2295
+ sessionId: journeyState.sessionId || null,
2296
+ },
2297
+ });
2298
+ } catch {
2299
+ // Funnel ledger is best-effort on page render; telemetry-pings remains
2300
+ // the authoritative observability path if the ledger write fails.
2301
+ }
2302
+
2164
2303
  if (isSeoAttributionSource(landingAttribution.source)) {
2165
2304
  appendBestEffortTelemetry(FEEDBACK_DIR, {
2166
2305
  eventType: 'seo_landing_view',
@@ -3100,6 +3239,18 @@ function createApiServer() {
3100
3239
  const expectedApiKey = getExpectedApiKey();
3101
3240
  const expectedOperatorKey = getExpectedOperatorKey();
3102
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
+
3103
3254
  return http.createServer(async (req, res) => {
3104
3255
  const parsed = new URL(req.url, 'http://localhost');
3105
3256
  const pathname = parsed.pathname;
@@ -3333,6 +3484,19 @@ function createApiServer() {
3333
3484
  landingPath,
3334
3485
  attribution,
3335
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
+ });
3336
3500
  }
3337
3501
  const journeyState = resolveJourneyState(req, parsed);
3338
3502
  appendBestEffortTelemetry(getFeedbackPaths().FEEDBACK_DIR, {
@@ -3655,8 +3819,8 @@ async function addContext(){
3655
3819
  const feedbackDir = requestSafeDataDir;
3656
3820
  const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
3657
3821
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
3658
- const deletedMemory = deleteRecordFromJsonl(memoryLogPath, lessonId);
3659
- const deletedFeedback = deleteRecordFromJsonl(feedbackLogPath, lessonId);
3822
+ const deletedMemory = deleteLessonJsonlRecord(memoryLogPath, lessonId);
3823
+ const deletedFeedback = deleteLessonJsonlRecord(feedbackLogPath, lessonId);
3660
3824
  if (!deletedMemory && !deletedFeedback) {
3661
3825
  sendJson(res, 404, { error: 'Record not found' });
3662
3826
  return;
@@ -3777,6 +3941,26 @@ async function addContext(){
3777
3941
  return;
3778
3942
  }
3779
3943
 
3944
+ if (isGetLikeRequest && (pathname === '/numbers' || pathname === '/numbers.html')) {
3945
+ // Route through servePublicMarketingPage so landing_page_view telemetry
3946
+ // + funnel-events.jsonl `discovery/landing_view` get captured with UTM
3947
+ // attribution — critical for Zernio social CTAs that target /numbers.
3948
+ try {
3949
+ servePublicMarketingPage({
3950
+ req,
3951
+ res,
3952
+ parsed,
3953
+ hostedConfig,
3954
+ isHeadRequest,
3955
+ renderHtml: () => fs.readFileSync(NUMBERS_PAGE_PATH, 'utf-8'),
3956
+ extraTelemetry: { pageType: 'numbers' },
3957
+ });
3958
+ } catch {
3959
+ sendJson(res, 404, { error: 'Numbers page not found' });
3960
+ }
3961
+ return;
3962
+ }
3963
+
3780
3964
  if (isGetLikeRequest && pathname === '/learn/learn.css') {
3781
3965
  try {
3782
3966
  const cssPath = path.join(LEARN_DIR, 'learn.css');
@@ -4373,7 +4557,8 @@ async function addContext(){
4373
4557
  const body = isFormSubmission
4374
4558
  ? await parseFormBody(req, 24 * 1024)
4375
4559
  : await parseJsonBody(req, 24 * 1024);
4376
- const lead = appendWorkflowSprintLead({
4560
+ const workflowSprintIntake = requirePrivateApiModule('workflowSprintIntake', 'Workflow sprint intake');
4561
+ const lead = workflowSprintIntake.appendWorkflowSprintLead({
4377
4562
  ...body,
4378
4563
  traceId: body.traceId || traceId,
4379
4564
  acquisitionId: body.acquisitionId || journeyState.acquisitionId,
@@ -4784,15 +4969,71 @@ async function addContext(){
4784
4969
  return;
4785
4970
  }
4786
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
+
4787
5027
  if (req.method === 'GET' && pathname === '/v1/intents/catalog') {
4788
5028
  const mcpProfile = parsed.searchParams.get('mcpProfile') || undefined;
4789
5029
  const bundleId = parsed.searchParams.get('bundleId') || undefined;
4790
5030
  const partnerProfile = parsed.searchParams.get('partnerProfile') || undefined;
4791
5031
  try {
4792
- const catalog = listIntents({ mcpProfile, bundleId, partnerProfile });
5032
+ const intentRouter = requirePrivateApiModule('intentRouter', 'Intent catalog');
5033
+ const catalog = intentRouter.listIntents({ mcpProfile, bundleId, partnerProfile });
4793
5034
  sendJson(res, 200, catalog);
4794
5035
  } catch (err) {
4795
- throw createHttpError(400, err.message || 'Invalid intent catalog request');
5036
+ throw createHttpError(err.statusCode || 400, err.message || 'Invalid intent catalog request');
4796
5037
  }
4797
5038
  return;
4798
5039
  }
@@ -4800,7 +5041,8 @@ async function addContext(){
4800
5041
  if (req.method === 'POST' && pathname === '/v1/intents/plan') {
4801
5042
  const body = await parseJsonBody(req);
4802
5043
  try {
4803
- const plan = planIntent({
5044
+ const intentRouter = requirePrivateApiModule('intentRouter', 'Intent planning');
5045
+ const plan = intentRouter.planIntent({
4804
5046
  intentId: body.intentId,
4805
5047
  context: body.context || '',
4806
5048
  mcpProfile: body.mcpProfile,
@@ -4812,7 +5054,7 @@ async function addContext(){
4812
5054
  });
4813
5055
  sendJson(res, 200, plan);
4814
5056
  } catch (err) {
4815
- throw createHttpError(400, err.message || 'Invalid intent plan request');
5057
+ throw createHttpError(err.statusCode || 400, err.message || 'Invalid intent plan request');
4816
5058
  }
4817
5059
  return;
4818
5060
  }
@@ -4820,7 +5062,9 @@ async function addContext(){
4820
5062
  if (req.method === 'POST' && pathname === '/v1/handoffs/start') {
4821
5063
  const body = await parseJsonBody(req);
4822
5064
  try {
4823
- const plan = planIntent({
5065
+ const intentRouter = requirePrivateApiModule('intentRouter', 'Handoff planning');
5066
+ const delegationRuntime = requirePrivateApiModule('delegationRuntime', 'Sequential handoffs');
5067
+ const plan = intentRouter.planIntent({
4824
5068
  intentId: body.intentId,
4825
5069
  context: body.context || '',
4826
5070
  mcpProfile: body.mcpProfile,
@@ -4830,7 +5074,7 @@ async function addContext(){
4830
5074
  approved: body.approved === true,
4831
5075
  repoPath: body.repoPath,
4832
5076
  });
4833
- const result = startHandoff({
5077
+ const result = delegationRuntime.startHandoff({
4834
5078
  plan,
4835
5079
  context: body.context || '',
4836
5080
  mcpProfile: body.mcpProfile || plan.mcpProfile,
@@ -4849,7 +5093,8 @@ async function addContext(){
4849
5093
  if (req.method === 'POST' && pathname === '/v1/handoffs/complete') {
4850
5094
  const body = await parseJsonBody(req);
4851
5095
  try {
4852
- const result = completeHandoff({
5096
+ const delegationRuntime = requirePrivateApiModule('delegationRuntime', 'Sequential handoffs');
5097
+ const result = delegationRuntime.completeHandoff({
4853
5098
  handoffId: body.handoffId,
4854
5099
  outcome: body.outcome,
4855
5100
  resultContext: body.resultContext || '',
@@ -4940,7 +5185,8 @@ async function addContext(){
4940
5185
  }
4941
5186
  const inputs = parseOptionalObject(body.inputs, 'inputs') || {};
4942
5187
  try {
4943
- const launched = launchHarnessJob(identifier, inputs, {
5188
+ const hostedJobLauncher = requirePrivateApiModule('hostedJobLauncher', 'Hosted harness jobs');
5189
+ const launched = hostedJobLauncher.launchHarnessJob(identifier, inputs, {
4944
5190
  jobId: normalizeNullableText(body.jobId) || undefined,
4945
5191
  skill: normalizeNullableText(body.skill) || undefined,
4946
5192
  partnerProfile: normalizeNullableText(body.partnerProfile) || undefined,
@@ -4999,8 +5245,9 @@ async function addContext(){
4999
5245
  throw createHttpError(409, `Job ${jobId} is already ${state.status}`);
5000
5246
  }
5001
5247
 
5248
+ const hostedJobLauncher = requirePrivateApiModule('hostedJobLauncher', 'Hosted job control');
5002
5249
  if (action === 'resume') {
5003
- const launched = resumeHostedJob(jobId);
5250
+ const launched = hostedJobLauncher.resumeHostedJob(jobId);
5004
5251
  sendJson(res, 202, {
5005
5252
  accepted: true,
5006
5253
  action,
@@ -5014,8 +5261,8 @@ async function addContext(){
5014
5261
 
5015
5262
  if (IDLE_JOB_STATUSES.has(state.status)) {
5016
5263
  const job = action === 'pause'
5017
- ? pauseQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {})
5018
- : cancelQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {});
5264
+ ? hostedJobLauncher.pauseQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {})
5265
+ : hostedJobLauncher.cancelQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {});
5019
5266
  sendJson(res, 202, {
5020
5267
  accepted: true,
5021
5268
  action,
@@ -5144,7 +5391,8 @@ async function addContext(){
5144
5391
  .split(',')
5145
5392
  .map((tag) => tag.trim())
5146
5393
  .filter(Boolean);
5147
- const results = searchLessons(query, {
5394
+ const lessonSearch = requirePrivateApiModule('lessonSearch', 'Lesson search');
5395
+ const results = lessonSearch.searchLessons(query, {
5148
5396
  limit: Number.isFinite(limit) ? limit : 10,
5149
5397
  category,
5150
5398
  tags,
@@ -5258,6 +5506,19 @@ async function addContext(){
5258
5506
  tags: extractTags(body.tags),
5259
5507
  skill: body.skill,
5260
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
+ }
5261
5522
  const code = result.accepted ? 200 : 422;
5262
5523
  sendJson(res, code, result);
5263
5524
  return;
@@ -5270,6 +5531,13 @@ async function addContext(){
5270
5531
  ? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
5271
5532
  : undefined;
5272
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
+ });
5273
5541
  sendJson(res, 200, {
5274
5542
  path: result.path,
5275
5543
  markdown: result.markdown,
@@ -5327,7 +5595,8 @@ async function addContext(){
5327
5595
 
5328
5596
  if (wantsAsync) {
5329
5597
  try {
5330
- const launched = launchDpoExportJob(paths, {
5598
+ const hostedJobLauncher = requirePrivateApiModule('hostedJobLauncher', 'Hosted DPO export jobs');
5599
+ const launched = hostedJobLauncher.launchDpoExportJob(paths, {
5331
5600
  jobId: normalizeNullableText(body.jobId) || undefined,
5332
5601
  });
5333
5602
  sendJson(res, 202, {
@@ -5370,8 +5639,8 @@ async function addContext(){
5370
5639
  const feedbackDir = requestSafeDataDir;
5371
5640
  const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
5372
5641
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
5373
- const memories = readJSONLLocal(memoryLogPath, { maxLines: 0 });
5374
- const feedbacks = readJSONLLocal(feedbackLogPath, { maxLines: 0 });
5642
+ const memories = readLessonJsonl(memoryLogPath, { maxLines: 0 });
5643
+ const feedbacks = readLessonJsonl(feedbackLogPath, { maxLines: 0 });
5375
5644
 
5376
5645
  // Merge into unified lesson records
5377
5646
  const lessonMap = new Map();
@@ -5461,7 +5730,7 @@ async function addContext(){
5461
5730
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
5462
5731
 
5463
5732
  // Load existing IDs for dedup
5464
- const existing = readJSONLLocal(feedbackLogPath, { maxLines: 0 });
5733
+ const existing = readLessonJsonl(feedbackLogPath, { maxLines: 0 });
5465
5734
  const existingIds = new Set(existing.map((r) => r.id).filter(Boolean));
5466
5735
  // Also dedup by title+signal content hash
5467
5736
  const existingHashes = new Set(existing.map((r) => {
@@ -5646,12 +5915,12 @@ async function addContext(){
5646
5915
 
5647
5916
  // GET /v1/semantic/describe — get canonical definition of a business entity
5648
5917
  if (req.method === 'GET' && pathname === '/v1/semantic/describe') {
5649
- const { describeSemanticSchema } = require('../../scripts/semantic-layer');
5918
+ const semanticLayer = requirePrivateApiModule('semanticLayer', 'Semantic schema');
5650
5919
  const type = parsed.query.type;
5651
5920
  if (!type) {
5652
5921
  throw createHttpError(400, 'type query parameter is required');
5653
5922
  }
5654
- const schema = describeSemanticSchema();
5923
+ const schema = semanticLayer.describeSemanticSchema();
5655
5924
  const entity = schema.entities[type] || schema.metrics[type];
5656
5925
  if (!entity) {
5657
5926
  sendProblem(res, {
@@ -5727,16 +5996,12 @@ async function addContext(){
5727
5996
  return;
5728
5997
  }
5729
5998
 
5730
- let summaryOptions;
5731
- try {
5732
- summaryOptions = resolveBillingSummaryOptions(parsed);
5733
- } catch (err) {
5734
- sendProblem(res, {
5735
- type: PROBLEM_TYPES.INVALID_REQUEST,
5736
- title: 'Invalid billing summary query',
5737
- status: 400,
5738
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
5739
- });
5999
+ const summaryOptions = resolveBillingSummaryOptionsOrRespondProblem(
6000
+ res,
6001
+ parsed,
6002
+ 'Invalid billing summary query',
6003
+ );
6004
+ if (!summaryOptions) {
5740
6005
  return;
5741
6006
  }
5742
6007
 
@@ -5760,7 +6025,8 @@ async function addContext(){
5760
6025
  const { FEEDBACK_DIR } = getFeedbackPaths();
5761
6026
  try {
5762
6027
  const body = await parseJsonBody(req, 24 * 1024);
5763
- const result = advanceWorkflowSprintLead(body, { feedbackDir: FEEDBACK_DIR });
6028
+ const workflowSprintIntake = requirePrivateApiModule('workflowSprintIntake', 'Workflow sprint intake');
6029
+ const result = workflowSprintIntake.advanceWorkflowSprintLead(body, { feedbackDir: FEEDBACK_DIR });
5764
6030
 
5765
6031
  appendBestEffortTelemetry(FEEDBACK_DIR, {
5766
6032
  eventType: 'workflow_sprint_lead_advanced',
@@ -5857,28 +6123,36 @@ async function addContext(){
5857
6123
  return;
5858
6124
  }
5859
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
+
5860
6143
  // GET /v1/dashboard -- Full ThumbGate dashboard JSON
5861
6144
  if (req.method === 'GET' && pathname === '/v1/dashboard') {
5862
- let summaryOptions;
5863
- try {
5864
- summaryOptions = resolveBillingSummaryOptions(parsed);
5865
- } catch (err) {
5866
- sendProblem(res, {
5867
- type: PROBLEM_TYPES.INVALID_REQUEST,
5868
- title: 'Invalid dashboard query',
5869
- status: 400,
5870
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
5871
- });
6145
+ const dashboardResult = await loadLiveDashboardDataOrRespondProblem(
6146
+ res,
6147
+ parsed,
6148
+ requestFeedbackDir,
6149
+ 'Invalid dashboard query',
6150
+ );
6151
+ if (!dashboardResult) {
5872
6152
  return;
5873
6153
  }
6154
+ const { data } = dashboardResult;
5874
6155
 
5875
- const billingSummary = await getBillingSummaryLive(summaryOptions);
5876
- const data = generateDashboard(requestFeedbackDir, {
5877
- analyticsWindow: summaryOptions,
5878
- billingSummary,
5879
- billingSource: 'live',
5880
- authContext: { tier: 'pro' },
5881
- });
5882
6156
  sendJson(res, 200, data);
5883
6157
  return;
5884
6158
  }
@@ -5915,27 +6189,18 @@ async function addContext(){
5915
6189
 
5916
6190
  // GET /v1/dashboard/render-spec -- Constrained hosted dashboard JSON spec
5917
6191
  if (req.method === 'GET' && pathname === '/v1/dashboard/render-spec') {
5918
- let summaryOptions;
5919
- try {
5920
- summaryOptions = resolveBillingSummaryOptions(parsed);
5921
- } catch (err) {
5922
- sendProblem(res, {
5923
- type: PROBLEM_TYPES.INVALID_REQUEST,
5924
- title: 'Invalid render-spec query',
5925
- status: 400,
5926
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
5927
- });
6192
+ const dashboardResult = await loadLiveDashboardDataOrRespondProblem(
6193
+ res,
6194
+ parsed,
6195
+ requestFeedbackDir,
6196
+ 'Invalid render-spec query',
6197
+ );
6198
+ if (!dashboardResult) {
5928
6199
  return;
5929
6200
  }
6201
+ const { summaryOptions, data } = dashboardResult;
5930
6202
 
5931
6203
  try {
5932
- const billingSummary = await getBillingSummaryLive(summaryOptions);
5933
- const data = generateDashboard(requestFeedbackDir, {
5934
- analyticsWindow: summaryOptions,
5935
- billingSummary,
5936
- billingSource: 'live',
5937
- authContext: { tier: 'pro' },
5938
- });
5939
6204
  const renderSpec = buildDashboardRenderSpec(data, {
5940
6205
  view: parsed.searchParams.get('view') || undefined,
5941
6206
  now: summaryOptions.now,
@@ -5954,42 +6219,87 @@ async function addContext(){
5954
6219
 
5955
6220
  if (req.method === 'POST' && pathname === '/v1/decisions/evaluate') {
5956
6221
  const body = await parseJsonBody(req);
5957
- 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) {
5958
6233
  sendProblem(res, {
5959
6234
  type: PROBLEM_TYPES.BAD_REQUEST,
5960
6235
  title: 'Bad Request',
5961
6236
  status: 400,
5962
- detail: 'toolName is required.',
6237
+ detail: 'toolName or provider tool call is required.',
5963
6238
  });
5964
6239
  return;
5965
6240
  }
5966
6241
 
5967
- 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 = {
5968
6247
  command: body.command,
5969
6248
  path: body.filePath,
5970
- changed_files: Array.isArray(body.changedFiles) ? body.changedFiles : [],
6249
+ changed_files: changedFiles,
5971
6250
  repoPath: body.repoPath,
5972
6251
  baseBranch: body.baseBranch,
5973
- }, {
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,
5974
6274
  repoPath: body.repoPath,
5975
6275
  baseBranch: body.baseBranch,
5976
- affectedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : undefined,
6276
+ affectedFiles: changedFiles.length > 0 ? changedFiles : undefined,
5977
6277
  requirePrForReleaseSensitive: body.requirePrForReleaseSensitive === true,
5978
6278
  requireVersionNotBehindBase: body.requireVersionNotBehindBase === true,
5979
- 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
+ },
5980
6288
  feedbackDir: requestFeedbackDir,
5981
6289
  });
5982
6290
  const evaluation = recordDecisionEvaluation(report, {
5983
6291
  source: 'api',
5984
- toolName: body.toolName,
6292
+ toolName: report.toolName,
5985
6293
  toolInput: {
5986
6294
  command: body.command,
5987
6295
  filePath: body.filePath,
5988
- changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
6296
+ changedFiles,
5989
6297
  repoPath: body.repoPath,
5990
6298
  baseBranch: body.baseBranch,
6299
+ normalizedAction: report.normalizedAction,
6300
+ costControl: report.costControl,
5991
6301
  },
5992
- changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
6302
+ changedFiles,
5993
6303
  }, {
5994
6304
  feedbackDir: requestFeedbackDir,
5995
6305
  });
@@ -6199,9 +6509,13 @@ module.exports = {
6199
6509
  startServer,
6200
6510
  __test__: {
6201
6511
  buildCheckoutFallbackUrl,
6512
+ createPrivateCoreUnavailableError,
6202
6513
  buildPosthogProxyRequestOptions,
6203
6514
  getPosthogProxyPath,
6204
6515
  isAllowedPosthogProxyPath,
6516
+ PRIVATE_API_MODULES,
6517
+ loadPrivateApiModule,
6518
+ requirePrivateApiModule,
6205
6519
  renderSitemapXml,
6206
6520
  renderPackagedDashboardHtml,
6207
6521
  renderPackagedLessonsHtml,