thumbgate 1.15.0 → 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 (129) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +59 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +210 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +157 -8
  12. package/config/evals/agent-safety-eval.json +338 -22
  13. package/config/gates/routine.json +43 -0
  14. package/config/github-about.json +3 -3
  15. package/config/model-candidates.json +131 -0
  16. package/openapi/openapi.yaml +118 -2
  17. package/package.json +55 -48
  18. package/public/blog.html +7 -7
  19. package/public/codex-plugin.html +6 -6
  20. package/public/compare.html +29 -23
  21. package/public/dashboard.html +82 -10
  22. package/public/guide.html +28 -28
  23. package/public/index.html +216 -98
  24. package/public/learn.html +50 -22
  25. package/public/lessons.html +1 -1
  26. package/public/numbers.html +17 -17
  27. package/public/pro.html +82 -18
  28. package/scripts/agent-audit-trace.js +55 -0
  29. package/scripts/agent-memory-lifecycle.js +96 -0
  30. package/scripts/agent-readiness-plan.js +118 -0
  31. package/scripts/agentic-data-pipeline.js +21 -1
  32. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  33. package/scripts/ai-org-governance.js +98 -0
  34. package/scripts/ai-search-distribution.js +43 -0
  35. package/scripts/artifact-agent-plan.js +81 -0
  36. package/scripts/billing.js +27 -8
  37. package/scripts/cli-schema.js +18 -2
  38. package/scripts/code-mode-mcp-plan.js +71 -0
  39. package/scripts/context-engine.js +1 -2
  40. package/scripts/context-manager.js +4 -1
  41. package/scripts/dashboard-render-spec.js +1 -1
  42. package/scripts/dashboard.js +275 -9
  43. package/scripts/decision-journal.js +13 -3
  44. package/scripts/document-workflow-governance.js +62 -0
  45. package/scripts/enterprise-agent-rollout.js +34 -0
  46. package/scripts/experience-replay-governance.js +69 -0
  47. package/scripts/export-hf-dataset.js +1 -1
  48. package/scripts/feedback-loop.js +92 -4
  49. package/scripts/feedback-to-rules.js +17 -23
  50. package/scripts/gates-engine.js +4 -6
  51. package/scripts/growth-campaigns.js +49 -0
  52. package/scripts/harness-selector.js +16 -4
  53. package/scripts/hybrid-supervisor-agent.js +64 -0
  54. package/scripts/inference-cache-policy.js +72 -0
  55. package/scripts/inference-economics.js +53 -0
  56. package/scripts/internal-agent-bootstrap.js +12 -2
  57. package/scripts/knowledge-layer-plan.js +108 -0
  58. package/scripts/lesson-inference.js +183 -44
  59. package/scripts/lesson-search.js +4 -1
  60. package/scripts/llm-client.js +157 -26
  61. package/scripts/mailer/resend-mailer.js +112 -1
  62. package/scripts/mcp-transport-strategy.js +66 -0
  63. package/scripts/memory-store-governance.js +60 -0
  64. package/scripts/meta-agent-loop.js +7 -13
  65. package/scripts/model-access-eligibility.js +38 -0
  66. package/scripts/model-migration-readiness.js +55 -0
  67. package/scripts/operational-integrity.js +96 -3
  68. package/scripts/otel-declarative-config.js +56 -0
  69. package/scripts/perplexity-client.js +1 -1
  70. package/scripts/post-training-governance.js +34 -0
  71. package/scripts/private-core-boundary.js +72 -0
  72. package/scripts/production-agent-readiness.js +40 -0
  73. package/scripts/prompt-eval.js +564 -32
  74. package/scripts/prompt-programs.js +93 -0
  75. package/scripts/provider-action-normalizer.js +585 -0
  76. package/scripts/scaling-law-claims.js +60 -0
  77. package/scripts/security-scanner.js +1 -1
  78. package/scripts/self-distill-agent.js +7 -32
  79. package/scripts/seo-gsd.js +232 -55
  80. package/scripts/skill-rag-router.js +53 -0
  81. package/scripts/spec-gate.js +1 -1
  82. package/scripts/student-consistent-training.js +73 -0
  83. package/scripts/synthetic-data-provenance.js +98 -0
  84. package/scripts/task-context-result.js +81 -0
  85. package/scripts/telemetry-analytics.js +149 -0
  86. package/scripts/thompson-sampling.js +2 -2
  87. package/scripts/token-savings.js +7 -6
  88. package/scripts/token-tco.js +46 -0
  89. package/scripts/tool-registry.js +63 -3
  90. package/scripts/verification-loop.js +10 -1
  91. package/scripts/verifier-scoring.js +71 -0
  92. package/scripts/workflow-sentinel.js +284 -28
  93. package/scripts/workspace-agent-routines.js +118 -0
  94. package/src/api/server.js +381 -120
  95. package/scripts/analytics-report.js +0 -328
  96. package/scripts/autonomous-workflow.js +0 -377
  97. package/scripts/billing-setup.js +0 -109
  98. package/scripts/creator-campaigns.js +0 -239
  99. package/scripts/cross-encoder-reranker.js +0 -235
  100. package/scripts/daemon-manager.js +0 -108
  101. package/scripts/decision-trace.js +0 -354
  102. package/scripts/delegation-runtime.js +0 -896
  103. package/scripts/dispatch-brief.js +0 -159
  104. package/scripts/distribution-surfaces.js +0 -110
  105. package/scripts/feedback-history-distiller.js +0 -382
  106. package/scripts/funnel-analytics.js +0 -35
  107. package/scripts/history-distiller.js +0 -200
  108. package/scripts/hosted-job-launcher.js +0 -256
  109. package/scripts/intent-router.js +0 -392
  110. package/scripts/lesson-reranker.js +0 -263
  111. package/scripts/lesson-retrieval.js +0 -148
  112. package/scripts/managed-lesson-agent.js +0 -183
  113. package/scripts/operational-dashboard.js +0 -103
  114. package/scripts/operational-summary.js +0 -129
  115. package/scripts/operator-artifacts.js +0 -608
  116. package/scripts/optimize-context.js +0 -17
  117. package/scripts/org-dashboard.js +0 -206
  118. package/scripts/partner-orchestration.js +0 -146
  119. package/scripts/predictive-insights.js +0 -356
  120. package/scripts/pulse.js +0 -80
  121. package/scripts/reflector-agent.js +0 -221
  122. package/scripts/sales-pipeline.js +0 -681
  123. package/scripts/session-episode-store.js +0 -329
  124. package/scripts/session-health-sensor.js +0 -242
  125. package/scripts/session-report.js +0 -120
  126. package/scripts/swarm-coordinator.js +0 -81
  127. package/scripts/tool-kpi-tracker.js +0 -12
  128. package/scripts/webhook-delivery.js +0 -62
  129. package/scripts/workflow-sprint-intake.js +0 -475
@@ -1,81 +0,0 @@
1
- 'use strict';
2
-
3
- const { constructContextPack, recordProvenance } = require('./contextfs');
4
-
5
- const DEFAULT_TTL_MS = 15 * 60 * 1000;
6
- const MAX_AGENTS = 32;
7
-
8
- function normalizeAgents(agents) {
9
- if (!Array.isArray(agents)) {
10
- throw new Error('agents must be a non-empty array of agent names');
11
- }
12
- const normalized = [];
13
- const seen = new Set();
14
- for (const raw of agents) {
15
- const name = typeof raw === 'string' ? raw.trim() : String((raw && raw.name) || '').trim();
16
- if (!name) continue;
17
- if (seen.has(name)) continue;
18
- seen.add(name);
19
- normalized.push(name);
20
- }
21
- if (normalized.length === 0) {
22
- throw new Error('agents must include at least one named agent');
23
- }
24
- if (normalized.length > MAX_AGENTS) {
25
- throw new Error(`agents list exceeds MAX_AGENTS (${MAX_AGENTS})`);
26
- }
27
- return normalized;
28
- }
29
-
30
- function distributeContextToAgents({
31
- query = '',
32
- agents,
33
- maxItems,
34
- maxChars,
35
- namespaces,
36
- ttlMs,
37
- } = {}) {
38
- const agentNames = normalizeAgents(agents);
39
- const ttl = Number.isFinite(Number(ttlMs)) && Number(ttlMs) > 0 ? Number(ttlMs) : DEFAULT_TTL_MS;
40
- const expiresAt = new Date(Date.now() + ttl).toISOString();
41
-
42
- const pack = constructContextPack({
43
- query,
44
- maxItems: Number.isFinite(Number(maxItems)) && Number(maxItems) > 0 ? Number(maxItems) : undefined,
45
- maxChars: Number.isFinite(Number(maxChars)) && Number(maxChars) > 0 ? Number(maxChars) : undefined,
46
- namespaces: Array.isArray(namespaces) ? namespaces : [],
47
- });
48
-
49
- const itemCount = Array.isArray(pack.items) ? pack.items.length : 0;
50
- const distributions = agentNames.map((agent) => {
51
- const provenance = recordProvenance({
52
- type: 'context_pack_distributed',
53
- packId: pack.packId,
54
- agent,
55
- query: pack.query,
56
- itemCount,
57
- expiresAt,
58
- });
59
- return {
60
- agent,
61
- packId: pack.packId,
62
- provenanceId: provenance.id,
63
- expiresAt,
64
- };
65
- });
66
-
67
- return {
68
- packId: pack.packId,
69
- query: pack.query,
70
- totalAgents: distributions.length,
71
- itemCount,
72
- expiresAt,
73
- distributions,
74
- };
75
- }
76
-
77
- module.exports = {
78
- distributeContextToAgents,
79
- DEFAULT_TTL_MS,
80
- MAX_AGENTS,
81
- };
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { resolveFeedbackDir } = require('./feedback-paths');
6
- const { readJsonl } = require('./fs-utils');
7
- function getKpiLogPath() { return path.join(resolveFeedbackDir(), 'tool-kpi.jsonl'); }
8
- function recordToolCall({ toolName, serverName, latencyMs, success, agentId, metadata } = {}) { const lp = getKpiLogPath(); const dir = path.dirname(lp); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const e = { id: `kpi_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, timestamp: new Date().toISOString(), toolName: toolName || 'unknown', serverName: serverName || 'default', latencyMs: typeof latencyMs === 'number' ? latencyMs : 0, success: success !== false, agentId: agentId || 'unknown', metadata: metadata || {} }; fs.appendFileSync(lp, JSON.stringify(e) + '\n'); return e; }
9
- function percentile(sorted, p) { if (sorted.length === 0) return 0; const idx = Math.ceil((p / 100) * sorted.length) - 1; return sorted[Math.max(0, idx)]; }
10
- function computeToolKpis({ periodHours = 24 } = {}) { const entries = readJsonl(getKpiLogPath()); const cutoff = Date.now() - periodHours * 60 * 60 * 1000; const recent = entries.filter((e) => new Date(e.timestamp).getTime() > cutoff); const byTool = {}; for (const e of recent) { const k = e.toolName; if (!byTool[k]) byTool[k] = { toolName: k, calls: [], successes: 0, failures: 0 }; byTool[k].calls.push(e.latencyMs); if (e.success) byTool[k].successes++; else byTool[k].failures++; } const tools = Object.values(byTool).map((t) => { const sorted = t.calls.slice().sort((a, b) => a - b); const total = t.successes + t.failures; return { toolName: t.toolName, requestCount: total, successRate: total > 0 ? Math.round((t.successes / total) * 1000) / 10 : 100, p50: Math.round(percentile(sorted, 50)), p90: Math.round(percentile(sorted, 90)), p95: Math.round(percentile(sorted, 95)), successes: t.successes, failures: t.failures }; }).sort((a, b) => b.requestCount - a.requestCount); const byServer = {}; for (const e of recent) { const k = e.serverName; if (!byServer[k]) byServer[k] = { serverName: k, total: 0, successes: 0 }; byServer[k].total++; if (e.success) byServer[k].successes++; } const servers = Object.values(byServer).map((s) => ({ serverName: s.serverName, totalCalls: s.total, successRate: s.total > 0 ? Math.round((s.successes / s.total) * 1000) / 10 : 100 })); return { periodHours, totalCalls: recent.length, tools, servers }; }
11
- function getAtRiskTools({ successRateThreshold = 90, p95Threshold = 500, periodHours = 24 } = {}) { const { tools } = computeToolKpis({ periodHours }); return tools.filter((t) => t.requestCount >= 3 && (t.successRate < successRateThreshold || t.p95 > p95Threshold)); }
12
- module.exports = { recordToolCall, computeToolKpis, getAtRiskTools, percentile, getKpiLogPath };
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const https = require('https');
5
- const http = require('http');
6
-
7
- function sendWebhook(webhookUrl, payload) {
8
- return new Promise((resolve, reject) => {
9
- const url = new URL(webhookUrl);
10
- const mod = url.protocol === 'https:' ? https : http;
11
- const body = JSON.stringify(payload);
12
-
13
- const req = mod.request(url, {
14
- method: 'POST',
15
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
16
- timeout: 10000,
17
- }, (res) => {
18
- let data = '';
19
- res.on('data', (c) => { data += c; });
20
- res.on('end', () => resolve({ status: res.statusCode, body: data }));
21
- });
22
-
23
- req.on('error', reject);
24
- req.on('timeout', () => { req.destroy(); reject(new Error('Webhook timeout')); });
25
- req.write(body);
26
- req.end();
27
- });
28
- }
29
-
30
- async function deliverToTeams(webhookUrl, title, message) {
31
- return sendWebhook(webhookUrl, {
32
- '@type': 'MessageCard',
33
- '@context': 'http://schema.org/extensions',
34
- summary: title,
35
- themeColor: '0076D7',
36
- title,
37
- text: message,
38
- });
39
- }
40
-
41
- async function deliverToSlack(webhookUrl, title, message) {
42
- return sendWebhook(webhookUrl, {
43
- text: `*${title}*\n${message}`,
44
- });
45
- }
46
-
47
- async function deliverToDiscord(webhookUrl, title, message) {
48
- return sendWebhook(webhookUrl, {
49
- embeds: [{ title, description: message.substring(0, 4096), color: 0x0076D7 }],
50
- });
51
- }
52
-
53
- async function deliver(platform, webhookUrl, title, message) {
54
- switch (platform) {
55
- case 'teams': return deliverToTeams(webhookUrl, title, message);
56
- case 'slack': return deliverToSlack(webhookUrl, title, message);
57
- case 'discord': return deliverToDiscord(webhookUrl, title, message);
58
- default: return sendWebhook(webhookUrl, { title, message });
59
- }
60
- }
61
-
62
- module.exports = { deliver, deliverToTeams, deliverToSlack, deliverToDiscord, sendWebhook };
@@ -1,475 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- const { getFeedbackPaths } = require('./feedback-loop');
8
- const { appendWorkflowRun } = require('./workflow-runs');
9
-
10
- const WORKFLOW_SPRINT_LEADS_FILE = 'workflow-sprint-leads.jsonl';
11
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
- const WORKFLOW_SPRINT_STATUS_FLOW = [
13
- 'new',
14
- 'qualified',
15
- 'named_pilot',
16
- 'proof_backed_run',
17
- 'paid_team',
18
- ];
19
-
20
- function normalizeText(value, maxLength = 280) {
21
- if (value === undefined || value === null) return null;
22
- const text = String(value).trim();
23
- if (!text) return null;
24
- return text.slice(0, maxLength);
25
- }
26
-
27
- function normalizeEmail(value) {
28
- const email = normalizeText(value, 320);
29
- if (!email) return null;
30
- const normalized = email.toLowerCase();
31
- return EMAIL_PATTERN.test(normalized) ? normalized : null;
32
- }
33
-
34
- function normalizeProofArtifacts(value) {
35
- if (!Array.isArray(value)) return [];
36
- return value
37
- .map((entry) => normalizeText(entry, 512))
38
- .filter(Boolean);
39
- }
40
-
41
- function normalizeWorkflowSprintStatus(value, fallback = null) {
42
- const normalized = normalizeText(value, 64);
43
- if (!normalized) return fallback;
44
- if (WORKFLOW_SPRINT_STATUS_FLOW.includes(normalized)) {
45
- return normalized;
46
- }
47
- return fallback;
48
- }
49
-
50
- function slugify(value, fallback = 'workflow_sprint') {
51
- const normalized = normalizeText(value, 120);
52
- if (!normalized) return fallback;
53
- const slug = normalized
54
- .toLowerCase()
55
- .replace(/[^a-z0-9]+/g, '_')
56
- .replace(/^_+|_+$/g, '');
57
- return slug || fallback;
58
- }
59
-
60
- function buildStatusHistoryEntry({
61
- fromStatus = null,
62
- toStatus,
63
- actor = null,
64
- note = null,
65
- reviewedBy = null,
66
- proofArtifacts = [],
67
- timestamp = new Date().toISOString(),
68
- } = {}) {
69
- return {
70
- fromStatus: normalizeWorkflowSprintStatus(fromStatus, null),
71
- toStatus: normalizeWorkflowSprintStatus(toStatus, 'new'),
72
- at: normalizeText(timestamp, 64) || new Date().toISOString(),
73
- actor: normalizeText(actor, 160),
74
- note: normalizeText(note, 1000),
75
- reviewedBy: normalizeText(reviewedBy, 160),
76
- proofArtifacts: normalizeProofArtifacts(proofArtifacts),
77
- };
78
- }
79
-
80
- function sanitizeWorkflowSprintLead(entry = {}) {
81
- const submittedAt = normalizeText(entry.submittedAt, 64) || new Date().toISOString();
82
- const updatedAt = normalizeText(entry.updatedAt, 64) || submittedAt;
83
- const status = normalizeWorkflowSprintStatus(entry.status, 'new');
84
- const proofArtifacts = normalizeProofArtifacts(entry.proof && entry.proof.artifacts);
85
- const reviewedBy = normalizeText(entry.proof && entry.proof.reviewedBy, 160);
86
- const history = Array.isArray(entry.statusHistory) && entry.statusHistory.length
87
- ? entry.statusHistory
88
- .map((item) => buildStatusHistoryEntry(item))
89
- .filter(Boolean)
90
- : [buildStatusHistoryEntry({
91
- fromStatus: null,
92
- toStatus: status,
93
- timestamp: updatedAt,
94
- actor: entry.actor || null,
95
- note: entry.statusNote || null,
96
- reviewedBy,
97
- proofArtifacts,
98
- })];
99
-
100
- return {
101
- leadId: normalizeText(entry.leadId, 160) || `lead_${Date.now().toString(36)}_${crypto.randomBytes(4).toString('hex')}`,
102
- submittedAt,
103
- updatedAt,
104
- status,
105
- offer: normalizeText(entry.offer, 120) || 'workflow_hardening_sprint',
106
- contact: {
107
- email: normalizeEmail(entry.contact && entry.contact.email),
108
- company: normalizeText(entry.contact && entry.contact.company, 160),
109
- },
110
- qualification: {
111
- workflow: normalizeText(entry.qualification && entry.qualification.workflow, 240),
112
- owner: normalizeText(entry.qualification && entry.qualification.owner, 160),
113
- blocker: normalizeText(entry.qualification && entry.qualification.blocker, 1000),
114
- runtime: normalizeText(entry.qualification && entry.qualification.runtime, 160),
115
- note: normalizeText(entry.qualification && entry.qualification.note, 1000),
116
- },
117
- attribution: {
118
- acquisitionId: normalizeText(entry.attribution && entry.attribution.acquisitionId, 160),
119
- visitorId: normalizeText(entry.attribution && entry.attribution.visitorId, 160),
120
- sessionId: normalizeText(entry.attribution && entry.attribution.sessionId, 160),
121
- traceId: normalizeText(entry.attribution && entry.attribution.traceId, 160),
122
- installId: normalizeText(entry.attribution && entry.attribution.installId, 160),
123
- source: normalizeText(entry.attribution && entry.attribution.source, 120),
124
- utmSource: normalizeText(entry.attribution && entry.attribution.utmSource, 120),
125
- utmMedium: normalizeText(entry.attribution && entry.attribution.utmMedium, 120),
126
- utmCampaign: normalizeText(entry.attribution && entry.attribution.utmCampaign, 160),
127
- utmContent: normalizeText(entry.attribution && entry.attribution.utmContent, 160),
128
- utmTerm: normalizeText(entry.attribution && entry.attribution.utmTerm, 160),
129
- creator: normalizeText(entry.attribution && entry.attribution.creator, 120),
130
- community: normalizeText(entry.attribution && entry.attribution.community, 120),
131
- postId: normalizeText(entry.attribution && entry.attribution.postId, 120),
132
- commentId: normalizeText(entry.attribution && entry.attribution.commentId, 120),
133
- campaignVariant: normalizeText(entry.attribution && entry.attribution.campaignVariant, 120),
134
- offerCode: normalizeText(entry.attribution && entry.attribution.offerCode, 120),
135
- ctaId: normalizeText(entry.attribution && entry.attribution.ctaId, 120),
136
- ctaPlacement: normalizeText(entry.attribution && entry.attribution.ctaPlacement, 120),
137
- planId: normalizeText(entry.attribution && entry.attribution.planId, 120),
138
- page: normalizeText(entry.attribution && entry.attribution.page, 160),
139
- landingPath: normalizeText(entry.attribution && entry.attribution.landingPath, 160),
140
- referrerHost: normalizeText(entry.attribution && entry.attribution.referrerHost, 255),
141
- referrer: normalizeText(entry.attribution && entry.attribution.referrer, 255),
142
- },
143
- workflowProgress: {
144
- qualifiedAt: normalizeText(entry.workflowProgress && entry.workflowProgress.qualifiedAt, 64),
145
- namedPilotAt: normalizeText(entry.workflowProgress && entry.workflowProgress.namedPilotAt, 64),
146
- proofBackedRunAt: normalizeText(entry.workflowProgress && entry.workflowProgress.proofBackedRunAt, 64),
147
- paidTeamAt: normalizeText(entry.workflowProgress && entry.workflowProgress.paidTeamAt, 64),
148
- },
149
- proof: {
150
- artifacts: proofArtifacts,
151
- reviewedBy,
152
- lastWorkflowRunKey: normalizeText(entry.proof && entry.proof.lastWorkflowRunKey, 240),
153
- },
154
- statusHistory: history,
155
- };
156
- }
157
-
158
- function appendWorkflowSprintLeadSnapshot(lead = {}, feedbackDir) {
159
- const sanitized = sanitizeWorkflowSprintLead(lead);
160
- const leadsPath = getWorkflowSprintLeadsPath(feedbackDir);
161
- fs.mkdirSync(path.dirname(leadsPath), { recursive: true });
162
- fs.appendFileSync(leadsPath, `${JSON.stringify(sanitized)}\n`, 'utf8');
163
- return sanitized;
164
- }
165
-
166
- function getWorkflowSprintLeadsPath(feedbackDir) {
167
- const baseDir = feedbackDir || getFeedbackPaths().FEEDBACK_DIR;
168
- return path.join(baseDir, WORKFLOW_SPRINT_LEADS_FILE);
169
- }
170
-
171
- function buildWorkflowSprintLead(payload = {}) {
172
- const email = normalizeEmail(payload.email);
173
- const workflow = normalizeText(payload.workflow, 240);
174
- const owner = normalizeText(payload.owner, 160);
175
- const blocker = normalizeText(payload.blocker, 1000);
176
- const runtime = normalizeText(payload.runtime, 160);
177
-
178
- if (!email) {
179
- const err = new Error('A valid email address is required.');
180
- err.statusCode = 400;
181
- throw err;
182
- }
183
- if (!workflow) {
184
- const err = new Error('Workflow is required.');
185
- err.statusCode = 400;
186
- throw err;
187
- }
188
- if (!owner) {
189
- const err = new Error('Workflow owner is required.');
190
- err.statusCode = 400;
191
- throw err;
192
- }
193
- if (!blocker) {
194
- const err = new Error('Repeated failure or rollout blocker is required.');
195
- err.statusCode = 400;
196
- throw err;
197
- }
198
- if (!runtime) {
199
- const err = new Error('Current agent or runtime is required.');
200
- err.statusCode = 400;
201
- throw err;
202
- }
203
-
204
- const submittedAt = normalizeText(payload.submittedAt, 64) || new Date().toISOString();
205
-
206
- return {
207
- leadId: `lead_${Date.now().toString(36)}_${crypto.randomBytes(4).toString('hex')}`,
208
- submittedAt,
209
- updatedAt: submittedAt,
210
- status: 'new',
211
- offer: 'workflow_hardening_sprint',
212
- contact: {
213
- email,
214
- company: normalizeText(payload.company, 160),
215
- },
216
- qualification: {
217
- workflow,
218
- owner,
219
- blocker,
220
- runtime,
221
- note: normalizeText(payload.note, 1000),
222
- },
223
- attribution: {
224
- acquisitionId: normalizeText(payload.acquisitionId, 160),
225
- visitorId: normalizeText(payload.visitorId, 160),
226
- sessionId: normalizeText(payload.sessionId, 160),
227
- traceId: normalizeText(payload.traceId, 160),
228
- installId: normalizeText(payload.installId, 160),
229
- source: normalizeText(payload.source, 120),
230
- utmSource: normalizeText(payload.utmSource, 120),
231
- utmMedium: normalizeText(payload.utmMedium, 120),
232
- utmCampaign: normalizeText(payload.utmCampaign, 160),
233
- utmContent: normalizeText(payload.utmContent, 160),
234
- utmTerm: normalizeText(payload.utmTerm, 160),
235
- creator: normalizeText(payload.creator, 120),
236
- community: normalizeText(payload.community, 120),
237
- postId: normalizeText(payload.postId, 120),
238
- commentId: normalizeText(payload.commentId, 120),
239
- campaignVariant: normalizeText(payload.campaignVariant, 120),
240
- offerCode: normalizeText(payload.offerCode, 120),
241
- ctaId: normalizeText(payload.ctaId, 120),
242
- ctaPlacement: normalizeText(payload.ctaPlacement, 120),
243
- planId: normalizeText(payload.planId, 120),
244
- page: normalizeText(payload.page, 160),
245
- landingPath: normalizeText(payload.landingPath, 160),
246
- referrerHost: normalizeText(payload.referrerHost, 255),
247
- referrer: normalizeText(payload.referrer, 255),
248
- },
249
- workflowProgress: {
250
- qualifiedAt: null,
251
- namedPilotAt: null,
252
- proofBackedRunAt: null,
253
- paidTeamAt: null,
254
- },
255
- proof: {
256
- artifacts: [],
257
- reviewedBy: null,
258
- lastWorkflowRunKey: null,
259
- },
260
- statusHistory: [
261
- buildStatusHistoryEntry({
262
- fromStatus: null,
263
- toStatus: 'new',
264
- actor: normalizeText(payload.actor, 160) || 'website',
265
- note: normalizeText(payload.note, 1000),
266
- timestamp: submittedAt,
267
- }),
268
- ],
269
- };
270
- }
271
-
272
- function appendWorkflowSprintLead(payload = {}, { feedbackDir } = {}) {
273
- const lead = buildWorkflowSprintLead(payload);
274
- return appendWorkflowSprintLeadSnapshot(lead, feedbackDir);
275
- }
276
-
277
- function loadWorkflowSprintLeadSnapshots(feedbackDir) {
278
- const leadsPath = getWorkflowSprintLeadsPath(feedbackDir);
279
- if (!fs.existsSync(leadsPath)) return [];
280
- const raw = fs.readFileSync(leadsPath, 'utf8').trim();
281
- if (!raw) return [];
282
- return raw
283
- .split('\n')
284
- .map((line) => line.trim())
285
- .filter(Boolean)
286
- .map((line) => {
287
- try {
288
- return sanitizeWorkflowSprintLead(JSON.parse(line));
289
- } catch {
290
- return null;
291
- }
292
- })
293
- .filter(Boolean);
294
- }
295
-
296
- function loadWorkflowSprintLeads(feedbackDir) {
297
- const latestByLeadId = new Map();
298
- for (const snapshot of loadWorkflowSprintLeadSnapshots(feedbackDir)) {
299
- const existing = latestByLeadId.get(snapshot.leadId);
300
- if (!existing || String(snapshot.updatedAt || '') >= String(existing.updatedAt || '')) {
301
- latestByLeadId.set(snapshot.leadId, snapshot);
302
- }
303
- }
304
- return Array.from(latestByLeadId.values())
305
- .sort((a, b) => String(a.submittedAt || '').localeCompare(String(b.submittedAt || '')));
306
- }
307
-
308
- function getWorkflowSprintLeadById(leadId, feedbackDir) {
309
- const normalizedLeadId = normalizeText(leadId, 160);
310
- if (!normalizedLeadId) return null;
311
- return loadWorkflowSprintLeads(feedbackDir)
312
- .find((entry) => entry.leadId === normalizedLeadId) || null;
313
- }
314
-
315
- function resolveLeadTeamId(lead = {}, overrideTeamId = null) {
316
- const explicitTeamId = normalizeText(overrideTeamId, 160);
317
- if (explicitTeamId) return explicitTeamId;
318
- const companySlug = slugify(lead.contact && lead.contact.company, '');
319
- if (companySlug) return companySlug;
320
- const email = normalizeEmail(lead.contact && lead.contact.email);
321
- if (email && email.includes('@')) {
322
- return slugify(email.split('@')[1], 'workflow_sprint_team');
323
- }
324
- return slugify(lead.qualification && lead.qualification.owner, 'workflow_sprint_team');
325
- }
326
-
327
- function appendWorkflowRunForSprintTransition(lead, {
328
- status,
329
- reviewedBy,
330
- proofArtifacts,
331
- workflowId,
332
- teamId,
333
- timestamp,
334
- } = {}, feedbackDir) {
335
- if (status !== 'named_pilot' && status !== 'proof_backed_run' && status !== 'paid_team') {
336
- return null;
337
- }
338
- const normalizedArtifacts = normalizeProofArtifacts(proofArtifacts);
339
- const normalizedReviewedBy = normalizeText(reviewedBy, 160);
340
- const workflowRun = appendWorkflowRun({
341
- timestamp: normalizeText(timestamp, 64) || new Date().toISOString(),
342
- workflowId: normalizeText(workflowId, 160) || slugify(lead.qualification.workflow, 'workflow_hardening_sprint'),
343
- workflowName: lead.qualification.workflow,
344
- owner: lead.qualification.owner,
345
- runtime: lead.qualification.runtime,
346
- status: status === 'named_pilot' ? 'in_progress' : 'passed',
347
- customerType: status === 'paid_team' ? 'paid_team' : 'named_pilot',
348
- teamId: resolveLeadTeamId(lead, teamId),
349
- reviewed: Boolean(normalizedReviewedBy || normalizedArtifacts.length > 0),
350
- reviewedBy: normalizedReviewedBy,
351
- proofBacked: status === 'proof_backed_run' || status === 'paid_team',
352
- proofArtifacts: normalizedArtifacts,
353
- source: `workflow_sprint:${status}`,
354
- metadata: {
355
- leadId: lead.leadId,
356
- pipelineStatus: status,
357
- offer: lead.offer,
358
- company: lead.contact && lead.contact.company ? lead.contact.company : null,
359
- },
360
- }, feedbackDir);
361
- return {
362
- ...workflowRun,
363
- workflowRunKey: `${workflowRun.workflowId}@${workflowRun.timestamp}`,
364
- };
365
- }
366
-
367
- function advanceWorkflowSprintLead(payload = {}, { feedbackDir } = {}) {
368
- const leadId = normalizeText(payload.leadId, 160);
369
- const nextStatus = normalizeWorkflowSprintStatus(payload.status, null);
370
- const actor = normalizeText(payload.actor, 160) || 'admin';
371
- const note = normalizeText(payload.note, 1000);
372
- const reviewedBy = normalizeText(payload.reviewedBy, 160);
373
- const proofArtifacts = normalizeProofArtifacts(payload.proofArtifacts);
374
- const workflowId = normalizeText(payload.workflowId, 160);
375
- const teamId = normalizeText(payload.teamId, 160);
376
-
377
- if (!leadId) {
378
- const err = new Error('leadId is required.');
379
- err.statusCode = 400;
380
- throw err;
381
- }
382
- if (!nextStatus) {
383
- const err = new Error(`status must be one of: ${WORKFLOW_SPRINT_STATUS_FLOW.join(', ')}`);
384
- err.statusCode = 400;
385
- throw err;
386
- }
387
-
388
- const currentLead = getWorkflowSprintLeadById(leadId, feedbackDir);
389
- if (!currentLead) {
390
- const err = new Error(`Unknown workflow sprint lead: ${leadId}`);
391
- err.statusCode = 404;
392
- throw err;
393
- }
394
-
395
- if (currentLead.status === nextStatus) {
396
- return {
397
- lead: currentLead,
398
- workflowRun: null,
399
- unchanged: true,
400
- };
401
- }
402
-
403
- const currentIndex = WORKFLOW_SPRINT_STATUS_FLOW.indexOf(currentLead.status);
404
- const nextIndex = WORKFLOW_SPRINT_STATUS_FLOW.indexOf(nextStatus);
405
- if (nextIndex !== currentIndex + 1) {
406
- const err = new Error(`Invalid workflow sprint transition: ${currentLead.status} -> ${nextStatus}`);
407
- err.statusCode = 400;
408
- throw err;
409
- }
410
- if (nextStatus === 'proof_backed_run' && !reviewedBy && proofArtifacts.length === 0) {
411
- const err = new Error('proof_backed_run requires reviewedBy or proofArtifacts.');
412
- err.statusCode = 400;
413
- throw err;
414
- }
415
-
416
- const transitionAt = new Date().toISOString();
417
- const workflowProgress = {
418
- ...currentLead.workflowProgress,
419
- };
420
- if (nextStatus === 'qualified') workflowProgress.qualifiedAt = transitionAt;
421
- if (nextStatus === 'named_pilot') workflowProgress.namedPilotAt = transitionAt;
422
- if (nextStatus === 'proof_backed_run') workflowProgress.proofBackedRunAt = transitionAt;
423
- if (nextStatus === 'paid_team') workflowProgress.paidTeamAt = transitionAt;
424
-
425
- const workflowRun = appendWorkflowRunForSprintTransition(currentLead, {
426
- status: nextStatus,
427
- reviewedBy,
428
- proofArtifacts,
429
- workflowId,
430
- teamId,
431
- timestamp: transitionAt,
432
- }, feedbackDir);
433
-
434
- const updatedLead = appendWorkflowSprintLeadSnapshot({
435
- ...currentLead,
436
- updatedAt: transitionAt,
437
- status: nextStatus,
438
- workflowProgress,
439
- proof: {
440
- artifacts: proofArtifacts.length ? proofArtifacts : currentLead.proof.artifacts,
441
- reviewedBy: reviewedBy || currentLead.proof.reviewedBy,
442
- lastWorkflowRunKey: workflowRun ? workflowRun.workflowRunKey : currentLead.proof.lastWorkflowRunKey,
443
- },
444
- statusHistory: currentLead.statusHistory.concat(buildStatusHistoryEntry({
445
- fromStatus: currentLead.status,
446
- toStatus: nextStatus,
447
- actor,
448
- note,
449
- reviewedBy,
450
- proofArtifacts,
451
- timestamp: transitionAt,
452
- })),
453
- }, feedbackDir);
454
-
455
- return {
456
- lead: updatedLead,
457
- workflowRun,
458
- unchanged: false,
459
- };
460
- }
461
-
462
- module.exports = {
463
- WORKFLOW_SPRINT_LEADS_FILE,
464
- WORKFLOW_SPRINT_STATUS_FLOW,
465
- buildWorkflowSprintLead,
466
- appendWorkflowSprintLead,
467
- appendWorkflowSprintLeadSnapshot,
468
- advanceWorkflowSprintLead,
469
- loadWorkflowSprintLeads,
470
- loadWorkflowSprintLeadSnapshots,
471
- getWorkflowSprintLeadById,
472
- getWorkflowSprintLeadsPath,
473
- sanitizeWorkflowSprintLead,
474
- normalizeWorkflowSprintStatus,
475
- };