Skip to Content
Code ReviewsCode Duplication & Inefficiency Audit v2 — Leadmetrics v3

Code Duplication & Inefficiency Audit v2 — Leadmetrics v3

Date: 2026-04-25 Reviewer: Claude Code Scope: Full codebase — all apps, packages/agents workers, API routes, server actions


Overview

This audit covers the entire monorepo. 14 duplication patterns were identified across:

  • 18 BullMQ worker files — each copy-pastes getClaudeAdapter() and the credits reserve/consume/release lifecycle
  • 21 API route files — all repeat the same pagination parsing boilerplate
  • 12+ DM API route files — all repeat the same tenant membership check
  • 30+ server action files — each wraps work in identical try/catch + { ok: boolean } returns
  • 2 content workers — slug generation and markdown extraction helpers defined twice each

Prior review: code-duplication-v1.md — resolved 15 of 16 items (D-1 through D-16).


Status

IDCategorySeverityFilesStatus
D-17Workers — Claude AdapterHigh18 filesOpen
D-18Workers — Insight start/stop boilerplateHigh8 filesOpen
D-19Workers — Credit lifecycle (reserve/consume/release)High16 filesOpen
D-20Workers — Worker factory start/stop patternMedium12 filesOpen
D-21Workers — JSON brace-depth extractorMedium4 filesOpen
D-22Workers — Markdown helpers (title/meta/slug/wordCount)Medium2 filesOpen
D-23API Routes — Pagination boilerplateMedium21 filesOpen
D-24API Routes — Tenant membership checkMedium12 filesOpen
D-25Actions — Inline fetch helper (dmFetch/apiFetch)High30+ filesOpen
D-26Actions — { ok: boolean } try/catch patternLow25+ filesOpen
D-27Actions — Audit log + requireSession boilerplateLow21 filesOpen
D-28Workers — mdToHtml conversionLow1 fileOpen
D-29Workers — publishAgentEvent progress callbackLow15 filesOpen
D-30Profile normalization (carried from v1 D-15)Low2 filesOpen

D-17 · Claude Adapter Definition — 18 Workers

Category: Workers
Severity: High — every new worker copies this block verbatim

Files:

packages/agents/src/workers/activity.worker.ts:36-47 packages/agents/src/workers/backlink-outreach-writer.worker.ts:28-39 packages/agents/src/workers/blog-faq-writer.worker.ts:29-40 packages/agents/src/workers/blog-writer.worker.ts:46-57 packages/agents/src/workers/content-repurposer.worker.ts:40-51 packages/agents/src/workers/content.worker.ts:36-47 packages/agents/src/workers/custom-report-writer.worker.ts:39-50 packages/agents/src/workers/gbp-post-writer.worker.ts:42-53 packages/agents/src/workers/landing-page-writer.worker.ts:45-56 packages/agents/src/workers/linkedin-ads-optimizer.worker.ts:18-29 packages/agents/src/workers/meta-ads-optimizer.worker.ts:18-29 packages/agents/src/workers/search-term-classifier.worker.ts:23-34 packages/agents/src/workers/social-post-writer.worker.ts:45-57 packages/agents/src/workers/strategy-writer.worker.ts (similar) packages/agents/src/workers/strategy.worker.ts (similar) packages/agents/src/workers/website-crawler.worker.ts (similar) packages/agents/src/workers/seo-optimizer.worker.ts (similar) packages/agents/src/workers/insights/insight-worker-base.ts:33-47 ← already centralised for insight workers

The duplicate block (repeated 17 times outside the base):

type ProgressEvent = { type: "tool_use"; toolName: string } | { type: "text"; preview: string }; async function getClaudeAdapter() { // eslint-disable-next-line @typescript-eslint/no-require-imports const mod = require("@leadmetrics/adapter-claude-local/server") as { execute: (ctx: unknown, onProgress?: (e: ProgressEvent) => void) => Promise<{ success: boolean; output?: string; error?: string; durationMs?: number; costUsd?: number; inputTokens?: number; outputTokens?: number; }>; }; return mod.execute; }

Proposed Fix: insight-worker-base.ts already has the canonical definition. Move it to the shared lib and re-export.

// packages/agents/src/lib/claude-adapter.ts (NEW FILE) export type ClaudeProgressEvent = | { type: "tool_use"; toolName: string } | { type: "text"; preview: string }; export type ClaudeExecuteFn = ( ctx: unknown, onProgress?: (e: ClaudeProgressEvent) => void ) => Promise<{ success: boolean; output?: string; error?: string; durationMs?: number; costUsd?: number; inputTokens?: number; outputTokens?: number; }>; export async function getClaudeAdapter(): Promise<ClaudeExecuteFn> { // eslint-disable-next-line @typescript-eslint/no-require-imports const mod = require("@leadmetrics/adapter-claude-local/server") as { execute: ClaudeExecuteFn }; return mod.execute; }

Then in every worker: import { getClaudeAdapter } from "../lib/claude-adapter";
And in insight-worker-base.ts: export { getClaudeAdapter } from "../lib/claude-adapter";


D-18 · Insight Worker Start/Stop Boilerplate — 8 Files

Category: Workers
Severity: High — the same 20-line pattern repeated verbatim in 8 files

Files:

packages/agents/src/workers/insights/facebook-insights.worker.ts:140-165 packages/agents/src/workers/insights/ga-insights.worker.ts:155-180 packages/agents/src/workers/insights/gbp-insights.worker.ts (similar) packages/agents/src/workers/insights/google-ads-insights.worker.ts (similar) packages/agents/src/workers/insights/gsc-insights.worker.ts (similar) packages/agents/src/workers/insights/instagram-insights.worker.ts (similar) packages/agents/src/workers/insights/linkedin-insights.worker.ts (similar) packages/agents/src/workers/insights/meta-ads-insights.worker.ts (similar)

The duplicate block:

let _worker: Worker<InsightJobData> | null = null; export function startFacebookInsightsWorkers(): void { _worker = new Worker<InsightJobData>(QUEUE_NAME, processJob, { connection: getRedisConnection(), concurrency: CONCURRENCY, }); _worker.on("failed", (job, err) => { log.error({ jobId: job?.id, insightId: job?.data.insightId, err }, "Facebook insights job failed"); }); log.info({ queue: QUEUE_NAME, concurrency: CONCURRENCY }, "Facebook insights worker started"); } export async function stopFacebookInsightsWorkers(): Promise<void> { await _worker?.close(); _worker = null; }

Proposed Fix: Add factory to insight-worker-base.ts:

// packages/agents/src/workers/insights/insight-worker-base.ts (add at bottom) export function createInsightWorker(config: { queueName: string; label: string; concurrency: number; processJob: (job: Job<InsightJobData>) => Promise<void>; }): { start: () => void; stop: () => Promise<void> } { let _worker: Worker<InsightJobData> | null = null; return { start() { _worker = new Worker<InsightJobData>(config.queueName, config.processJob, { connection: getRedisConnection(), concurrency: config.concurrency, }); _worker.on("failed", (job, err) => { log.error({ jobId: job?.id, insightId: job?.data.insightId, err }, `${config.label} job failed`); }); log.info({ queue: config.queueName, concurrency: config.concurrency }, `${config.label} worker started`); }, async stop() { await _worker?.close(); _worker = null; }, }; }

Then in each insight worker:

// packages/agents/src/workers/insights/facebook-insights.worker.ts const { start, stop } = createInsightWorker({ queueName: QUEUE_NAME, label: "Facebook insights", concurrency: CONCURRENCY, processJob, }); export const startFacebookInsightsWorkers = start; export const stopFacebookInsightsWorkers = stop;

D-19 · Credit Lifecycle (reserve/consume/release) — 16 Workers

Category: Workers
Severity: High — reserve + try + consume / catch + release repeated in 16 files with subtle variation

Files:

packages/agents/src/workers/activity.worker.ts packages/agents/src/workers/ai-visibility-monitor.worker.ts packages/agents/src/workers/backlink-outreach-writer.worker.ts packages/agents/src/workers/blog-writer.worker.ts packages/agents/src/workers/content-repurposer.worker.ts packages/agents/src/workers/content.worker.ts packages/agents/src/workers/custom-report-writer.worker.ts packages/agents/src/workers/gbp-post-writer.worker.ts packages/agents/src/workers/insights/brand-narrative-analyst.worker.ts packages/agents/src/workers/landing-page-writer.worker.ts packages/agents/src/workers/seo-optimizer.worker.ts packages/agents/src/workers/setup.worker.ts packages/agents/src/workers/social-post-designer.worker.ts packages/agents/src/workers/social-post-writer.worker.ts packages/agents/src/workers/strategy-writer.worker.ts packages/agents/src/workers/strategy.worker.ts

The duplicate pattern (from blog-writer.worker.ts:309-352):

await getOrCreateCreditBalance(db, tenantId); await reserveCredits(db, tenantId, getCreditCost("blog_post"), runId, "blog_post"); // ... do work ... // on failure: await releaseCredits(db, tenantId, getCreditCost("blog_post"), runId).catch(() => {}); throw new Error(result.error ?? "..."); // on success: await consumeCredits(db, tenantId, getCreditCost("blog_post"), runId, "blog_post").catch((err) => log.warn({ tenantId, err }, "Failed to consume credits") );

Proposed Fix: Extract to packages/agents/src/lib/with-credits.ts:

import { db } from "@leadmetrics/db"; import { getOrCreateCreditBalance, reserveCredits, consumeCredits, releaseCredits, getCreditCost } from "@leadmetrics/billing"; import type { CreditType } from "@leadmetrics/billing"; import { createLogger } from "@leadmetrics/logger"; const log = createLogger({ service: "credits", logFile: false }); export async function withCredits<T>( tenantId: string, runId: string, creditType: CreditType, fn: () => Promise<T> ): Promise<T> { await getOrCreateCreditBalance(db, tenantId); await reserveCredits(db, tenantId, getCreditCost(creditType), runId, creditType); try { const result = await fn(); await consumeCredits(db, tenantId, getCreditCost(creditType), runId, creditType).catch((err: unknown) => log.warn({ tenantId, runId, err }, "Failed to consume credits after successful run") ); return result; } catch (err) { await releaseCredits(db, tenantId, getCreditCost(creditType), runId).catch(() => {}); throw err; } }

Then in each worker:

const result = await withCredits(tenantId, runId, "blog_post", async () => { return await execute({ ... }); });

D-20 · Worker Factory Start/Stop Pattern — 12 Top-Level Workers

Category: Workers
Severity: Medium — identical Map<string, Worker> management in every top-level worker

Files (representative sample):

packages/agents/src/workers/blog-writer.worker.ts:516-549 packages/agents/src/workers/social-post-writer.worker.ts (similar) packages/agents/src/workers/landing-page-writer.worker.ts (similar) packages/agents/src/workers/content.worker.ts (similar) packages/agents/src/workers/strategy.worker.ts (similar) packages/agents/src/workers/activity.worker.ts (similar) ... 6 more

The duplicate block:

const _workers = new Map<string, Worker>(); export function startBlogWriterWorkers(): void { const queueName = `agent__blog-writer`; if (_workers.has(queueName)) return; const worker = new Worker<ActivityJobData>(queueName, processJob, { connection: getRedisConnection(), concurrency: 1, lockDuration: 360_000, maxStalledCount: 2, }); worker.on("failed", (job, err) => { log.error({ queueName, jobId: job?.id, err }, "Blog writer job failed"); }); worker.on("completed", (job) => { log.info({ queueName, jobId: job.id }, "Blog writer job completed"); }); _workers.set(queueName, worker); } export async function stopBlogWriterWorkers(): Promise<void> { await Promise.all(Array.from(_workers.values()).map(w => w.close())); _workers.clear(); }

Proposed Fix: Add factory to packages/agents/src/lib/worker-factory.ts:

import { Worker, type Job } from "bullmq"; import type { ActivityJobData } from "@leadmetrics/queue"; import { getRedisConnection } from "@leadmetrics/queue"; import { createLogger } from "@leadmetrics/logger"; const log = createLogger({ service: "worker-factory", logFile: false }); export function createActivityWorker(config: { queueName: string; label: string; concurrency?: number; lockDuration?: number; processJob: (job: Job<ActivityJobData>) => Promise<void>; }): { start: () => void; stop: () => Promise<void> } { const _workers = new Map<string, Worker>(); return { start() { if (_workers.has(config.queueName)) return; const worker = new Worker<ActivityJobData>(config.queueName, config.processJob, { connection: getRedisConnection(), concurrency: config.concurrency ?? 1, lockDuration: config.lockDuration ?? 360_000, maxStalledCount: 2, }); worker.on("failed", (job, err) => log.error({ queueName: config.queueName, jobId: job?.id, err }, `${config.label} job failed`)); worker.on("completed", (job) => log.info({ queueName: config.queueName, jobId: job.id }, `${config.label} job completed`)); _workers.set(config.queueName, worker); }, async stop() { await Promise.all(Array.from(_workers.values()).map(w => w.close())); _workers.clear(); }, }; }

Then in each worker:

const { start, stop } = createActivityWorker({ queueName: "agent__blog-writer", label: "Blog writer", processJob: processBlogWriterJob, }); export const startBlogWriterWorkers = start; export const stopBlogWriterWorkers = stop;

D-21 · JSON Brace-Depth Extractor — 4 Workers

Category: Workers
Severity: Medium — subtle bugs in one version don’t propagate fixes to others

Files:

packages/agents/src/workers/activity.worker.ts:54-86 ← most complete version packages/agents/src/workers/content-repurposer.worker.ts packages/agents/src/workers/strategy.worker.ts packages/agents/src/workers/gbp-post-writer.worker.ts ← simpler indexOf/lastIndexOf variant (fragile)

Problem: The activity.worker.ts version uses brace-depth matching (correct), while gbp-post-writer.worker.ts uses lastIndexOf("}") which breaks when the output contains trailing JSON objects or prose after the target object.

Proposed Fix: Move the canonical version to packages/agents/src/lib/json-extractor.ts:

export function extractJson<T>(output: string): T { const fenced = output.match(/```(?:json)?\s*([\s\S]*?)```/); const raw = (fenced ? fenced[1] : output).trim(); const start = raw.indexOf("{"); if (start === -1) throw new Error(`No JSON object in output. Preview: ${raw.slice(0, 200)}`); let depth = 0; let inString = false; let escape = false; let end = -1; for (let i = start; i < raw.length; i++) { const ch = raw[i]; if (escape) { escape = false; continue; } if (ch === "\\" && inString) { escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === "{") depth++; if (ch === "}") { depth--; if (depth === 0) { end = i; break; } } } if (end === -1) throw new Error(`Unclosed JSON in output. Preview: ${raw.slice(0, 200)}`); return JSON.parse(raw.slice(start, end + 1)) as T; }

Then: import { extractJson } from "../lib/json-extractor";


D-22 · Markdown Helpers (extractTitle / extractMetaTitle / slugify / countWords) — 2 Workers

Category: Workers
Severity: Medium — 4 identical utility functions defined in 2 files

Files:

packages/agents/src/workers/blog-writer.worker.ts:111-150 packages/agents/src/workers/landing-page-writer.worker.ts:122-160

The duplicated functions:

export function extractTitle(markdown: string): string { ... } export function extractMetaTitle(markdown: string): string | undefined { ... } export function extractMetaDescription(markdown: string): string | undefined { ... } export function slugify(title: string): string { ... } export function countWords(text: string): number { ... }

Proposed Fix: Move all five to packages/agents/src/lib/markdown-helpers.ts and export from there. slugify and countWords are generic enough to also live in packages/common/src/format.ts and be exported from its public index.

// packages/agents/src/lib/markdown-helpers.ts export function extractTitle(markdown: string, fallback = "Untitled"): string { return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? fallback; } export function extractMetaTitle(markdown: string): string | undefined { return markdown.match(/\*\*Meta Title:\*\*\s*(.+)/i)?.[1]?.trim(); } export function extractMetaDescription(markdown: string): string | undefined { return ( markdown.match(/\*\*Meta Description:\*\*\s*(.+)/i)?.[1]?.trim() ?? markdown.match(/^Meta Description:\s*(.+)/im)?.[1]?.trim() ); } export function slugify(title: string): string { return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 80); } export function countWords(text: string): number { return text.split(/\s+/).filter(w => w.length > 0).length; }

D-23 · Pagination Boilerplate — 21 API Route Files

Category: API Routes
Severity: Medium — one off-by-one error in one file won’t be noticed; inconsistent defaults across routes

Files (confirmed):

apps/api/src/routers/admin/audit.ts apps/api/src/routers/admin/billing.ts apps/api/src/routers/admin/content.ts apps/api/src/routers/blog.ts apps/api/src/routers/campaigns.ts apps/api/src/routers/dm/blog.ts apps/api/src/routers/dm/calendar.ts apps/api/src/routers/dm/content-briefs.ts apps/api/src/routers/dm/credits.ts apps/api/src/routers/dm/landing-pages.ts apps/api/src/routers/dm/media.ts apps/api/src/routers/dm/newsletters.ts apps/api/src/routers/dm/social.ts apps/api/src/routers/insights.ts apps/api/src/routers/landing-pages.ts apps/api/src/routers/leads.ts apps/api/src/routers/media-library.ts apps/api/src/routers/media.ts apps/api/src/routers/newsletters.ts apps/api/src/routers/social.ts apps/api/src/routers/tenant/credits.ts

The duplicate block:

const page = Math.max(1, parseInt(rawPage ?? "1", 10) || 1); const limit = Math.min(100, Math.max(1, parseInt(rawLimit ?? "20", 10) || 20)); const skip = (page - 1) * limit;

Proposed Fix: apps/api/src/lib/pagination.ts

export interface PaginationResult { page: number; limit: number; skip: number; } export function parsePagination(params: { page?: string; limit?: string }): PaginationResult { const page = Math.max(1, parseInt(params.page ?? "1", 10) || 1); const limit = Math.min(100, Math.max(1, parseInt(params.limit ?? "20", 10) || 20)); return { page, limit, skip: (page - 1) * limit }; }

Usage:

const { page, limit, skip } = parsePagination({ page: request.query.page, limit: request.query.limit });

D-24 · Tenant Membership Check — 12 DM API Route Files

Category: API Routes
Severity: Medium — one change to access rules must be made in 12 places

Files (confirmed subset):

apps/api/src/routers/dm/activities.ts apps/api/src/routers/dm/agents.ts apps/api/src/routers/dm/blog.ts apps/api/src/routers/dm/calendar.ts apps/api/src/routers/dm/content-briefs.ts apps/api/src/routers/dm/context.ts apps/api/src/routers/dm/landing-pages.ts apps/api/src/routers/dm/media.ts apps/api/src/routers/dm/newsletters.ts apps/api/src/routers/dm/reports.ts apps/api/src/routers/dm/social.ts apps/api/src/routers/dm/strategy.ts

The duplicate block:

if (actor.role !== "super_admin") { const membership = await db.tenantMember.findFirst({ where: { userId: actor.sub, tenantId } }); if (!membership) return apiError(reply, 403, "FORBIDDEN", "No access to this tenant."); }

Proposed Fix: The existing apps/api/src/lib/auth.ts already has requireTenantUser. Verify that function includes the super_admin bypass and use it consistently. If it doesn’t have the bypass, add it:

// apps/api/src/lib/auth.ts — add to existing file export async function assertTenantAccess( actor: { sub: string; role: string }, tenantId: string, reply: FastifyReply ): Promise<boolean> { if (actor.role === "super_admin") return true; const member = await db.tenantMember.findFirst({ where: { userId: actor.sub, tenantId } }); if (!member) { apiError(reply, 403, "FORBIDDEN", "No access to this tenant."); return false; } return true; }

Usage:

if (!await assertTenantAccess(actor, tenantId, reply)) return;

D-25 · Inline Fetch Helper (dmFetch / apiFetch) — 30+ Files

Category: Actions / API proxy routes
Severity: High — inconsistent error handling, auth header construction, and base URL resolution across all portals

Files (confirmed sample):

apps/dm/src/app/(dm)/profile/actions.ts apps/dm/src/app/(dm)/strategy/deliverable-plan/actions.ts apps/dm/src/app/api/activities/route.ts apps/dm/src/app/api/activities/grouped/route.ts apps/dm/src/app/api/activities/kanban/route.ts apps/dm/src/app/api/activities/calendar/route.ts apps/dm/src/app/api/agents/running/route.ts apps/dm/src/app/api/approvals/route.ts apps/dm/src/app/api/calendar/route.ts apps/dm/src/app/api/campaigns/[campaignId]/emails/[emailId]/route.ts ... 20+ more in dm apps/dashboard/src/app/(dashboard)/client-info/actions.ts apps/dashboard/src/app/(dashboard)/settings/brand-voice/actions.ts

The duplicate pattern:

// Defined inline in each file: async function dmFetch(token: string, path: string, method: string, body?: object) { const res = await fetch(`${API_URL}${path}`, { method, headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: body ? JSON.stringify(body) : undefined, }); return res; } // Then each caller does its own res.ok check with its own error message

Proposed Fix: One shared client per portal (since portals have different base URLs and cookie requirements):

// apps/dm/src/lib/api-client.ts (NEW FILE) const API_URL = process.env.API_URL ?? "http://localhost:3003"; export class DmApiClient { constructor(private token: string) {} async fetch<T = unknown>(path: string, method: string, body?: object): Promise<T> { const res = await fetch(`${API_URL}${path}`, { method, headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}` }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const err = await res.json().catch(() => ({})) as { error?: { message?: string } }; throw new Error(err.error?.message ?? `API error ${res.status}: ${path}`); } return res.json() as Promise<T>; } get<T>(path: string) { return this.fetch<T>(path, "GET"); } post<T>(path: string, body?: object){ return this.fetch<T>(path, "POST", body); } patch<T>(path: string, body?: object){ return this.fetch<T>(path, "PATCH", body); } delete<T>(path: string) { return this.fetch<T>(path, "DELETE"); } }

Same pattern for apps/dashboard/src/lib/api-client.ts. Then in actions:

const { token } = await requireAuth(); const api = new DmApiClient(token); const data = await api.post("/dm/v1/goals", { planId, title });

D-26 · { ok: boolean } Try/Catch Pattern — 25+ Action Files

Category: Actions
Severity: Low — structural duplication; low risk but adds noise

Files: 12 in apps/dashboard, 9 in apps/dm, 4 in apps/manage

The duplicate pattern:

export async function doSomething(...): Promise<{ ok: boolean; error?: string }> { try { // ... do work ... return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : "Unknown error" }; } }

Proposed Fix: Shared wrapper in each portal’s lib folder:

// apps/dashboard/src/lib/action.ts (NEW FILE — same pattern for dm, manage) export async function safeAction<T = void>( fn: () => Promise<T> ): Promise<{ ok: true; data: T } | { ok: false; error: string }> { try { return { ok: true, data: await fn() }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : "An error occurred" }; } }

Usage:

export async function updateProfile(data: ProfileData) { return safeAction(async () => { const { token } = await requireAuth(); await api.patch("/dm/v1/profile", data); revalidatePath("/profile"); }); }

D-27 · Audit Log + requireSession Boilerplate — 21 Dashboard Files

Category: Actions
Severity: Low — verbose but low coupling; writing the wrong action string is the main risk

Files: 21 files in apps/dashboard/src/app/(dashboard)/*/actions.ts

The duplicate pattern:

const { tenantId, userId, userName } = await requireSession(); // ... do work ... writeAuditLog({ action: "activity.status_changed", actorId: userId, actorName: userName, actorRole: "client", tenantId, resourceType: "activity", resourceId: activityId, resourceName: activity.label, metadata: { status }, });

Proposed Fix: A thin wrapper in apps/dashboard/src/lib/audit.ts:

export async function auditAction( action: string, resource: { type: string; id: string; name?: string }, metadata?: Record<string, unknown> ) { const { tenantId, userId, userName } = await requireSession(); writeAuditLog({ action, actorId: userId, actorName: userName, actorRole: "client", tenantId, resourceType: resource.type, resourceId: resource.id, resourceName: resource.name, metadata, }); }

D-28 · mdToHtml Conversion — email-writer

Category: Workers
Severity: Low — single file, but useful to share if landing pages / blog also need HTML output

File:

packages/agents/src/workers/email-writer.worker.ts:4-17

Proposed Fix: Move to packages/common/src/markdown.ts and export from packages/common/src/index.ts. This avoids re-implementation if a future worker needs HTML output from markdown.


D-29 · publishAgentEvent Progress Callback — 15 Workers

Category: Workers
Severity: Low — boilerplate, not a correctness risk; but the toolsUsed++ counter pattern is repeated identically

Files: 15 workers that call Claude (same set as D-17)

The duplicate block:

let toolsUsed = 0; const result = await execute(ctx, async (event) => { if (event.type === "tool_use") { toolsUsed++; await publishAgentEvent({ type: "agent:progress", tenantId, agentRole: "blog-writer", agentName: "Blog Writer", runId, timestamp: new Date().toISOString(), toolName: event.toolName, toolsUsed, message: `Using tool: ${event.toolName}`, }); } });

Proposed Fix: Extract to a factory in packages/agents/src/lib/claude-adapter.ts (same file as D-17 fix):

export function makeProgressHandler(opts: { tenantId: string; runId: string; agentRole: string; agentName: string; }): { handler: (event: ClaudeProgressEvent) => Promise<void>; getToolsUsed: () => number } { let toolsUsed = 0; return { async handler(event) { if (event.type === "tool_use") { toolsUsed++; await publishAgentEvent({ type: "agent:progress", ...opts, timestamp: new Date().toISOString(), toolName: event.toolName, toolsUsed, message: `Using tool: ${event.toolName}`, }); } }, getToolsUsed: () => toolsUsed, }; }

D-30 · Profile Data Normalization (carried from v1 D-15)

Category: Actions
Severity: Low — skipped in v1; still open

Files:

apps/dashboard/src/app/(dashboard)/settings/profile/actions.ts apps/dm/src/app/(dm)/profile/actions.ts

Both files trim strings and coerce empty strings to null for the same profile shape (name, phone, country, about, whatsapp). The difference is the target backend (BetterAuth DB vs. Fastify API), but the normalization itself is identical.

Proposed Fix: (unchanged from v1)

// packages/common/src/profile.ts export function normalizeProfileInput(data: { name: string; phone: string; country: string; about: string; whatsapp: string; }) { return { name: data.name.trim(), phone: data.phone.trim() || null, country: data.country.trim() || null, about: data.about.trim() || null, whatsapp: data.whatsapp.trim() || null, }; }

Do first (High — widest impact, unblocks others):

  1. D-17getClaudeAdapter: one-line import change in 17 files; zero runtime risk
  2. D-25DmApiClient: centralises error handling for all DM proxy routes; reduces silent 200-with-error bugs
  3. D-19withCredits: prevents credits leaking on partial failures in 16 workers

Do next (Medium — correctness risk in specific paths): 4. D-21extractJson: gbp-post-writer fragile lastIndexOf will silently corrupt on complex outputs 5. D-23parsePagination: ensures consistent defaults across 21 routes 6. D-24assertTenantAccess: single place to enforce tenant isolation rules 7. D-18 — Insight worker factory: 8-file boilerplate collapse 8. D-20 — Activity worker factory: 12-file boilerplate collapse 9. D-22 — Markdown helpers: consolidates 5 functions from 2 workers

Do when convenient (Low): 10. D-26 — safeAction wrapper 11. D-27 — auditAction wrapper 12. D-28 — mdToHtml to common 13. D-29 — makeProgressHandler factory 14. D-30 — normalizeProfileInput to common


Total estimated LOC to remove: ~1,200–1,500 lines across workers, routes, and actions.

© 2026 Leadmetrics — Internal use only