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.
- 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 +59 -35
- package/adapters/chatgpt/openapi.yaml +118 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +210 -84
- package/adapters/opencode/opencode.json +1 -1
- package/bench/prompt-eval-suite.json +5 -1
- package/bin/cli.js +157 -8
- package/config/evals/agent-safety-eval.json +338 -22
- package/config/gates/routine.json +43 -0
- package/config/github-about.json +3 -3
- package/config/model-candidates.json +131 -0
- package/openapi/openapi.yaml +118 -2
- package/package.json +55 -48
- package/public/blog.html +7 -7
- package/public/codex-plugin.html +6 -6
- package/public/compare.html +29 -23
- package/public/dashboard.html +82 -10
- package/public/guide.html +28 -28
- package/public/index.html +216 -98
- package/public/learn.html +50 -22
- package/public/lessons.html +1 -1
- package/public/numbers.html +17 -17
- package/public/pro.html +82 -18
- 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-schema.js +18 -2
- package/scripts/code-mode-mcp-plan.js +71 -0
- package/scripts/context-engine.js +1 -2
- package/scripts/context-manager.js +4 -1
- 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 +92 -4
- 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 +16 -4
- 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-inference.js +183 -44
- package/scripts/lesson-search.js +4 -1
- 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/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/private-core-boundary.js +72 -0
- package/scripts/production-agent-readiness.js +40 -0
- package/scripts/prompt-eval.js +564 -32
- package/scripts/prompt-programs.js +93 -0
- package/scripts/provider-action-normalizer.js +585 -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 +232 -55
- 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 +63 -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/src/api/server.js +381 -120
- 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
|
@@ -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
|
-
};
|