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.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +5 -5
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +60 -35
- package/adapters/chatgpt/openapi.yaml +118 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +217 -84
- package/adapters/opencode/opencode.json +1 -1
- package/bench/prompt-eval-suite.json +5 -1
- package/bin/cli.js +211 -8
- package/config/enforcement.json +59 -7
- package/config/evals/agent-safety-eval.json +338 -22
- package/config/gates/default.json +33 -0
- package/config/gates/routine.json +43 -0
- package/config/github-about.json +3 -3
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +2 -1
- package/config/model-candidates.json +131 -0
- package/openapi/openapi.yaml +118 -2
- package/package.json +70 -51
- package/public/blog.html +7 -7
- package/public/codex-plugin.html +13 -7
- package/public/compare.html +29 -23
- package/public/dashboard.html +105 -12
- package/public/guide.html +28 -28
- package/public/index.html +233 -97
- package/public/learn.html +87 -20
- package/public/lessons.html +26 -2
- package/public/numbers.html +271 -0
- package/public/pro.html +89 -19
- package/scripts/agent-audit-trace.js +55 -0
- package/scripts/agent-memory-lifecycle.js +96 -0
- package/scripts/agent-readiness-plan.js +118 -0
- package/scripts/agentic-data-pipeline.js +21 -1
- package/scripts/agents-sdk-sandbox-plan.js +57 -0
- package/scripts/ai-org-governance.js +98 -0
- package/scripts/ai-search-distribution.js +43 -0
- package/scripts/artifact-agent-plan.js +81 -0
- package/scripts/billing.js +27 -8
- package/scripts/cli-feedback.js +2 -1
- package/scripts/cli-schema.js +60 -5
- package/scripts/code-mode-mcp-plan.js +71 -0
- package/scripts/commercial-offer.js +1 -1
- package/scripts/context-engine.js +1 -2
- package/scripts/context-manager.js +4 -1
- package/scripts/contextfs.js +214 -32
- package/scripts/dashboard-render-spec.js +1 -1
- package/scripts/dashboard.js +275 -9
- package/scripts/decision-journal.js +13 -3
- package/scripts/document-workflow-governance.js +62 -0
- package/scripts/enterprise-agent-rollout.js +34 -0
- package/scripts/experience-replay-governance.js +69 -0
- package/scripts/export-hf-dataset.js +1 -1
- package/scripts/feedback-loop.js +141 -9
- package/scripts/feedback-to-rules.js +17 -23
- package/scripts/gates-engine.js +4 -6
- package/scripts/growth-campaigns.js +49 -0
- package/scripts/harness-selector.js +145 -1
- package/scripts/hybrid-supervisor-agent.js +64 -0
- package/scripts/inference-cache-policy.js +72 -0
- package/scripts/inference-economics.js +53 -0
- package/scripts/internal-agent-bootstrap.js +12 -2
- package/scripts/knowledge-layer-plan.js +108 -0
- package/scripts/lesson-canonical.js +181 -0
- package/scripts/lesson-db.js +71 -10
- package/scripts/lesson-inference.js +183 -44
- package/scripts/lesson-search.js +4 -1
- package/scripts/lesson-synthesis.js +23 -2
- package/scripts/llm-client.js +157 -26
- package/scripts/mailer/resend-mailer.js +112 -1
- package/scripts/mcp-transport-strategy.js +66 -0
- package/scripts/memory-store-governance.js +60 -0
- package/scripts/meta-agent-loop.js +7 -13
- package/scripts/model-access-eligibility.js +38 -0
- package/scripts/model-migration-readiness.js +55 -0
- package/scripts/native-messaging-audit.js +514 -0
- package/scripts/operational-integrity.js +96 -3
- package/scripts/otel-declarative-config.js +56 -0
- package/scripts/perplexity-client.js +1 -1
- package/scripts/post-training-governance.js +34 -0
- package/scripts/pr-manager.js +47 -7
- package/scripts/private-core-boundary.js +72 -0
- package/scripts/production-agent-readiness.js +40 -0
- package/scripts/profile-router.js +16 -1
- package/scripts/prompt-eval.js +564 -32
- package/scripts/prompt-programs.js +93 -0
- package/scripts/provider-action-normalizer.js +585 -0
- package/scripts/rule-validator.js +285 -0
- package/scripts/scaling-law-claims.js +60 -0
- package/scripts/security-scanner.js +1 -1
- package/scripts/self-distill-agent.js +7 -32
- package/scripts/seo-gsd.js +400 -43
- package/scripts/skill-rag-router.js +53 -0
- package/scripts/spec-gate.js +1 -1
- package/scripts/student-consistent-training.js +73 -0
- package/scripts/synthetic-data-provenance.js +98 -0
- package/scripts/task-context-result.js +81 -0
- package/scripts/telemetry-analytics.js +149 -0
- package/scripts/thompson-sampling.js +2 -2
- package/scripts/token-savings.js +7 -6
- package/scripts/token-tco.js +46 -0
- package/scripts/tool-registry.js +75 -3
- package/scripts/verification-loop.js +10 -1
- package/scripts/verifier-scoring.js +71 -0
- package/scripts/workflow-sentinel.js +284 -28
- package/scripts/workspace-agent-routines.js +118 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +434 -120
- package/.claude-plugin/README.md +0 -170
- package/adapters/README.md +0 -12
- package/scripts/analytics-report.js +0 -328
- package/scripts/autonomous-workflow.js +0 -377
- package/scripts/billing-setup.js +0 -109
- package/scripts/creator-campaigns.js +0 -239
- package/scripts/cross-encoder-reranker.js +0 -235
- package/scripts/daemon-manager.js +0 -108
- package/scripts/decision-trace.js +0 -354
- package/scripts/delegation-runtime.js +0 -896
- package/scripts/dispatch-brief.js +0 -159
- package/scripts/distribution-surfaces.js +0 -110
- package/scripts/feedback-history-distiller.js +0 -382
- package/scripts/funnel-analytics.js +0 -35
- package/scripts/history-distiller.js +0 -200
- package/scripts/hosted-job-launcher.js +0 -256
- package/scripts/intent-router.js +0 -392
- package/scripts/lesson-reranker.js +0 -263
- package/scripts/lesson-retrieval.js +0 -148
- package/scripts/managed-lesson-agent.js +0 -183
- package/scripts/operational-dashboard.js +0 -103
- package/scripts/operational-summary.js +0 -129
- package/scripts/operator-artifacts.js +0 -608
- package/scripts/optimize-context.js +0 -17
- package/scripts/org-dashboard.js +0 -206
- package/scripts/partner-orchestration.js +0 -146
- package/scripts/predictive-insights.js +0 -356
- package/scripts/pulse.js +0 -80
- package/scripts/reflector-agent.js +0 -221
- package/scripts/sales-pipeline.js +0 -681
- package/scripts/session-episode-store.js +0 -329
- package/scripts/session-health-sensor.js +0 -242
- package/scripts/session-report.js +0 -120
- package/scripts/swarm-coordinator.js +0 -81
- package/scripts/tool-kpi-tracker.js +0 -12
- package/scripts/webhook-delivery.js +0 -62
- package/scripts/workflow-sprint-intake.js +0 -475
- package/skills/agent-memory/SKILL.md +0 -97
- package/skills/solve-architecture-autonomy/SKILL.md +0 -17
- package/skills/solve-architecture-autonomy/tool.js +0 -33
- 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
|
-
} =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
537
|
-
const updatedFeedback =
|
|
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 =
|
|
3659
|
-
const deletedFeedback =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
5374
|
-
const feedbacks =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
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
|
|
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
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
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
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
6276
|
+
affectedFiles: changedFiles.length > 0 ? changedFiles : undefined,
|
|
5977
6277
|
requirePrForReleaseSensitive: body.requirePrForReleaseSensitive === true,
|
|
5978
6278
|
requireVersionNotBehindBase: body.requireVersionNotBehindBase === true,
|
|
5979
|
-
governanceState:
|
|
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:
|
|
6292
|
+
toolName: report.toolName,
|
|
5985
6293
|
toolInput: {
|
|
5986
6294
|
command: body.command,
|
|
5987
6295
|
filePath: body.filePath,
|
|
5988
|
-
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
|
|
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,
|