thumbgate 1.14.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +60 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +217 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +211 -8
  12. package/config/enforcement.json +59 -7
  13. package/config/evals/agent-safety-eval.json +338 -22
  14. package/config/gates/default.json +33 -0
  15. package/config/gates/routine.json +43 -0
  16. package/config/github-about.json +3 -3
  17. package/config/mcp-allowlists.json +4 -0
  18. package/config/merge-quality-checks.json +2 -1
  19. package/config/model-candidates.json +131 -0
  20. package/openapi/openapi.yaml +118 -2
  21. package/package.json +70 -51
  22. package/public/blog.html +7 -7
  23. package/public/codex-plugin.html +13 -7
  24. package/public/compare.html +29 -23
  25. package/public/dashboard.html +105 -12
  26. package/public/guide.html +28 -28
  27. package/public/index.html +233 -97
  28. package/public/learn.html +87 -20
  29. package/public/lessons.html +26 -2
  30. package/public/numbers.html +271 -0
  31. package/public/pro.html +89 -19
  32. package/scripts/agent-audit-trace.js +55 -0
  33. package/scripts/agent-memory-lifecycle.js +96 -0
  34. package/scripts/agent-readiness-plan.js +118 -0
  35. package/scripts/agentic-data-pipeline.js +21 -1
  36. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  37. package/scripts/ai-org-governance.js +98 -0
  38. package/scripts/ai-search-distribution.js +43 -0
  39. package/scripts/artifact-agent-plan.js +81 -0
  40. package/scripts/billing.js +27 -8
  41. package/scripts/cli-feedback.js +2 -1
  42. package/scripts/cli-schema.js +60 -5
  43. package/scripts/code-mode-mcp-plan.js +71 -0
  44. package/scripts/commercial-offer.js +1 -1
  45. package/scripts/context-engine.js +1 -2
  46. package/scripts/context-manager.js +4 -1
  47. package/scripts/contextfs.js +214 -32
  48. package/scripts/dashboard-render-spec.js +1 -1
  49. package/scripts/dashboard.js +275 -9
  50. package/scripts/decision-journal.js +13 -3
  51. package/scripts/document-workflow-governance.js +62 -0
  52. package/scripts/enterprise-agent-rollout.js +34 -0
  53. package/scripts/experience-replay-governance.js +69 -0
  54. package/scripts/export-hf-dataset.js +1 -1
  55. package/scripts/feedback-loop.js +141 -9
  56. package/scripts/feedback-to-rules.js +17 -23
  57. package/scripts/gates-engine.js +4 -6
  58. package/scripts/growth-campaigns.js +49 -0
  59. package/scripts/harness-selector.js +145 -1
  60. package/scripts/hybrid-supervisor-agent.js +64 -0
  61. package/scripts/inference-cache-policy.js +72 -0
  62. package/scripts/inference-economics.js +53 -0
  63. package/scripts/internal-agent-bootstrap.js +12 -2
  64. package/scripts/knowledge-layer-plan.js +108 -0
  65. package/scripts/lesson-canonical.js +181 -0
  66. package/scripts/lesson-db.js +71 -10
  67. package/scripts/lesson-inference.js +183 -44
  68. package/scripts/lesson-search.js +4 -1
  69. package/scripts/lesson-synthesis.js +23 -2
  70. package/scripts/llm-client.js +157 -26
  71. package/scripts/mailer/resend-mailer.js +112 -1
  72. package/scripts/mcp-transport-strategy.js +66 -0
  73. package/scripts/memory-store-governance.js +60 -0
  74. package/scripts/meta-agent-loop.js +7 -13
  75. package/scripts/model-access-eligibility.js +38 -0
  76. package/scripts/model-migration-readiness.js +55 -0
  77. package/scripts/native-messaging-audit.js +514 -0
  78. package/scripts/operational-integrity.js +96 -3
  79. package/scripts/otel-declarative-config.js +56 -0
  80. package/scripts/perplexity-client.js +1 -1
  81. package/scripts/post-training-governance.js +34 -0
  82. package/scripts/pr-manager.js +47 -7
  83. package/scripts/private-core-boundary.js +72 -0
  84. package/scripts/production-agent-readiness.js +40 -0
  85. package/scripts/profile-router.js +16 -1
  86. package/scripts/prompt-eval.js +564 -32
  87. package/scripts/prompt-programs.js +93 -0
  88. package/scripts/provider-action-normalizer.js +585 -0
  89. package/scripts/rule-validator.js +285 -0
  90. package/scripts/scaling-law-claims.js +60 -0
  91. package/scripts/security-scanner.js +1 -1
  92. package/scripts/self-distill-agent.js +7 -32
  93. package/scripts/seo-gsd.js +400 -43
  94. package/scripts/skill-rag-router.js +53 -0
  95. package/scripts/spec-gate.js +1 -1
  96. package/scripts/student-consistent-training.js +73 -0
  97. package/scripts/synthetic-data-provenance.js +98 -0
  98. package/scripts/task-context-result.js +81 -0
  99. package/scripts/telemetry-analytics.js +149 -0
  100. package/scripts/thompson-sampling.js +2 -2
  101. package/scripts/token-savings.js +7 -6
  102. package/scripts/token-tco.js +46 -0
  103. package/scripts/tool-registry.js +75 -3
  104. package/scripts/verification-loop.js +10 -1
  105. package/scripts/verifier-scoring.js +71 -0
  106. package/scripts/workflow-sentinel.js +284 -28
  107. package/scripts/workspace-agent-routines.js +118 -0
  108. package/skills/thumbgate/SKILL.md +1 -1
  109. package/src/api/server.js +434 -120
  110. package/.claude-plugin/README.md +0 -170
  111. package/adapters/README.md +0 -12
  112. package/scripts/analytics-report.js +0 -328
  113. package/scripts/autonomous-workflow.js +0 -377
  114. package/scripts/billing-setup.js +0 -109
  115. package/scripts/creator-campaigns.js +0 -239
  116. package/scripts/cross-encoder-reranker.js +0 -235
  117. package/scripts/daemon-manager.js +0 -108
  118. package/scripts/decision-trace.js +0 -354
  119. package/scripts/delegation-runtime.js +0 -896
  120. package/scripts/dispatch-brief.js +0 -159
  121. package/scripts/distribution-surfaces.js +0 -110
  122. package/scripts/feedback-history-distiller.js +0 -382
  123. package/scripts/funnel-analytics.js +0 -35
  124. package/scripts/history-distiller.js +0 -200
  125. package/scripts/hosted-job-launcher.js +0 -256
  126. package/scripts/intent-router.js +0 -392
  127. package/scripts/lesson-reranker.js +0 -263
  128. package/scripts/lesson-retrieval.js +0 -148
  129. package/scripts/managed-lesson-agent.js +0 -183
  130. package/scripts/operational-dashboard.js +0 -103
  131. package/scripts/operational-summary.js +0 -129
  132. package/scripts/operator-artifacts.js +0 -608
  133. package/scripts/optimize-context.js +0 -17
  134. package/scripts/org-dashboard.js +0 -206
  135. package/scripts/partner-orchestration.js +0 -146
  136. package/scripts/predictive-insights.js +0 -356
  137. package/scripts/pulse.js +0 -80
  138. package/scripts/reflector-agent.js +0 -221
  139. package/scripts/sales-pipeline.js +0 -681
  140. package/scripts/session-episode-store.js +0 -329
  141. package/scripts/session-health-sensor.js +0 -242
  142. package/scripts/session-report.js +0 -120
  143. package/scripts/swarm-coordinator.js +0 -81
  144. package/scripts/tool-kpi-tracker.js +0 -12
  145. package/scripts/webhook-delivery.js +0 -62
  146. package/scripts/workflow-sprint-intake.js +0 -475
  147. package/skills/agent-memory/SKILL.md +0 -97
  148. package/skills/solve-architecture-autonomy/SKILL.md +0 -17
  149. package/skills/solve-architecture-autonomy/tool.js +0 -33
  150. package/skills/thumbgate-feedback/SKILL.md +0 -49
@@ -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
- };